Flutter实现左侧菜单栏

1,379 阅读2分钟

支持展开和收缩,效果:

menu.gif

SiderDemo2是示例,需要放到MaterialApp中

import 'dart:math';

import 'package:flutter/material.dart';

typedef MenuItemKeyType = String;

class SiderDemo2 extends StatelessWidget {
  SiderDemo2({super.key});
  final PageController _pageController = PageController(initialPage: 0);
  final Random r = Random(3);

  void onSelect(MenuItemKeyType key) {
    int page = r.nextInt(3);
    print(page);
    _pageController.jumpToPage(page);
  }

  final List<MenuProp> items = [
    MenuProp(
        label: "菜单1",
        key: "菜单1",
        children: [
          MenuProp(label: "子菜单1", key: "子菜单1", icon: const Icon(Icons.send)),
          MenuProp(
              label: "子菜单2", key: "子菜单2", icon: const Icon(Icons.access_alarm)),
          MenuProp(label: "子菜单3", key: "子菜单3", icon: const Icon(Icons.egg)),
        ],
        icon: const Icon(Icons.star)),
    MenuProp(label: "菜单2", key: "菜单2", icon: const Icon(Icons.desk)),
    MenuProp(label: "菜单3", key: "菜单3", icon: const Icon(Icons.pan_tool)),
    MenuProp(label: "菜单4", key: "菜单4", icon: const Icon(Icons.back_hand)),
    MenuProp(label: "菜单5", key: "菜单5", icon: const Icon(Icons.cabin)),
    MenuProp(label: "菜单6", key: "菜单6", icon: const Icon(Icons.dangerous)),
    MenuProp(label: "菜单7", key: "菜单7", icon: const Icon(Icons.earbuds)),
    MenuProp(label: "菜单8", key: "菜单8", icon: const Icon(Icons.face)),
    MenuProp(
        label: "菜单9",
        key: "菜单9",
        children: [
          MenuProp(label: "子菜单1", key: "子菜单1", icon: const Icon(Icons.gamepad)),
          MenuProp(label: "子菜单2", key: "子菜单2", icon: const Icon(Icons.hail)),
          MenuProp(
              label: "子菜单3", key: "子菜单3", icon: const Icon(Icons.ice_skating)),
          MenuProp(label: "子菜单4", key: "子菜单4"),
          MenuProp(label: "子菜单5", key: "子菜单5"),
          MenuProp(label: "子菜单6", key: "子菜单6"),
        ],
        icon: const Icon(Icons.join_right)),
  ];

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Menu(
          items: items,
          collapsed: true,
          onSelect: onSelect,
        ),
        Expanded(
          child: PageView(controller: _pageController, children: const [
            Text("Page A"),
            Text("Page B"),
            Text("Page C"),
          ]),
        ),
      ],
    );
  }
}

class Menu extends StatefulWidget {
  final List<MenuProp> items;
  final bool? collapsed;
  final ValueChanged<MenuItemKeyType>? onSelect;

  const Menu({super.key, this.onSelect, this.collapsed, required this.items});
  @override
  State<StatefulWidget> createState() {
    return _MenuState();
  }
}

class _MenuState extends State<Menu> {
  bool _opened = true;
  MenuItemKeyType? selectKey;

  void toggleOpen() {
    setState(() {
      _opened = !_opened;
    });
  }

  void onSelect(MenuItemKeyType selectKey) {
    setState(() {
      this.selectKey = selectKey;
    });
    if (widget.onSelect != null) {
      widget.onSelect!(selectKey);
    }
  }

  @override
  Widget build(BuildContext context) {
    return MenuWidget(
      menuModel: MenuModel(
          onSelect: onSelect,
          collapsed: widget.collapsed,
          items: widget.items,
          opened: _opened,
          toggleOpen: toggleOpen,
          selectKey: selectKey),
      child: const _MenuLayout(),
    );
  }
}

class _MenuLayout extends StatelessWidget {
  const _MenuLayout();

  Widget _buildMenuItem(MenuProp menuProp, BuildContext context) {
    bool opened = MenuWidget.of(context).menuModel.opened;
    if (menuProp.children != null && menuProp.children!.isNotEmpty && !opened) {
      return _PopupSubMenu(key: Key(menuProp.key), menuProp: menuProp);
    } else if (menuProp.children != null &&
        menuProp.children!.isNotEmpty &&
        opened) {
      return _SubMenu(key: Key(menuProp.key), menuProp: menuProp);
    }
    return _MenuItem(key: Key(menuProp.key), menuProp: menuProp);
  }

