flutter - 将小部件放置在边界框之外

标签 flutter flutter-layout flutter-web

我的应用程序显示各种 Container() Widget()位于某个 View 中的多个列中。

我尝试在 Container() 中放置一些图标提供删除、最小化等操作。不幸的是,这在 native 目标上看起来不太好。

因此,我想保持视觉外观不变,并在实际 Container() 上方显示操作菜单 一旦鼠标指针移到 Container() 上.

此菜单将位于所有其他小部件之上,是非模式的,一旦指针离开 Container() 的边界框,该菜单就会消失。 。 Containers()不应更改大小和位置。

使用MouseRegion() ,我会让菜单出现和消失。

我可以放一些Widget()Container() 的边界矩形之外[或其他小部件)?理想情况下,我想将其相对于另一个边界框放置。

更新 2022-03-24

创建了一个 OverlayMenu() 类,它呈现如下内容:

enter image description here

用法:

OverlayMenu(
              actionWidget: 
                  const Icon(Icons.settings, color: Colors.blue, size: 20), 
              callbacks: [
                () {
                  // Click on icons 1 action
                },
                () {
                  // Click on icon 2 action
                }
                ],
              icons: [
                 Icons.delete, Icons.tv
                ],
              leftOffset: StoreProvider.of<EModel>(context)
                  .state
                  .columnWidth
                  .toDouble())

以及OverlayMenu()的实现:

导入'package:flutter/material.dart';

class OverlayMenu {

  List<IconData> icons;
  List<Function> callbacks;
  Widget actionWidget;

  double leftOffset = 0.0;
  bool mayShowMenu = true;

  OverlayMenu({ required this.actionWidget, required this.icons, required this.callbacks, required this.leftOffset  });

  Widget insert( BuildContext context ) {

    return MouseRegion(
      onEnter: (_) {
        if (mayShowMenu) {
          _showOverlay( context );
        }
      },
      onExit: (_) {
        mayShowMenu = true;
      },
      child: const Icon(Icons.settings, color: Colors.blue, size: 20),
    );

  }


  ///
  ///
  ///
  void _showOverlay(BuildContext outerContext ) async {
    final renderBox = outerContext.findRenderObject() as RenderBox;
    var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);

    assert( icons.length == callbacks.length, 'Need to provide as many icons as callbacks' );

    List<Widget> actionElements = List.empty( growable: true );

    for( var n=0; n<icons.length; n++ ) {

      actionElements.add( GestureDetector(
          onTap: () {
            callbacks[ n ]();
          },
          child: Icon( icons[ n ], size: 22 ))
      );

    }

    // Declaring and Initializing OverlayState
    // and OverlayEntry objects
    OverlayState? overlayState = Overlay.of(outerContext);
    OverlayEntry? overlayEntry;
    overlayEntry = OverlayEntry(builder: (context) {
      // You can return any widget you like here
      // to be displayed on the Overlay
      return Stack(children: [
        Positioned(
          top: offset.dy,
          left: offset.dx +
              leftOffset -
              40,
          child: MouseRegion(
              onExit: (_) {
                overlayEntry?.remove();
                print(DateTime.now().toString() + ' ovl: removed');
              },
              cursor: SystemMouseCursors.contextMenu,
              child: Material(
                  child:Container(
                      decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(4),
                          border: Border.all(
                            color: Colors.grey.shade300,
                            width: 1,
                          ),
                          color: Colors.white,
                          boxShadow: [
                            BoxShadow(
                                color: Colors.grey.withOpacity(0.1),
                                spreadRadius: 4,
                                blurRadius: 4,
                                offset: const Offset( 4,4 ),
                                blurStyle: BlurStyle.normal),
                          ]
                      ),
                      child: SizedBox(
                          width: 60,
                          height: 60,
                          child: Wrap(spacing: 4, children: actionElements
                          ))))),
        ),
      ]);
    });

    // Inserting the OverlayEntry into the Overlay
    overlayState?.insert(overlayEntry);
    mayShowMenu = true;

    print(DateTime.now().toString() + ' ovl: inserted');

  }

}

最佳答案

您可以用来实现您想要的目标的一种方法是 Overlay小部件,因为它是非模式的,也不需要更改布局/大小来命中可测试的项目。

根据你的问题,我认为这个流程就是想要的:

animated image showing sample

指针进入小部件后插入覆盖条目,并在指针离开后将其删除

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Stack(
          children: [
            MouseRegion(
              onEnter: (_) {
                Overlay.of(context)!.insert(this._overlayEntry);
              },
              onExit: (_) => clear(),
              child: FloatingActionButton(
                key: buttonKey,
                onPressed: () {},
              ),
            ),
          ],
        ),
      ),
    );
  } 

这就是我们删除条目的方式,检查是否可以删除以及延迟(用于平滑):

  Future<void> clear() async {
    if (!keepMenuVisible) {
      await Future.delayed(Duration(milliseconds: 200));
      if (!keepMenuVisible) {
        _overlayEntry.remove();
      }
    }
  }

额外的延迟是为了确保菜单不会 react 性地绝望,而是让它变得更加平滑。

keepMenuVisible 用于锁定菜单并在菜单本身悬停时保持其可见。

最后,我们创建条目并将项目相对于主小部件(在本例中为 FAB)放置:

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
      _overlayEntry = _createOverlayEntry();
    });
  }

  OverlayEntry _createOverlayEntry() {
    final renderBox = buttonKey.currentContext!.findRenderObject() as RenderBox;
    var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);

    return OverlayEntry(
      builder: (_) => Positioned(
        left: offset.dx,
        top: offset.dy + size.height + 5.0,
        width: 200,
        child: MouseRegion(
          onEnter: (_) {
            keepMenuVisible = true;
          },
          onHover: (_) {
            keepMenuVisible = true;
          },
          onExit: (_) async {
            keepMenuVisible = false;
            clear();
          },
          child: Material(
            elevation: 4.0,
            child: ListView(
              padding: EdgeInsets.zero,
              shrinkWrap: true,
              children: <Widget>[
                ListTile(
                  onTap: () => print('tap action 1'),
                  title: Text('Action 1'),
                ),
                ListTile(
                  onTap: () => print('tap action 2'),
                  title: Text('Action 2'),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }

查看完整示例 here

关于flutter - 将小部件放置在边界框之外,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71550736/

相关文章:

flutter - Flutter 的自定义 CircularProgressIndicator

flutter 。将一个 Draggable 项拖到另一个 Draggable 项上

flutter - Flutter for Web 生成的标签,<flt-*> 是什么?

flutter - 运行flutter pub升级的错误在5分钟后停止

android-studio - 如何使用 https 地址而不是 http : 在 Web 上运行 Flutter 应用程序

flutter - 在 flutter 中将字符串读取为 dart 代码的方法?

listview - Flutter 类型 'List<dynamic>' 不是类型 'list<Widget>' 的子类型

flutter - 如何找到为 flutter Web 应用程序提供服务的 URL?

flutter - 文档与集合

flutter - 无效的InputConnection