Flutter Web:需要带有子菜单的菜单

Posted

技术标签:

【中文标题】Flutter Web:需要带有子菜单的菜单【英文标题】:Flutter Web : Need menu with sub menu 【发布时间】:2020-07-02 20:31:26 【问题描述】:

如何使用 Flutter web 构建带有子菜单的菜单,如下图所示

【问题讨论】:

【参考方案1】:

在标准 Flutter 库 (ma​​terial.dart) 中,有一个抽象类 PopupMenuEntryPopupMenuButton 的所有子类都继承自该类。目前,有三个具体的子类:PopupMenuItem(你经常看到的常规项目)、'CheckedPopupMenuItem'(常规项目+复选框)和PopupMenuDivider(水平线)。没有什么能阻止我们实现自己的子类。

使用@AbhilashChandran 的第一个答案并稍作修改,我们可以创建以下泛型类:

import 'package:flutter/material.dart';

/// An item with sub menu for using in popup menus
/// 
/// [title] is the text which will be displayed in the pop up
/// [items] is the list of items to populate the sub menu
/// [onSelected] is the callback to be fired if specific item is pressed
/// 
/// Selecting items from the submenu will automatically close the parent menu
/// Closing the sub menu by clicking outside of it, will automatically close the parent menu
class PopupSubMenuItem<T> extends PopupMenuEntry<T> 
  const PopupSubMenuItem(
    @required this.title,
    @required this.items,
    this.onSelected,
  );

  final String title;
  final List<T> items;
  final Function(T) onSelected;

  @override
  double get height => kMinInteractiveDimension; //Does not actually affect anything

  @override
  bool represents(T value) => false; //Our submenu does not represent any specific value for the parent menu

  @override
  State createState() => _PopupSubMenuState<T>();


/// The [State] for [PopupSubMenuItem] subclasses.
class _PopupSubMenuState<T> extends State<PopupSubMenuItem<T>> 
  @override
  Widget build(BuildContext context) 
    return PopupMenuButton<T>(
      tooltip: widget.title,
      child: Padding(
        padding: const EdgeInsets.only(left: 16.0, right: 8.0, top: 12.0, bottom: 12.0),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            Expanded(
              child: Text(widget.title),
            ),
            Icon(
              Icons.arrow_right,
              size: 24.0,
              color: Theme.of(context).iconTheme.color,
            ),
          ],
        ),
      ),
      onCanceled: () 
        if (Navigator.canPop(context)) 
          Navigator.pop(context);
        
      ,
      onSelected: (T value) 
        if (Navigator.canPop(context)) 
          Navigator.pop(context);
        
        widget.onSelected?.call(value);
      ,
      offset: Offset.zero, //TODO This is the most complex part - to calculate the correct position of the submenu being populated. For my purposes is does not matter where exactly to display it (Offset.zero will open submenu at the poistion where you tapped the item in the parent menu). Others might think of some value more appropriate to their needs.
      itemBuilder: (BuildContext context) 
        return widget.items
            .map(
              (item) => PopupMenuItem<T>(
            value: item,
            child: Text(item.toString()), //MEthod toString() of class T should be overridden to repesent something meaningful
          ),
        )
            .toList();
      ,
    );
  

这个类的使用简单直观:

PopupMenuButton<int>(
  icon: Icon(Icons.arrow_downward),
  tooltip: 'Parent menu',
  onSelected: (value) 
    //Do something with selected parent value
  ,
  itemBuilder: (BuildContext context) 
    return <PopupMenuEntry<int>>[
      PopupMenuItem<int>(
        value: 10,
        child: Text('Item 10'),
      ),
      PopupMenuItem<int>(
        value: 20,
        child: Text('Item 20'),
      ),
      PopupMenuItem<int>(
        value: 50,
        child: Text('Item 50'),
      ),
      PopupSubMenuItem<int>(
        title: 'Other items',
        items: [
          100,
          200,
          300,
          400,
          500,
        ],
        onSelected: (value) 
          //Do something with selected child value
        ,
      ),
    ];
  ,
)

