好的!以下是一篇基於你提供的簡書文章(原文鏈接)整理而成的技術博客。我保留了原文的核心內容、問題分析思路和所有關鍵代碼,並進行了結構優化、語言潤色與邏輯梳理,使其更適合作為一篇清晰易讀的 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 源碼可知:當 paddingnull 時,會自動應用 MediaQuery.of(context).padding

解決方案

顯式將 ListViewpadding 設為 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,通過 initStatedispose 模擬開關事件:

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 的六大核心需求:

功能

解決方案

去除灰色頭部

ListView(padding: EdgeInsets.zero)

自定義寬度

封裝 SmartDrawer,使用 ConstrainedBox

監聽開關狀態

利用 StatefulWidgetinitState / dispose

自定義打開按鈕

Scaffold.of(context).openDrawer()

禁用側滑

drawerEdgeDragWidth: 0.0

結構清晰

使用 ListView + DrawerHeader + ListTile

通過這些技巧,你可以輕鬆打造符合產品需求的個性化側邊欄。

📌 提示:雖然 Drawer 使用簡單,但過度定製可能影響用户體驗。建議在 Material Design 規範基礎上進行適度優化。