背景
最近要使用Flutter实现一个下拉菜单,需求就是,在当前组件下点击,其下方弹出一个菜单选项,如下图所示:
实现起来,貌似没什么障碍,在Flutter中本身就提供了弹出层PopupMenuButton组件和showMenu方法,于是开搞,代码如下:
PopupMenuButton<String>( initialValue: '下拉菜单一', child: const Text("下拉菜单"), itemBuilder: (context) { return <PopupMenuEntry<String>>[ const PopupMenuItem<String>( value: '下拉菜单一', child: Text('下拉菜单一'), ), const PopupMenuItem<String>( value: '下拉菜单二', child: Text('下拉菜单二'), ), const PopupMenuItem<String>( value: '下拉菜单三', child: Text('下拉菜单三'), ) ]; }, )
直接使用showMenu也行,代码如下:
showMenu( context: context, position: const RelativeRect.fromLTRB(0, 0, 0, 0), items: <PopupMenuEntry>[ const PopupMenuItem(value: "下拉菜单一",child: Text("下拉菜单一"),), const PopupMenuItem(value: "下拉菜单二",child: Text("下拉菜单二"),), const PopupMenuItem(value: "下拉菜单三",child: Text("下拉菜单三"),), ]);
PopupMenuButton运行看结果:
showMenu位置传的是左上角,这个就不贴图了。
看到效果后,我诧异了,这也不符合我的需求啊,直接把选项给我盖住了,这还得了,况且位置也不对啊,怎么搞?还好,无论使用PopupMenuButton还是showMenu,都给我们提供了位置。
PopupMenuButton设置位置:
offset: Offset(dx, dy)
showMenu设置位置:
position: const RelativeRect.fromLTRB(left, top, right, bottom)
使用位置后,我们再看效果:
dx设置为0,dy设置为50:
PopupMenuButton<String>( initialValue: '下拉菜单一', offset: const Offset(0, 50), itemBuilder: (context) { return <PopupMenuEntry<String>>[ const PopupMenuItem<String>( value: '下拉菜单一', child: Text('下拉菜单一'), ), const PopupMenuItem<String>( value: '下拉菜单二', child: Text('下拉菜单二'), ), const PopupMenuItem<String>( value: '下拉菜单三', child: Text('下拉菜单三'), ) ]; }, child: Text( "下拉菜单", key: _key, ), )
效果如下图:
这样看起来确实好多了,但是我的疑问就来了,如果我想实现在左边展示呢?在上边、右边,甚至左上右上,左下右下呢?通过坐标计算,确实能实现,但是计算起来麻烦,也不精确,很难作为上上策,再者,这种弹窗方式样式,在实际开发中也很难满足我们的需求。
既然原生的组件无法满足我们的需求,怎么搞?只有自定义一个组件了。
今天的内容大致如下:
1、自定义弹出层效果一览
2、弹出层逻辑实现
3、使用注意事项
4、源码
一、自定义弹出层效果一览
目前自定义的组件,可以在目标组件,左、上、右、下,左上、右上,左下、右下八个方向进行精确的弹出,当然了,除此之外,也可以动态的展示到自己想要的位置,并且弹出层效果可以自定义,效果是我弹出了一个黑色矩形,你可以弹出一个列表,一个图片等等。
二、弹出层逻辑实现
1、悬浮在其他顶部小部件之上
为了更好的展示弹出效果,和不影响UI层的相关逻辑,针对弹出层,我们可以悬浮在内容层之上,做透明处理即可,这里使用到了Overlay对象,它是一个类似悬浮小弹窗,如Toast,安卓的PopupWindow效果。
相关代码如下,创建OverlayEntry,并插入到Overlay中,这样就可以把OverlayEntry中构建的小部件叠加悬浮在其他顶部小部件之上。
OverlayState overlayState = Overlay.of(key.currentContext!); OverlayEntry _overlayEntry = OverlayEntry(); overlayState.insert(_overlayEntry!);
2、获取弹出目标组件的左上右下
所谓目标组件,就是,你想要在哪个组件(左上右下)进行弹出,确定了目标组件之后,为了使弹出层,精确的展示在目标组件的方位,需要拿到目标组件的位置,也就是左上右下的位置,这里使用到了GlobalKey作为获取方式,具体的位置信息获取如下:
///获取组件的位置 static WidgetSize getWidgetSize(GlobalKey key) { //获取组件的位置,在左上右下 final RenderBox renderBox = (key.currentContext?.findRenderObject() as RenderBox); final left = renderBox.localToGlobal(Offset.zero).dx; //左边 final top = renderBox.localToGlobal(Offset(renderBox.size.width, 0)).dy; final bottom = renderBox.localToGlobal(Offset(0, renderBox.size.height)).dy; final right = renderBox .localToGlobal(Offset(renderBox.size.width, renderBox.size.height)) .dx; return WidgetSize(left, top, right, bottom); }
创建记录位置对象,用来标记左上右下。
///组件对象,标记左上右下 class WidgetSize { double left; double top; double right; double bottom; WidgetSize(this.left, this.top, this.right, this.bottom); }
3、设置弹出层的位置
弹出层位置,这里利用到了Positioned组件,控制其left和top位置,基本上和PopupMenuButton类似,无非就是自己实现了位置的测量而已。
首先根据传递的属性WindowDirection,确定要设置的方位。
具体各个方位计算如下:
目标组件下边:
top坐标:目标组件的底部坐标+边距
left坐标:目标组件的右部坐标-弹出层的宽度/2-目标组件宽度/2
目标组件左边:
top坐标:目标组件的底部坐标-弹出层的高度/2-目标组件的高度/2
left坐标:目标组件的左边坐标-弹出层的宽度-边距
目标组件上边:
top坐标:目标组件的上边坐标-弹出层的高度-边距
left坐标:目标组件的右部坐标-弹出层的宽度/2-目标组件宽度/2
目标组件右边:
top坐标:目标组件的底部坐标-弹出层的高度/2-目标组件的高度/2
left坐标:目标组件的右边坐标+边距
目标组件左上:
top坐标:目标组件的底部坐标-弹出层的高度-目标组件的高度-边距
left坐标:目标组件的左边坐标-弹出层的宽度-边距
目标组件右上:
top坐标:目标组件的底部坐标-弹出层的高度-目标组件的高度-边距
left坐标:目标组件的左边坐标+边距
目标组件左下:
top坐标:目标组件的底部坐标+边距
left坐标:目标组件的左边坐标-弹出层的宽度-边距
目标组件右下:
top坐标:目标组件+边距
left坐标:目标组件右边的坐标+边距
var size = getWidgetSize(key); //获取在目标组件的位置 double widgetTop = 0.0; double widgetLeft = 0.0; switch (direction) { case WindowDirection.bottom: //下面 widgetTop = size.bottom + margin; widgetLeft = size.right - childWidth / 2 - ((size.right - size.left) / 2); break; case WindowDirection.left: //左面 widgetTop = size.bottom - childHeight / 2 - ((size.bottom - size.top) / 2); widgetLeft = size.left - childWidth - margin; break; case WindowDirection.top: //上面 widgetTop = size.top - childHeight - margin; widgetLeft = size.right - childWidth / 2 - ((size.right - size.left) / 2); break; case WindowDirection.right: //右面 widgetTop = size.bottom - childHeight / 2 - ((size.bottom - size.top) / 2); widgetLeft = size.right + margin; break; case WindowDirection.topLeft: //左上 widgetTop = size.bottom - childHeight - (size.bottom - size.top) - margin; widgetLeft = size.left - childWidth - margin; break; case WindowDirection.topRight: //右上 widgetTop = size.bottom - childHeight - (size.bottom - size.top) - margin; widgetLeft = size.right + margin; break; case WindowDirection.bottomLeft: //左下 widgetTop = size.bottom + margin; widgetLeft = size.left - childWidth - margin; break; case WindowDirection.bottomRight: //右下 widgetTop = size.bottom + margin; widgetLeft = size.right + margin; break; case WindowDirection.none: //取消 自己测量位置 widgetTop = top; widgetLeft = left; break; }
三、使用注意事项
1、为了能够精确的设置弹出层的位置,其弹出层的宽度和高度是必须要传递的,也就是childWidth和childHeight属性。
2、如果想自己设置位置,可以不传childWidth和childHeight,设置direction为WindowDirection.none,并且left和top坐标需要传递。
3、margin属性设置弹出层距离目标组件的距离。
四、源码
源码地址
github.com/AbnerMing888/flutter_widget/blob/master/lib/utils/popup_window.dart
使用方式
PopupWindow.create( _key, const BaseWidget( width: 100, height: 100, backgroundColor: Colors.black, ), direction: direction, margin: 10, childWidth: 100, childHeight: 100);
参数介绍
属性 | 类型 | 概述 |
---|---|---|
key | GlobalKey | 目标组件的key |
child | Widget | 弹出层 |
childWidth | double | 弹出层的宽 |
childHeight | double | 弹出层的高 |
direction | WindowDirection | 位置:left//左top//上right//右bottom//下topLeft, //左上角topRight, //右上角bottomLeft, //左下bottomRight, //右下none//取消位置,自己定义 |
left | double | 相对于屏幕的左侧坐标 |
top | double | 相对于屏幕的顶部坐标 |
margin | double | 弹出层距离目标组件的距离 |
到此这篇关于Flutter自定义实现弹出层的示例代码的文章就介绍到这了,更多相关Flutter弹出层内容请搜索好代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持好代码网!