Flutter 實作長按顯示上下文自定義菜單
很多場景需要我們點選或長按顯示操作菜單,如點選gridview中的一項顯示上下文菜單,本博就以gridview來作為例子,具體效果如下

要在指定的位置顯示上下文菜單,需要解決以下幾個問題
- 淡化目前頁面,如同我們顯示對話框淡化背景一樣
- 定位上下文菜單顯示位置
- 點選淡化的背景,關閉上下文菜單,回複之前的狀态
讓我們來按順序解決這幾個問題,首先我們來解決淡化背景這個問題,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參數的原型是
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 目前主要有兩種布局方式
- Box布局
- Sliver布局
其渲染對象分别對應RenderBox,RenderSliver,Sliver布局主要針對scrollable widget,比較複雜,超出本博的範疇,這裡提到布局方式,主要是如果遇到_getPosition調用崩潰問題,主要原因就是context對應的渲染對象不是RenderBox所緻
位置大小資訊擷取到了,那麼我們怎麼在指定的位置顯示上下文菜單呢,方法有多種,我們通過Stack + Positioned來顯示,具體怎麼建構該布局,大家可以自己試一試,下面主要是通過另一種方法來建構:CustomSingleChildLayout,
CustomSingleChildLayout提供了一個控制單一child布局的SingleChildLayoutDelegate ,這個delegate可以控制這些因素:
- 可以控制child的布局constraints
- 可以控制child的位置;
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可以實作很多普通方式難以實作的效果, 比如我們可以實作以下功能
- hero動畫效果
- 顯示Toast消息
- 頁面上浮一層功能介紹
具體實作方法,我想通過本文的學習大家可能找到實作方法,大家可以自己練手, 有空的話我會抽空再寫一篇關于Overlay使用的文章