likes
comments
collection
share

关于Flutter Desktop菜单的优化

作者站长头像
站长
· 阅读数 39

关于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;
        });
      }
    });

可以获得如下效果

关于Flutter Desktop菜单的优化

表面看起来很完美, 但是它有个小缺陷, 就是点开菜单后我们没办法操作其它按钮, 比如我们点击Show Toast是没有效果的, 而是会先让菜单消失, 然后再次点击Show Toast才有效果.

关于Flutter Desktop菜单的优化

但是这在桌面端上操作并不是很友好, 我们的操作系统和IDE的右键菜单都是不会影响其它事件的.

比如Android Studio的菜单效果是这样的, 它并不需要先消失菜单才能执行其它操作.

关于Flutter Desktop菜单的优化

如果我们让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);
  }

最后再看一下效果对比

关于Flutter Desktop菜单的优化

其中Open Menu2使用的新代码, 展示菜单后可以正常点击其它按钮, 点击其它按钮时菜单也能正常消失了.

转载自:https://juejin.cn/post/7242277683625836605
评论
请登录