  @override
  Widget build(BuildContext context) {
    Widget menusChildren;
    bool? collapsed = MenuWidget.of(context).menuModel.collapsed;
    bool opened = MenuWidget.of(context).menuModel.opened;
    VoidCallback toggleOpen = MenuWidget.of(context).menuModel.toggleOpen;
    List<MenuProp> items = MenuWidget.of(context).menuModel.items;

    if (collapsed != null && collapsed) {
      menusChildren = Column(
        children: [
          IconButton(
            onPressed: toggleOpen,
            icon: opened ? const Icon(Icons.menu_open) : const Icon(Icons.menu),
          ),
          const Divider(height: 2),
          Expanded(
            child: ListView(
              children:
                  items.map((item) => _buildMenuItem(item, context)).toList(),
            ),
          )
        ],
      );
    } else {
      menusChildren = ListView(
        children: items.map((item) => _buildMenuItem(item, context)).toList(),
      );
    }
    return Drawer(
      width: opened ? 300 : 100,
      child: menusChildren,
    );
  }
}

class _SubMenu extends StatelessWidget {
  const _SubMenu({super.key, required this.menuProp});
  final MenuProp menuProp;

  @override
  Widget build(BuildContext context) {
    List<_MenuItem> children =
        menuProp.children!.map((it) => _MenuItem(menuProp: it)).toList();
    Color primaryColor = Theme.of(context).primaryColor;

    return ExpansionTile(
      title: Text(menuProp.label),
      leading: menuProp.icon,
      iconColor: primaryColor,
      textColor: primaryColor,
      tilePadding: const EdgeInsets.only(left: 0),
      childrenPadding: const EdgeInsets.only(left: 40),
      children: children,
    );
  }
}

class _MenuItem extends StatelessWidget {
  const _MenuItem({super.key, required this.menuProp});
  final MenuProp menuProp;

  @override
  Widget build(BuildContext context) {
    MenuItemKeyType? selectKey = MenuWidget.of(context).menuModel.selectKey;
    ValueChanged<MenuItemKeyType>? onSelect =
        MenuWidget.of(context).menuModel.onSelect;
    bool opened = MenuWidget.of(context).menuModel.opened;
    Color primaryColor = Theme.of(context).primaryColor;
    // Theme.of(context).accentColor
    return ListTile(
      title: opened ? Text(menuProp.label) : menuProp.icon,
      leading: opened ? menuProp.icon : null,
      textColor:
          selectKey != null && selectKey == menuProp.key ? primaryColor : null,
      iconColor:
          selectKey != null && selectKey == menuProp.key ? primaryColor : null,
      contentPadding: const EdgeInsets.only(left: 0),
      onTap: () => {if (onSelect != null) onSelect(menuProp.key)},
    );
  }
}

class _PopupSubMenu extends StatelessWidget {
  const _PopupSubMenu({super.key, required this.menuProp});
  final MenuProp menuProp;

  @override
  Widget build(BuildContext context) {
    ValueChanged<MenuItemKeyType>? onSelect =
        MenuWidget.of(context).menuModel.onSelect;
    MenuItemKeyType? selectKey = MenuWidget.of(context).menuModel.selectKey;
    Color primaryColor = Theme.of(context).primaryColor;
    return PopupMenuButton(
      icon: menuProp.icon,
      tooltip: "展开",
      offset: const Offset(100, 0),
      onSelected: (key) => {if (onSelect != null) onSelect(key)},
      itemBuilder: (BuildContext context) => menuProp.children!
          .map((item) => PopupMenuItem(
                value: item.key,
                child: Text(item.label,
                    style: selectKey == item.key
                        ? TextStyle(color: primaryColor)
                        : null),
              ))
          .toList(),
    );
  }
}

class MenuProp {
  //显示完整标题
  String label;
  MenuItemKeyType key;
  Icon? icon;
  bool? disabled = false;
  //收缩时的短标题
  String? title;
  List<MenuProp>? children;

  MenuProp(
      {required this.label,
      required this.key,
      this.icon,
      this.disabled,
      this.title,
      this.children});
}

class MenuModel {
  final MenuItemKeyType? selectKey;
  final ValueChanged<MenuItemKeyType>? onSelect;
  final bool? collapsed;
  bool opened;
  final List<MenuProp> items;

  VoidCallback toggleOpen;

  MenuModel({
    this.selectKey,
    this.onSelect,
    this.collapsed,
    required this.opened,
    required this.items,
    required this.toggleOpen,
  });
}

class MenuWidget extends InheritedWidget {
  const MenuWidget({
    super.key,
    required this.menuModel,
    required super.child,
  });

  final MenuModel menuModel;

  static MenuWidget? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MenuWidget>();
  }

  static MenuWidget of(BuildContext context) {
    final MenuWidget? result = maybeOf(context);
    assert(result != null, 'No MenuWidget found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(MenuWidget oldWidget) {
    bool shouldNotify = menuModel.opened != oldWidget.menuModel.opened ||
        menuModel.selectKey != oldWidget.menuModel.selectKey ||
        menuModel.collapsed != oldWidget.menuModel.collapsed;
    return shouldNotify;
  }
}