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

Place widget outside of bounding box

我的应用在特定视图的多个列中显示各种 Container() Widget()

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

因此,我想保持视觉外观不变,并在鼠标指针移到 Container().

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

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

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

更新 2022-03-24

创建了一个 OverlayMenu() class 呈现如下内容:

用法:

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 小部件,因为它是 non-modal 并且不需要 layout/size 更改即可获得可测试的项目。

根据你的问题,我假设这个流程是想要的:

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

  @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();
      }
    }
  }

额外的延迟用于确保菜单不会被动地绝望,而是让它更平滑。

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