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; }); }, ); }, ), ), ); } }
实现说明
- 状态管理:通过
StatefulWidget维护选中项索引,控制图标颜色和子菜单的显示/隐藏 - 图标变色:根据选中状态动态设置图标颜色为红色
- 精准定位:利用
GlobalKey获取菜单项的全局位置,通过Positioned组件将子菜单固定在对应项的右侧同一水平线 - 多子菜单支持:通过Map存储不同菜单项的子菜单数据,统一构建子菜单列表,扩展性强
内容的提问来源于stack exchange,提问作者Hamou Ouyaba
相关产品推荐
相关产品推荐

