Flutter之drawer詳細分析(你要的操做都有)

1. 簡介

這篇文章主要講解有關drawer的一切。android

另:接Flutter相關項目,須要的私信或經過QQ:708959817,聯繫我markdown

2. 初探

咱們先來看看簡單的drawer在Flutter的應用app

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appbar,
      drawer: _drawer,
    );
  }

  get _appbar=>AppBar(
    title: Text('Drawer Test'),
  );


  get _drawer =>Drawer(
    child: Text('This is Drawer'),
  );
}


複製代碼

而後運行一下項目: 以下圖所示 less

image.png
image.png

能夠看到,根據咱們對drawer的認識,並非想要的結果,因此這個drawer並不完整,而後咱們繼續添加代碼,修改draweride

///...

  get _drawer => Drawer(
    ///edit start
        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('設置'),
            )
          ],
        ),
    ///edit end
      );
複製代碼

我這裏添加了 ListView => 裝載抽屜的部件 DrawerHeader =>抽屜的頭部 SizeBox => 用於限制CircleAvatar的大小 CircleAvatar => 頭像部件 ListTile => 一個名爲"設置"的點擊項 而後咱們熱部署一下 佈局

image.png
Oh,emmm....仍是很醜的一個 drawer嘢!上面那坨灰色的東西是怎麼肥事!不急不急,咱們慢慢來分析

3 . 解決Drawer灰色頭部

由於加了一個DrawerHeader,因此,咱們須要看看DrawerHeader裏面是什麼緣由致使添加灰色的地方 DrawerHeader源碼: 動畫

image.png

能夠看到: Container=>限制高度(默認高度+狀態欄高度) BoxDecoration=> 底部添加毫無用處的分割線 AnimatedContainer =>動畫版的Container添加默認內邊距+頂部狀態欄高度的內邊距 嗯,感受沒錯啊,這是怎麼肥事,MediaQuery.of(context).padding.top是獲取狀態欄的高度,而後自身高度加上狀態欄的高度,應該是顯示藍色纔對,那會不會跟ListView有關係呢? 咱們將DrawerHeader去掉看看ui

get _drawer => Drawer(
        child: ListView(
          children: <Widget>[
            ///edit start
// DrawerHeader(
// decoration: BoxDecoration(
// color: Colors.lightBlueAccent,
// ),
// child: Center(
// child: SizedBox(
// width: 60.0,
// height: 60.0,
// child: CircleAvatar(
// child: Text('R'),
// ),
// ),
// ),
// ),
            ///edit end
            ListTile(
              leading: Icon(Icons.settings),
              title: Text('設置'),
            )
          ],
        ),
      );
複製代碼

image.png
確實,跟 ListView有關,這是什麼緣由致使 ListView加上一個 statusBarHeight大小的內邊距呢?咱們能夠繼續找 ListView的源碼
image.png
能夠直接點擊 ListView的構造方法,跳轉到455行可看到 1.當 ListView的屬性 padding爲空時,獲取 MediaQueryData的信息

2.由於ListView的滾動方向默認爲垂直,會使用mediaQueryVerticalPaddingthis

3.sliver添加一層MediaQuery,這個代表sliver的子部件會使用該MediaQuery的值,根據判斷,子部件會使用mediaQueryHorizontalPadding,而上面的兩個複製:spa

mediaQueryHorizontalPadding =>將原有的MediaQuery的padding複製爲topbottom都爲0,該值會被子部件使用,因此能夠知道,DrawerHeader使用了該值,致使statusBarHeader爲0 mediaQueryVerticalPadding =>將原有的MediaQuery的padding複製爲leftright都爲0

因此,咱們只要不讓ListViewpadding屬性爲空就能夠了,這裏我傳入一個zero給ListView,而後把DrawerHeader的註釋去掉,熱部署一下

get _drawer => Drawer(
        child: ListView(
            ///edit start
          padding: EdgeInsets.zero,
            ///edit end
          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('設置'),
            )
          ],
        ),
      );
複製代碼

image.png
ok,咱們成功解決了Drawer灰色頭部

4. 定製Drawer的滑出大小

咱們來看看drawer的源碼,其實看源碼並非一件痛苦的事,咱們通常直接跳到build方法就好

image.png

能夠看到Drawer這個部件就是咱們日常的一些部件組合而成 Semantics=> 語義,用於給無障礙的 ConstrainedBox => 限制Drawer的寬度的,以致於Drawer不會鋪滿你的屏幕 Material => 添加陰影的 咦!聽我這樣解(Hu)釋(Che),是否是對Drawer這個部件清晰了很多呀! 因此,其實Drawer就是一個普通的StatelessWidget,咱們徹底能夠定(Fu)制(Zhi)咱們的Drawer,好比定製Drawer的滑出大小

