让我们来按顺序解决这几个问题,首先我们来解决淡化背景这个问题,Flutter显示对话框时是通过push一个新的Route(路由)来实现,那路由Route又是怎么管理界面的呢,其实路由是由Overlay管理的,Overlay管理一堆OverlayEntry,push一个新页面,会创建Overlay,按照这个思路,我们可以在当前的Overlay插入一个OverlayEntry来覆盖当前页面从而达到淡化页面的效果,首先我们先构建测试用的GridView
class ContextMenuPage extends StatelessWidget {
ContextMenuPage({this.items});
OverlayEntry _overlayEntry;
final List<String> items;
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.0,
),
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return LayoutBuilder(
builder: (context, _) => GestureDetector(
onLongPress: () {
_showLayer(context);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
border: Border.all(
width: 1.0 / MediaQuery.of(context).devicePixelRatio,
color: Colors.grey.withOpacity(0.5)),
),
alignment: Alignment.center,
//color: Colors.red,
child: Text('item ${index}'),
),
),
);
});
}
代码不多,值得一提的是,我们这里使用LayoutBuilder,LayoutBuilder构造函数中的builder参数的原型是
typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints);
constraints为父控件追加的约束条件,context其实就是布局渲染对象,我们可以通过它获取Grid Item的RenderBox,RenderBox包含位置和大小信息,至于怎么获取该信息后面会讲解,代码中现在只剩下响应长按事件的方法_showLayer没实现,这也是我们本博的关键代码。
那如何定位长按的Grid Item的位置和大小信息呢, 就是通过上面提到的context,还可以通过设置Widget key属性为 GlobalKey,然后通过GlobalKey的currentContext属性获取,其实该属性和context值相同,废话不多说,上代码
Rect _getPosition(BuildContext context) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset topLeft = box.size.topLeft(box.localToGlobal(Offset.zero));
final Offset bottomRight =
box.size.bottomRight(box.localToGlobal(Offset.zero));
return Rect.fromLTRB(
topLeft.dx, topLeft.dy, bottomRight.dx, bottomRight.dy);
}
Flutter 目前主要有两种布局方式
其渲染对象分别对应RenderBox,RenderSliver,Sliver布局主要针对scrollable widget,比较复杂,超出本博的范畴,这里提到布局方式,主要是如果遇到_getPosition调用崩溃问题,主要原因就是context对应的渲染对象不是RenderBox所致
位置大小信息获取到了,那么我们怎么在指定的位置显示上下文菜单呢,方法有多种,我们通过Stack + Positioned来显示,具体怎么构建该布局,大家可以自己试一试,下面主要是通过另一种方法来构建:CustomSingleChildLayout,
CustomSingleChildLayout提供了一个控制单一child布局的SingleChildLayoutDelegate ,这个delegate可以控制这些因素:
SingleChildLayoutDelegate是一个抽象类,我们需要通过派生来实现它
class _ContextMenuLayoutDelegate extends SingleChildLayoutDelegate {
_ContextMenuLayoutDelegate({this.position});
final Rect position;
// 获取child的size constraint, 参数是传入的父视图constraint
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints.loose(Size(position.width, position.height));
}
// 定位child
@override
Offset getPositionForChild(Size size, Size childSize) {
return Offset(position.left, position.top);
}
// 是否需要重新布局
@override
bool shouldRelayout(_ContextMenuLayoutDelegate oldDelegate) {
return position != oldDelegate.position;
}
}
准备工作做好后,就开始进入创建上下文菜单的流程,我们创建OverlayEntry来显示上下文菜单
_createContextMenuOverlayEntry({Rect widgetPosition}) {
return OverlayEntry(
builder: (BuildContext context) {
return GestureDetector(
onTap: _removeOverlay,
child: Material(
color: Colors.black54,
child: CustomSingleChildLayout(
delegate: _ContextMenuLayoutDelegate(
position: widgetPosition,
),
child: Container(
alignment: Alignment.bottomCenter,
color: Colors.green.withOpacity(0.5),
child: Row(
children: <Widget>[
FlatButton(child: const Icon(Icons.add, color: Colors.white)),
FlatButton(child: const Icon(Icons.delete, color: Colors.white))
],
),
)),
),
);
},
);
}
点击淡化的背景时需要移除OverlayEntry
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
下面我们来实现显示OverlayEntry
void _showLayer(BuildContext context) {
OverlayState overlayState = Overlay.of(context);
final Rect overlayPosition = _getPosition(overlayState.context);
final Rect widgetPosition = _getPosition(context).translate(
-overlayPosition.left,
-overlayPosition.top,
);
_overlayEntry = _createContextMenuOverlayEntry(widgetPosition: widgetPosition);
overlayState.insert(
_overlayEntry,
);
}
Overlay.of(context)获取当前的OverlayState, 上下文菜单位置相对Overlay需要通过translate转换坐标系,一切就绪后,直接OverlayState中插入OverlayEntry, 到此我们就完成了整个上下文OverlayEntry的整个实现流程。Overlay可以实现很多普通方式难以实现的效果, 比如我们可以实现以下功能
具体实现方法,我想通过本文的学习大家可能找到实现方法,大家可以自己练手, 有空的话我会抽空再写一篇关于Overlay使用的文章