结果是这样的:

这种方法有几个缺点:

显然,子菜单未显示在您希望显示的位置 - 可能需要通过一些复杂的计算来处理; 尽管可以将多个子菜单放在另一个子菜单中,但我不确定当底部的关闭(或选择值)时顶部的子菜单是否会正确关闭 - 可能会通过Navigator 调用和检查来处理; 父菜单和子菜单都应该具有相同类型的值 - 可以使用子类来处理; 需要指定onSelected 方法两次(父菜单和子菜单) - 可以使用方法或闭包来处理; 其他一些我可能没有想到的事情 - 可以通过在下面编写 cmets 来处理。

PopupSubMenuItem 类可以扩展为包含 final String Function(T) formatter; 之类的内容,以便以有意义的方式表示您的值,但为了简洁起见,省略了此功能。

【讨论】:

非常感谢!我修改了您的代码并在我的项目中使用,它显示得非常好!只是一点建议:您可以使用 Navigator.pop(context, value);在您的 onSelected 处理程序中将所选值传递给父菜单,因此您不需要像您所说的那样编写两次方法。【参考方案2】:

到目前为止,flutter 没有 NestedMenu 小部件。然而,现有的小部件可以帮助构建可以具有不同子菜单的自定义菜单。在这个dartPad 中,我使用两种不同的想法创建了子菜单。

    使用现有的PopupMenuButon 小部件嵌套在另一个中,并使用offset 属性定位子菜单。 使用全局showMenu函数,可以将菜单定位在屏幕的任何位置。

您可以检查下面显示的两个实现。请注意,这两种方法都有其自身的警告。就像关闭弹出窗口并处理选择和取消一样。然而,这只是为了展示它在颤振中的可能性,处理这些情况超出了这个答案的范围。

嵌套弹出菜单按钮

enum WhyFarther  harder, smarter, selfStarter, tradingCharter 

class MainMenu extends StatefulWidget 
  MainMenu(Key key, this.title) : super(key: key);

  final String title;

  @override
  _MainMenuState createState() => _MainMenuState();


class _MainMenuState extends State<MainMenu> 
  WhyFarther _selection = WhyFarther.smarter;

  @override
  Widget build(BuildContext context) 
// This menu button widget updates a _selection field (of type WhyFarther,
// not shown here).
    return Padding(
      padding: const EdgeInsets.all(2.0),
      child: PopupMenuButton<WhyFarther>(
        child: Material(
          textStyle: Theme.of(context).textTheme.subtitle1,
          elevation: 2.0,
          child: Container(
            padding: EdgeInsets.all(8),
            child: Text(widget.title),
          ),
        ),
        onSelected: (WhyFarther result) 
          setState(() 
            _selection = result;
          );
        ,
        itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[
          const PopupMenuItem<WhyFarther>(
            value: WhyFarther.harder,
            child: Text('Working a lot harder'),
          ),
          const PopupMenuItem<WhyFarther>(
            value: WhyFarther.smarter,
            child: Text('Being a lot smarter'),
          ),
          const PopupMenuItem<WhyFarther>(
            value: WhyFarther.selfStarter,
            child: SubMenu('Sub Menu is too long'),
          ),
          const PopupMenuItem<WhyFarther>(
            value: WhyFarther.tradingCharter,
            child: Text('Placed in charge of trading charter'),
          ),
        ],
      ),
    );
  


class SubMenu extends StatefulWidget 
  final String title;
  const SubMenu(this.title);

  @override
  _SubMenuState createState() => _SubMenuState();


class _SubMenuState extends State<SubMenu> 
  WhyFarther _selection = WhyFarther.smarter;

  @override
  Widget build(BuildContext context) 
