关于Flutter Desktop菜单的优化
关于Flutter Desktop菜单的优化
在Flutter中有一个显示菜单的方法showMenu
它可以显示一个菜单.
以此代码为例
showMenu(
context: context,
position: RelativeRect.fromLTRB(100.0, 200.0, 100.0, 200.0), // 在屏幕的哪个位置弹出
items: <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'Option 1',
child: Text('Option 1'),
),
PopupMenuItem<String>(
value: 'Option 2',
child: Text('Option 2'),
),
],
).then((value) {
if (value != null) {
setState(() {
_selection = value;
});
}
});
可以获得如下效果
表面看起来很完美, 但是它有个小缺陷, 就是点开菜单后我们没办法操作其它按钮, 比如我们点击Show Toast是没有效果的, 而是会先让菜单消失, 然后再次点击Show Toast才有效果.
但是这在桌面端上操作并不是很友好, 我们的操作系统和IDE的右键菜单都是不会影响其它事件的.
比如Android Studio的菜单效果是这样的, 它并不需要先消失菜单才能执行其它操作.
如果我们让Flutter实现菜单不影响其它按钮的点击那该如何实现呢?
首先分析Flutter自带的showMenu为什么不能穿透? 这是因为Flutter的showMenu其实也是在Overlay中插入的OverlayEntry, 虽然没有内容的地方是透明的, 但是这些透明的地方无法穿透, 有点击事件监听.
那么我们是不是只要创建一个OverlayEntry并且让透明区域可以穿透过去就行呢?
其实确实可以, 但是这样有个缺陷, 我们的菜单并不知道你点击了其它区域, 然后它不会自动消失.
如果我们想完美实现, 则我们需要自定义Widget.
我们可以实现这么一个Widget
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
typedef ChildOffsetGetter = Offset Function(Size parentSize, Size childSize);
typedef OnOutsideClick = void Function(Offset position);
class TransParentContainer extends SingleChildRenderObjectWidget {
final ChildOffsetGetter? childOffsetGetter;
final OnOutsideClick? onOutsideClick;
const TransParentContainer({super.key, required super.child, this.childOffsetGetter, this.onOutsideClick});
@override
RenderObject createRenderObject(BuildContext context) {
return TransParentRenderBox(childOffsetGetter, onOutsideClick);
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
super.updateRenderObject(context, renderObject);
final ro = (renderObject as TransParentRenderBox);
ro.childOffsetGetter = childOffsetGetter;
ro.onOutsideClick = onOutsideClick;
}
}
class TransParentRenderBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
ChildOffsetGetter? childOffsetGetter;
OnOutsideClick? onOutsideClick;
TransParentRenderBox(this.childOffsetGetter, this.onOutsideClick);
@override
void performLayout() {
child!.layout(constraints, parentUsesSize: true);
size = Size(constraints.maxWidth, constraints.maxHeight);
final getter = childOffsetGetter ?? defaultCenterOffsetGetter;
_getChildParentData().offset = getter(size, child!.size);
}
@override
void paint(PaintingContext context, Offset offset) {
//绘制子控件
context.paintChild(child!, offset + _getChildParentData().offset);
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (size.contains(position)) {
if (child!.hitTest(result, position: position - _getChildParentData().offset)) {
return true;
} else {
result.add(BoxHitTestEntry(this, position));
return false;
}
}
return false;
}
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
if (event is PointerDownEvent) {
onOutsideClick?.call(event.position);
}
}
Offset defaultCenterOffsetGetter(parentSize, childSize) {
return Offset((parentSize.width - childSize.width) / 2, (parentSize.height - childSize.height) / 2);
}
BoxParentData _getChildParentData() {
return child!.parentData as BoxParentData;
}
}
这里原理主要是在hitTest, 这个组件在点击事件不在子控件中时依然会对事件进行监听, 但是不会截断事件. 这样能实现既不影响其它按钮的点击, 菜单也会自动消失.
这里不再赘述自定义Widget相关的内容, 可以参考前一篇Flutter自定义View基础,重写SingleChildRenderObjectWidget
这里我们添加另一个展示菜单的方法
void _showMenu2(BuildContext context) {
var state = Overlay.of(context);
late OverlayEntry entry;
entry = OverlayEntry(builder: (c) {
return Container(
alignment: Alignment.topLeft,
child: TransParentContainer(
childOffsetGetter: (ps, cs) {
return Offset(100, 200);
},
onOutsideClick: (position) {
entry.remove();
},
child: Wrap(
children: [
Material(
elevation: 5.0,
shadowColor: Colors.grey,
color: Colors.white,
child: Column(
children: [
Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 10, bottom: 4),
child: Text(
"Menu Item 1",
style: TextStyle(fontSize: 24),
),
),
Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 10, bottom: 4),
child: Text(
"Menu Item 1",
style: TextStyle(fontSize: 24),
),
),
Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 10, bottom: 4),
child: Text(
"Menu Item 1",
style: TextStyle(fontSize: 24),
),
),
],
),
)
],
),
),
);
});
state.insert(entry);
}
最后再看一下效果对比
其中Open Menu2使用的新代码, 展示菜单后可以正常点击其它按钮, 点击其它按钮时菜单也能正常消失了.
转载自:https://juejin.cn/post/7242277683625836605