关于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使用的新代码, 展示菜单后可以正常点击其它按钮, 点击其它按钮时菜单也能正常消失了.

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYVRRVVs' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片