好的!以下是一篇基於你提供的簡書文章(原文鏈接)整理而成的技術博客。我保留了原文的核心內容、問題分析思路和所有關鍵代碼,並進行了結構優化、語言潤色與邏輯梳理,使其更適合作為一篇清晰易讀的 Flutter 技術分享。
Flutter Drawer 詳解:從基礎使用到高級定製(附完整代碼)
作者:Qwen
參考來源:簡書《Flutter之drawer詳細分析(你要的操作都有)》
在 Flutter 中,Drawer 是實現側邊欄導航的經典組件。然而,許多開發者在初次使用時會遇到“灰色頭部”、“無法監聽開關狀態”、“不能自定義寬度”等問題。本文將帶你一步步深入 Drawer 的使用細節,解決常見坑點,並實現高度定製化功能。
一、基礎用法與問題初現
最簡單的 Drawer 寫法如下:
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar,
drawer: _drawer,
);
}
AppBar get _appBar => AppBar(title: Text('Drawer Test'));
Drawer get _drawer => Drawer(
child: Text('This is Drawer'),
);
}
但這樣顯示的效果非常簡陋,且沒有頭部區域。於是我們嘗試添加 DrawerHeader 和菜單項:
Drawer get _drawer => Drawer(
child: ListView(
children: <Widget>[
DrawerHeader(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Center(
child: SizedBox(
width: 60.0,
height: 60.0,
child: CircleAvatar(child: Text('R')),
),
),
),
ListTile(
leading: Icon(Icons.settings),
title: Text('設置'),
)
],
),
);
問題來了:頂部出現一塊灰色區域!這並非我們設置的藍色 DrawerHeader,而是系統自動添加的 padding。
二、解決灰色頭部問題
原因分析
ListView 默認會從 MediaQuery 獲取系統安全區域(如狀態欄高度),並自動添加內邊距(padding)。而 DrawerHeader 內部也處理了狀態欄,導致兩者疊加,出現灰色空白。
查看 ListView 源碼可知:當 padding 為 null 時,會自動應用 MediaQuery.of(context).padding。
解決方案
顯式將 ListView 的 padding 設為 EdgeInsets.zero:
Drawer get _drawer => Drawer(
child: ListView(
padding: EdgeInsets.zero, // 👈 關鍵修復
children: <Widget>[
DrawerHeader(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Center(
child: SizedBox(
width: 60.0,
height: 60.0,
child: CircleAvatar(child: Text('R')),
),
),
),
ListTile(
leading: Icon(Icons.settings),
title: Text('設置'),
)
],
),
);
✅ 灰色頭部消失,DrawerHeader 正常顯示藍色背景。
三、自定義 Drawer 寬度
默認 Drawer 寬度固定為屏幕寬度的 80% 左右。若想自由控制彈出寬度(例如只佔 40%),需自定義 Drawer 組件。
實現 SmartDrawer
繼承 StatelessWidget,重寫構建邏輯,暴露 widthPercent 參數:
class SmartDrawer extends StatelessWidget {
final double elevation;
final Widget child;
final String semanticLabel;
final double widthPercent; // 自定義寬度百分比
const SmartDrawer({
Key? key,
this.elevation = 16.0,
required this.child,
this.semanticLabel,
this.widthPercent = 0.7,
}) : assert(widthPercent > 0.0 && widthPercent < 1.0),
super(key: key);
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
String label = semanticLabel;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
label ??= MaterialLocalizations.of(context)?.drawerLabel;
}
final double _width = MediaQuery.of(context).size.width * widthPercent;
return Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: label,
child: ConstrainedBox(
constraints: BoxConstraints.expand(width: _width), // 👈 控制寬度
child: Material(
elevation: elevation,
child: child,
),
),
);
}
}
使用方式
將 Scaffold 中的 drawer 替換為 SmartDrawer:
SmartDrawer get _drawer => SmartDrawer(
widthPercent: 0.4, // 彈出寬度為屏幕 40%
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Center(
child: CircleAvatar(child: Text('R')),
),
),
ListTile(leading: Icon(Icons.settings), title: Text('設置')),
],
),
);
✅ 成功實現自定義寬度的抽屜!
四、監聽 Drawer 的打開與關閉
官方 Drawer 不提供回調,且 Scaffold 內部已封裝了 DrawerController,直接套用會導致需要滑動兩次才能打開。
巧妙方案:利用 Widget 生命週期
將 SmartDrawer 改為 StatefulWidget,通過 initState 和 dispose 模擬開關事件:
typedef DrawerCallback = void Function(bool isOpen);
class SmartDrawer extends StatefulWidget {
final double elevation;
final Widget child;
final String? semanticLabel;
final double widthPercent;
final DrawerCallback? callback; // 👈 新增回調
const SmartDrawer({
Key? key,
this.elevation = 16.0,
required this.child,
this.semanticLabel,
this.widthPercent = 0.7,
this.callback,
}) : assert(widthPercent > 0.0 && widthPercent < 1.0),
super(key: key);
@override
_SmartDrawerState createState() => _SmartDrawerState();
}
class _SmartDrawerState extends State<SmartDrawer> {
@override
void initState() {
super.initState();
widget.callback?.call(true); // 打開時觸發
}
@override
void dispose() {
widget.callback?.call(false); // 關閉時觸發
super.dispose();
}
@override
Widget build(BuildContext context) {
// ... 構建邏輯同上(略)
}
}
使用回調
SmartDrawer get _drawer => SmartDrawer(
widthPercent: 0.4,
callback: (isOpen) {
print('Drawer opened: $isOpen');
},
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.lightBlueAccent),
child: Center(child: CircleAvatar(child: Text('R'))),
),
ListTile(leading: Icon(Icons.settings), title: Text('設置')),
],
),
);
💡 注意:此方法依賴於 Drawer 被創建/銷燬的時機,在大多數場景下有效。若需更精確控制,可結合
Navigator監聽或自定義DrawerController。
五、自定義打開按鈕
默認 AppBar 左側會自動顯示 ☰ 圖標。若想替換為其他圖標或自定義按鈕:
AppBar get _appBar => AppBar(
leading: IconButton(
icon: Icon(Icons.storage), // 自定義圖標
onPressed: _openDrawer,
),
title: Text('Drawer Test'),
);
void _openDrawer() {
Scaffold.of(context).openDrawer(); // 手動觸發打開
}
✅ 任何按鈕只要調用 Scaffold.of(context).openDrawer() 即可打開抽屜。
六、禁用手勢側滑打開
若希望用户只能通過按鈕打開抽屜,禁止從屏幕邊緣滑動:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar,
drawer: _drawer,
drawerEdgeDragWidth: 0.0, // 👈 禁用側滑
);
}
總結
本文覆蓋了 Flutter Drawer 的六大核心需求:
|
功能
|
解決方案
|
|
去除灰色頭部
|
|
|
自定義寬度
|
封裝 |
|
監聽開關狀態
|
利用 |
|
自定義打開按鈕
|
|
|
禁用側滑
|
|
|
結構清晰
|
使用 |
通過這些技巧,你可以輕鬆打造符合產品需求的個性化側邊欄。
📌 提示:雖然
Drawer使用簡單,但過度定製可能影響用户體驗。建議在 Material Design 規範基礎上進行適度優化。