Flutter Web: нужно меню с подменю - PullRequest
1 голос
/ 22 марта 2020

Как создать меню с подменю, как показано ниже на изображении с использованием веб-флаттера

enter image description here

Ответы [ 2 ]

1 голос
/ 22 марта 2020

На данный момент у флаттера нет виджета NestedMenu . Однако существующие виджеты могут помочь создать пользовательское меню, которое может иметь другое подменю. Здесь, в этом dartPad я создал подменю, используя две разные идеи.

  1. Использование существующего PopupMenuButon виджета, вложенного друг в друга и использование атрибута offset для позиционирования подменю.
  2. Использование глобальной функции showMenu, которая может позиционировать меню в любом месте экрана.

Вы можете проверить две реализации, показанные ниже. Обратите внимание, что оба метода имеют свои предостережения. Как отклонение всплывающих окон и обработка выбора и отмены. Однако это только для того, чтобы показать, что это возможно во флаттере, и обработка этих случаев выходит за рамки этого ответа.

Nested PopupMenuButton

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),                
              ],
            ),
              ),)

      ),
    );
  }
}

0 голосов
/ 28 апреля 2020

В стандартной библиотеке Flutter ( material.dart ) есть абстрактный класс PopupMenuEntry, от которого наследуются все дочерние элементы PopupMenuButton. В настоящее время существует три конкретных подкласса: PopupMenuItem (обычный элемент, который вы видите постоянно), 'CheckedPopupMenuItem' (обычный элемент + флажок) и PopupMenuDivider (горизонтальная линия). Ничто не мешает нам реализовать наш собственный подкласс.

Используя первый ответ @AbhilashChandran и немного его изменив, мы можем создать следующий обобщенный c класс:

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
        },
      ),
    ];
  },
)

Результат примерно такой:

Screenshot

У этого подхода есть несколько недостатков:

  • Очевидно, что подменю отображается не в том месте, где вы хотели, чтобы оно отображалось, - может быть связано с некоторыми сложными вычислениями;
  • Даже если несколько подменю можно поместить одно в другое, я не убедитесь, что верхние будут закрыты правильно, когда нижние закрыты (или выбрано значение) - могут обрабатываться Navigator вызовами и проверками;
  • И родительское меню и подменю должны иметь значения тот же тип - может быть решен с помощью подклассов;
  • Необходимость указать onSelected метод дважды (для родительского меню и для дочернего меню) - может быть решена с помощью методов или замыканий;

Класс PopupSubMenuItem можно расширить, добавив в него что-то вроде final String Function(T) formatter; для представления ваших значений в осмысленно, но для краткости эта функциональность была опущена.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...