class SmartDrawer extends StatelessWidget {
  final double elevation;
  final Widget child;
  final String semanticLabel;
///new start
  final double widthPercent;
///new end
  const SmartDrawer({
    Key key,
    this.elevation = 16.0,
    this.child,
    this.semanticLabel,
///new start
    this.widthPercent = 0.7,
///new end
  }) : 
///new start
   assert(widthPercent!=null&&widthPercent<1.0&&widthPercent>0.0)
///new end
   ,super(key: key);
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    String label = semanticLabel;
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        label = semanticLabel;
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        label = semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel;
    }
///new start
    final double _width=MediaQuery.of(context).size.width*widthPercent;
///new end
    return Semantics(
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      label: label,
      child: ConstrainedBox(
///edit start
        constraints: BoxConstraints.expand(width: _width),
///edit end
        child: Material(
          elevation: elevation,
          child: child,
        ),
      ),
    );
  }
}
複製代碼

我這裏將原來的Drawer代碼基礎上修改_kWidth的值,把它暴露給用戶本身去定製,讓他能傳入一個double類型的寬度百分比,彈出根據屏幕的百分之幾的Drawer,該值只容許傳入大於0小於1的值,默認爲0.7 下面咱們將上面的Drawer改成咱們的SmartDrawer

///edit
get _drawer => SmartDrawer(
        widthPercent: 0.4,
///edit
        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('設置'),
            )
          ],
        ),
      );
複製代碼

image.png
能夠看到,咱們成功的修改了 Drawer彈出的大小

5.監聽Drawer的彈出和關閉

監聽Drawer這裏官方給咱們埋了一個坑 監聽咱們以Tab爲例,Flutter會給我咱們一個XXXController部件,而Drawer會不會也會有個DrawerController呢?

image.png
能夠看到,Flutter是有一個 DrawerController的,而後咱們就將 DrawerController添加到咱們的 _drawer中去

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appbar,
///edit start
      drawer: DrawerController(
        child: _drawer,
        alignment: DrawerAlignment.start,
        drawerCallback: (isOpen) {
          print('打開狀態:$isOpen');
        },
      ),
    );
///edit end
  }
複製代碼

咱們來運行一下吧

image.png
當我點擊 AppBar中左邊的按鈕是發現,彈出了一個蒙版, Drawer並無彈出來,這是怎麼回事?別急,咱們開啓一下佈局邊界
image.png
點擊Toggle Debug Paint按鈕
image.png
會發現,你的佈局左邊有一條矩形,這個是什麼,咱們在左邊矩形區域拖動一下看看
image.png
誒!咱們的 Drawer出現了,這是什麼回事?爲何要拖動兩遍纔出現,神奇了? 別急,這一切均可以分析 咱們先來看看 Scaffold是怎麼定義 DrawerScaffold源碼
image.png

該代碼比較簡單: 1.先判斷drawer是否爲空,若不爲空添加drawer

  1. _addIfNonNull該方法從命名能夠看出若不爲空添加到children裏面

  2. 這裏被添加了一個DrawerController,可知道Flutter寫死了一個DrawerController(這個真的很鬱悶,還不把callback放出來給用戶) 由此能夠點擊_drawerOpendCallback看看作了什麼操做 _drawerOpendCallback部分代碼:

    image.png
    這裏將值給了_drawerOpened,用於
    image.png
    給endDrawer打開作判斷,emmm....這個不合理吧!

到這裏,咱們能夠總結:Scaffold爲咱們添加了一個DrawerController後,咱們又添加了一個DrawerController致使須要滑動兩次才能顯示咱們的Drawer,因此,咱們能夠猜想DrawerController就是控制彈出跟關閉的一個部件

那麼,到這裏,咱們基本上想要監聽drawer的彈出跟關閉就是死路一條了。 要怎樣監聽呢?咱們可不能夠經過咱們定製的SmartDrawer去監聽呢? 這裏先作一個埋點,先來看一段代碼

///edit start
class SmartDrawer extends StatefulWidget {
///edit end
  final double elevation;
  final Widget child;
  final String semanticLabel;
  final double widthPercent;

  const SmartDrawer({
    Key key,
    this.elevation = 16.0,
    this.child,
    this.semanticLabel,
    this.widthPercent,
  })  : assert(widthPercent < 1.0 && widthPercent > 0.0),
        super(key: key);

///edit start
  @override
  _SmartDrawerState createState() => _SmartDrawerState();
///edit end
}