//     print(rendBox.size.bottomRight);

    return PopupMenuButton<WhyFarther>(
      child: Row(
        children: <Widget>[
          Text(widget.title),
          Spacer(),
          Icon(Icons.arrow_right, size: 30.0),
        ],
      ),
      onCanceled: () 
        if (Navigator.canPop(context)) 
          Navigator.pop(context);
        
      ,
      onSelected: (WhyFarther result) 
        setState(() 
          _selection = result;
        );
      ,
      // how much the submenu should offset from parent. This seems to have an upper limit.
      offset: Offset(300, 0),
      itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[
        const PopupMenuItem<WhyFarther>(
          value: WhyFarther.harder,
          child: Text('Working a lot harder'),
        ),
        const PopupMenuItem<WhyFarther>(
          value: WhyFarther.smarter,
          child: Text('Being a lot smarter'),
        ),
        const PopupMenuItem<WhyFarther>(
          value: WhyFarther.selfStarter,
          child: Text('Being a lot smarter'),
        ),
        const PopupMenuItem<WhyFarther>(
          value: WhyFarther.tradingCharter,
          child: Text('Placed in charge of trading charter'),
        ),
      ],
    );
  

使用 showMenu 方式

class CustomMenu extends StatefulWidget 
  const CustomMenu(Key key, this.title, this.rootMenu=false) : super(key: key);

  final String title;
  final bool rootMenu;

  @override
  _CustomMenuState createState() => _CustomMenuState();


class _CustomMenuState extends State<CustomMenu> 
  WhyFarther _selection = WhyFarther.smarter;

  @override
  Widget build(BuildContext context) 
// This menu button widget updates a _selection field (of type WhyFarther,
// not shown here).

    return Padding(
      padding: const EdgeInsets.all(2.0),
      child: GestureDetector(
        onTap: () 

          // This offset should depend on the largest text and this is tricky when
          // the menu items are changed
          Offset offset = widget.rootMenu?Offset.zero:Offset(-300,0);

          final RenderBox button = context.findRenderObject();
          final RenderBox overlay =
              Overlay.of(context).context.findRenderObject();
          final RelativeRect position = RelativeRect.fromRect(
            Rect.fromPoints(
              button.localToGlobal(Offset.zero, ancestor: overlay),
              button.localToGlobal(button.size.bottomRight(Offset.zero),
                  ancestor: overlay),
            ),
            offset & overlay.size,
          );
          showMenu(            
              context: context,
              position: position,
              items: <PopupMenuEntry<WhyFarther>>[
                const PopupMenuItem<WhyFarther>(
                  value: WhyFarther.harder,
                  child: Text('Working a lot harder'),
                ),
                const PopupMenuItem<WhyFarther>(
                  value: WhyFarther.smarter,
                  child: Text('Being a lot smarter'),
                ),
                const PopupMenuItem<WhyFarther>(
                  value: WhyFarther.selfStarter,
                  child: CustomMenu(title: 'Sub Menu long'),
                ),
                const PopupMenuItem<WhyFarther>(
                  value: WhyFarther.tradingCharter,
                  child: Text('Placed in charge of trading charter'),
                ),
              ]).then((selectedValue)
            // do something with the value
            if(Navigator.canPop(context)) Navigator.pop(context);
          );
        ,
        child: Material(
              textStyle: Theme.of(context).textTheme.subtitle1,
              elevation: widget.rootMenu?2.0:0.0,              
              child: Padding(
                padding: widget.rootMenu? EdgeInsets.all(8.0):EdgeInsets.all(0.0),
                child: Row(
              children: <Widget>[
                Text(widget.title),
                if(!widget.rootMenu)
                  Spacer(),
                if(!widget.rootMenu)
                  Icon(Icons.arrow_right),                
              ],
            ),
              ),)

      ),
    );
  


【讨论】:

您的回答是一种解决方案,非常感谢。

以上是关于Flutter Web:需要带有子菜单的菜单的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Vuetify 创建带有子菜单的菜单?

带有可切换子菜单的 Bootstrap 3 垂直菜单

带有多个子菜单下拉菜单的 jQuery 导航菜单关闭父菜单项

为带有子菜单的移动菜单添加关闭功能

带有悬停子菜单的固定菜单有点关闭

带有子菜单的大量性能下降渲染菜单