You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter自定义带侧边子菜单的Drawer实现需求及问题

自定义Flutter Drawer实现需求

需要实现一个具备以下特性的自定义Drawer:

  • Drawer宽度可指定
  • 点击图标时,该图标变为红色
  • 在Drawer右侧、与点击图标同一水平线的位置打开对应的子菜单

初始代码

class CustomDrawer extends StatelessWidget {
  const CustomDrawer({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius:
          BorderRadius.circular(25.0), // 设置圆角边框
      child: SizedBox(
        width: MediaQuery.of(context).size.width * .25,
        child: Drawer(
          child: ListView(
            padding: EdgeInsets.zero,
            children: <Widget>[
              SizedBox(
                height: MediaQuery.of(context).size.height * .10,
              ),
              ListTile(
                title: const Icon(CupertinoIcons.book),
                onTap: () {
                  // 在此添加操作
                },
              ),

              // 根据需要添加更多项
            ],
          ),
        ),
      ),
    );
  }
}

尝试过的方案(未完全满足需求)

class CustomDrawer extends StatelessWidget {
  const CustomDrawer({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(25.0),
      child: SizedBox(
        width: MediaQuery.of(context).size.width * 0.25,
        child: Drawer(
          child: ListView(
            padding: EdgeInsets.zero,
            children: <Widget>[
              SizedBox(
                height: MediaQuery.of(context).size.height * 0.10,
              ),
              GestureDetector(
                onTap: () {
                  // 处理书籍图标的点击事件
                  _showBookMenu(context);
                },
                child: const ListTile(
                  title: Row(
                    children: [
                      Icon(CupertinoIcons.book),
                      SizedBox(width: 8.0), // 添加间距
                    ],
                  ),
                ),
              ),
              // 根据需要添加更多项
            ],
          ),
        ),
      ),
    );
  }

  void _showBookMenu(BuildContext context) {
    final RenderBox renderBox = context.findRenderObject() as RenderBox;
    final Offset offset = renderBox.localToGlobal(Offset.zero);
    showMenu(
      context: context,
      position: RelativeRect.fromLTRB(
        offset.dx + renderBox.size.width,
        offset.dy + 100,
        offset.dx + renderBox.size.width * 2,
        offset.dy + renderBox.size.height, 
      ),
      items: <PopupMenuEntry<String>>[
        const PopupMenuItem<String>(
          value: 'item1',
          child: Text('菜单选项1'),
        ),
        const PopupMenuItem<String>(
          value: 'item2',
          child: Text('菜单选项2'),
        ),
        // 根据需要添加更多菜单项
      ],
    );
  }
}

该方案接近需求,但Drawer内包含多个带子菜单的项,需要完善实现方法。预期效果示意图如下:

_______
|       |
|       |_____________
|Book                 |___________ 子菜单
|       |  book 1     |
|Aurhor |             |
|       |  book 2     |
|Mobile |             |
|       |  book 3     | 
|       |_____________|
|       |
|       |
|       |
|       |
|       |
|       |
|       |______ Drawer
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|_______|

 _______
|       |
|       |
|Book   |             
|       |_____________  
|Aurhor               |
|       |  Auth 1     |
|Mobile |             |
|       |  Auth 2     | 
|       |_____________|
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|_______|

解决方案

将组件改为StatefulWidget来管理选中状态和子菜单定位,实现多子菜单支持:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class CustomDrawer extends StatefulWidget {
  const CustomDrawer({super.key});

  @override
  State<CustomDrawer> createState() => _CustomDrawerState();
}

class _CustomDrawerState extends State<CustomDrawer> {
  // 记录当前选中的菜单项索引,null表示未选中
  int? _selectedIndex;
  // 存储各菜单项的子菜单数据
  final Map<int, List<String>> _subMenus = {
    0: ['book 1', 'book 2', 'book 3'],
    1: ['Auth 1', 'Auth 2'],
    2: ['Mobile 1', 'Mobile 2'],
  };
  // 为每个菜单项添加全局Key,用于获取位置信息
  final Map<int, GlobalKey> _itemKeys = {
    0: GlobalKey(),
    1: GlobalKey(),
    2: GlobalKey(),
  };

  @override
  Widget build(BuildContext context) {
    final drawerWidth = MediaQuery.of(context).size.width * .25;
    return Stack(
      children: [
        // 主Drawer
        ClipRRect(
          borderRadius: BorderRadius.circular(25.0),
          child: SizedBox(
            width: drawerWidth,
            child: Drawer(
              child: ListView(
                padding: EdgeInsets.zero,
                children: <Widget>[
                  SizedBox(height: MediaQuery.of(context).size.height * .10),
                  // 书籍菜单项
                  _buildDrawerItem(index: 0, icon: CupertinoIcons.book),
                  // 作者菜单项
                  _buildDrawerItem(index: 1, icon: CupertinoIcons.person),
                  // 移动端菜单项
                  _buildDrawerItem(index: 2, icon: CupertinoIcons.phone),
                ],
              ),
            ),
          ),
        ),
        // 显示选中项的子菜单
        if (_selectedIndex != null) _buildSubMenu(drawerWidth),
      ],
    );
  }

  // 构建单个Drawer菜单项
  Widget _buildDrawerItem({required int index, required IconData icon}) {
    return ListTile(
      key: _itemKeys[index],
      title: Icon(
        icon,
        // 选中时图标变为红色
        color: _selectedIndex == index ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          // 点击同一项则关闭子菜单,否则切换选中项
          _selectedIndex = _selectedIndex == index ? null : index;
        });
      },
    );
  }

  // 构建子菜单
  Widget _buildSubMenu(double drawerWidth) {
    final selectedKey = _itemKeys[_selectedIndex!]!;
    final renderBox = selectedKey.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) return const SizedBox.shrink();

    // 获取选中项的全局位置和高度
    final offset = renderBox.localToGlobal(Offset.zero);
    final itemHeight = renderBox.size.height;

    return Positioned(
      left: drawerWidth,
      top: offset.dy,
      width: drawerWidth,
      child: Material(
        elevation: 4,
        borderRadius: const BorderRadius.only(
          topRight: Radius.circular(25),
          bottomRight: Radius.circular(25),
        ),
        child: ListView.builder(
          shrinkWrap: true,
          padding: EdgeInsets.zero,
          itemCount: _subMenus[_selectedIndex!]!.length,
          itemBuilder: (context, subIndex) {
            return ListTile(
              title: Text(_subMenus[_selectedIndex!]![subIndex]),
              onTap: () {
                // 处理子菜单点击逻辑,点击后关闭子菜单
                setState(() {
                  _selectedIndex = null;
                });
              },
            );
          },
        ),
      ),
    );
  }
}

实现说明

  1. 状态管理:通过StatefulWidget维护选中项索引,控制图标颜色和子菜单的显示/隐藏
  2. 图标变色:根据选中状态动态设置图标颜色为红色
  3. 精准定位:利用GlobalKey获取菜单项的全局位置,通过Positioned组件将子菜单固定在对应项的右侧同一水平线
  4. 多子菜单支持:通过Map存储不同菜单项的子菜单数据,统一构建子菜单列表,扩展性强

内容的提问来源于stack exchange,提问作者Hamou Ouyaba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 10:17:09