class _SmartDrawerState extends State<SmartDrawer> {

///add start
  @override
  void initState() {
    print('initState');
    super.initState();
  }
  @override
  void dispose() {
    print('dispose');
    super.dispose();
  }
///add end

///edit xxx 2 width.xxx start
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    String label = widget.semanticLabel;
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        label = widget.semanticLabel;
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        label = widget.semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel;
    }
    final double _width = MediaQuery.of(context).size.width * widget.widthPercent;
    return Semantics(
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      label: label,
      child: ConstrainedBox(
        constraints: BoxConstraints.expand(width: _width),
        child: Material(
          elevation: widget.elevation,
          child: widget.child,
        ),
      ),
    );
  }
}
///edit xxx 2 width.xxx end
複製代碼

先把SmartDrawer的父類由StatelessWidget改成StatefulWidget,而後添加部件的兩個生命週期(建立和銷燬) 而後繼續熱部署進行使用,正常的打開和關閉Drawer

image.png
誒,能夠看到,每次的打開會觸發 initState,每次的關閉會觸發 dispose,這個不就是咱們一直想要的 Drawer打開和關閉嗎? 因而能夠改爲這樣:

class SmartDrawer extends StatefulWidget {
  final double elevation;
  final Widget child;
  final String semanticLabel;
  final double widthPercent;
///add start
  final DrawerCallback callback;
///add end
  const SmartDrawer({
    Key key,
    this.elevation = 16.0,
    this.child,
    this.semanticLabel,
    this.widthPercent,
///add start
    this.callback,
///add end
  })  : assert(widthPercent < 1.0 && widthPercent > 0.0),
        super(key: key);
  @override
  _SmartDrawerState createState() => _SmartDrawerState();
}

class _SmartDrawerState extends State<SmartDrawer> {

  @override
  void initState() {
///add start
    if(widget.callback!=null){
      widget.callback(true);
    }
///add end
    super.initState();
  }
  @override
  void dispose() {
///add start
    if(widget.callback!=null){
      widget.callback(false);
    }
///add end
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    String label = widget.semanticLabel;
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
        label = widget.semanticLabel;
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        label = widget.semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel;
    }
    final double _width = MediaQuery.of(context).size.width * widget.widthPercent;
    return Semantics(
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      label: label,
      child: ConstrainedBox(
        constraints: BoxConstraints.expand(width: _width),
        child: Material(
          elevation: widget.elevation,
          child: widget.child,
        ),
      ),
    );
  }
}
複製代碼

如今就能夠監聽到drawer的打開了,完美!

6.定製彈出Drawer的按鈕

到目前爲止,咱們使用的drawer打開按鈕都是Scaffold默認給咱們添加的,咱們能夠經過Scaffold源碼看到 Scaffold源碼:

image.png
能夠看到,獲取 leading參數的內容,而後判斷是否爲空和是否自動添加 leading,若爲空,若是存在 DrawerScaffold會默認給咱們添加一個 IconIcons.menuIconButton,若是不存在,會判斷是否能返回,若是能返回,就添加返回按鈕。 咱們這裏只須要知道,Scaffold爲咱們默認添加一個IconButton 如今,咱們來看一下默認添加的 IconButton的點擊事件 onPressed作了什麼
image.png
調用 Scaffold.of(context).openDrawer()打開drawer,因此,咱們定製彈出 Drawer按鈕能夠以下這樣寫:

//.....
//new start
  void _handlerDrawerButton() {
    Scaffold.of(context).openDrawer();
  }
//new end

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appbar,
      drawer: _drawer,
    );
  }

  get _appbar=>AppBar(
//edit start
    leading: IconButton(icon: Icon(Icons.storage), onPressed: _handlerDrawerButton),
//edit end
    title: Text('Drawer Test'),
  );

//...
複製代碼

而後就能夠經過該按鈕進行點擊了,有人可能問,能不能換成其餘的按鈕形式,答案是能夠的,只要點擊事件裏面調用的是_handlerDrawerButton()方法

7.禁止手勢側滑出Drawer

有同窗問我如何禁止手勢側滑出Drawer,咱們只須要修改一個屬性便可

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appbar,
      drawer: _drawer,
//new start
      drawerEdgeDragWidth: 0.0,
//new end
    );
  }
複製代碼

目前遇到上面的定製問題,本篇文章會繼續更新,請持續關注! 若是這篇文章對你有所幫助,但願能討個贊,謝謝!

相關文章
相關標籤/搜索