Flutter 之 AppBar 這樣的騷操作你知道嗎?

沐風同學發表於2019-03-01

好久不見了,這陣子在忙公司的專案,加班比較嚴重,這周終於抽了點時間來幫國外一家公司做一款跨平臺的 App。由於去年九月份在上海參加過 Google 舉辦的 Google develop days, 受益頗多,特別在其目前正在大力熱推的 Flutter 框架。相比於目前熱門的跨平臺框架 React Native,Flutter在 UI 繪製以及效能方便不遑多讓。因此,這款 app 打算基於 Dart 語言,並採用 Flutter 框架來完成。

花了大概幾天時間熟悉了下 Dart 語法和 Flutter 基本組成控制元件,就開始摸索著做一個練手專案。AppBar 想必大家都用過,當其處於啟動頁時是隱藏的,而處於其他頁面時左邊預設攜帶來返回按鈕。目前,我們的需求是:首頁的 AppBar 最左邊為使用者頭像,點選使用者頭像可以自動開啟左側抽屜欄。

最終效果圖如下所示:

效果圖

初學者一開始總是痛苦的,還好,解決問題的途徑“萬變不離其宗”。我們先來檢視一下 Flutter 官方文件 發現,要使用 AppBar來操作頁面,決定其左邊點選事件的屬性就是 leading

AppBar({
Key key,
Widget leading,
bool automaticallyImplyLeading: true,
Widget title,
List<Widget> actions,
Widget flexibleSpace,
PreferredSizeWidget bottom,
double elevation,
Color backgroundColor,
Brightness brightness,
IconThemeData iconTheme,
TextTheme textTheme,
bool primary: true,
bool centerTitle,
double titleSpacing: NavigationToolbar.kMiddleSpacing,
double toolbarOpacity: 1.0,
double bottomOpacity: 1.0
})
複製程式碼

可以看到,我們 AppBar 中的 leading 屬性是一個 Widget 型別,那麼它接收的引數範圍就很廣了,換句話說,我們可以直接將一個按鈕傳遞給它,並在 onPressed 方法中處理左邊側滑欄的開啟和關閉狀態。?,有思路了我們們久開始試驗一下:

 final GlobalKey<ScaffoldState> _scaffoldKey = new 													GlobalKey();
 
 @override
  Widget build(BuildContext context) {
    return new DefaultTabController(
        length: _pages.length,
        child: new Scaffold(
          key: _scaffoldKey,
          appBar: new AppBar(
            /// 第一種方式
            /// 通過可監聽點選的IconButton傳入widget,
            /// 並在onPressed中處理drawer開啟,藉助於GlobalKey
            leading: new IconButton(
                icon: new Container(
                  padding: EdgeInsets.all(3.0),
                  child: new CircleAvatar(
                      radius: 30.0,
                      backgroundImage: AssetImage("assets/images/moosphon_logo.jpeg")
                  ),
                ),
              onPressed: (){
                _scaffoldKey.currentState.openDrawer();
              },
            ),
            centerTitle: true,
            title: new TabBar(
                isScrollable: true,
                labelPadding: EdgeInsets.all(14),
                indicatorSize: TabBarIndicatorSize.label,
                indicatorColor: Colors.white,
                tabs: _titles.map((String title) =>
                new Tab(text: title)).toList()
            ),
            actions: <Widget>[
              new IconButton(
                  icon: new Icon(Icons.search),
                  tooltip: '搜尋',
                  onPressed: (){
                    NavigatorUtil.intentToPage(context, new SearchPage(), pageName: "SearchPage");
                  }
              )
            ],
          ),
          body: new TabBindingView(),
          drawer: new Drawer(
            child: new HomeLeftDrawerPage(),
          ),
        )
    );
  }

複製程式碼

為了不影響篇幅,這裡我只貼了關鍵程式碼,至於 Drawer 用法相信大家沒什麼問題,這裡我們給 AppBar 的leading 引數傳了一個 IconButton 控制元件,裡面我還實現了圓形頭像。這樣一來,我們的使用者頭像就可以被點選了響應了,接下來我們需要處理頭像點選事件與 Drawer 的聯動性了。這裡我們通過設定 GlobalKey 來讓 Scaffold 容器獲取到其內部的 Drawer 元件,進而控制它的開閉,這樣,我們就已經可以通過點選自定義的使用者頭像來開啟左邊側滑欄啦?。

細心的小夥伴一定會發現,上面的程式碼中我註釋這只是第一種實現方式,難道還有第二種實現方式?哈哈,沒錯,的確還有第二種思路。其實,通過檢視 AppBar 的原始碼,我們可以發現:AppBar內部已經自動實現了 AppBar 的 leadingDrawer 抽屜欄的關聯:

Widget leading = widget.leading;
    if (leading == null && widget.automaticallyImplyLeading) {
      if (hasDrawer) {
        leading = IconButton(
          icon: const Icon(Icons.menu),
          onPressed: _handleDrawerButton,
          tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
        );
      } else {
        if (canPop)
          leading = useCloseButton ? const CloseButton() : const BackButton();
      }
    }
    if (leading != null) {
      leading = ConstrainedBox(
        constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
        child: leading,
      );
    }
複製程式碼

由於程式碼較多,這裡我只放上了 build 方法中關於 leading 部分的關鍵程式碼,通過分析我們可以發現:

  • leading 如果 為空並且 automaticallyImplyLeading 屬性為true,那麼就會自動推斷出 leadingWidget 的型別和用途,另外如果當前 Scaffold 中存在 Drawer ,則會自動建立一個 IconButton 作為leading 使用,同時它的點選事件中處理了與 Drawer 抽屜欄的關聯事件,無需開發者再處理。也就是說,在這種情況下,如果我們自定義了 leading (例如當前我們給它傳的是使用者的頭像 Widget ),那麼 leading 就無法自動關聯 Drawer ,也就是說關聯 Drawer 的這部分程式碼需要開發者自行實現。
  • 如果 leading 不為空呢?並且 automaticallyImplyLeading 開關關閉,那麼 leading 的空間就會被 title 給佔據。

說了這麼多,最終的結論是:如果我們想要自定義 leading ,那麼目前官方原始碼中 AppBar 中 leading 自動關聯 Drawer 的處理我們沒辦法使用。如果我們非要用呢?那就只能修改原始碼啦:

/// 注意,前方高能來襲,以下為修改的部分code

    Widget leading = widget.leading;
    if (/*leading == null && */widget.automaticallyImplyLeading) {
      if (hasDrawer) {
        leading = IconButton(
          icon: /*const Icon(Icons.menu)*/ leading ?? Icon(Icons.home), // 如果leading指定了widget那麼
          onPressed: _handleDrawerButton,
          tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
        );
      } else {
        if (canPop)
          leading = useCloseButton ? const CloseButton() : const BackButton();
      }
    }
    if (leading != null) {
      leading = ConstrainedBox(
        constraints: const BoxConstraints.tightFor(width: _kLeadingWidth),
        child: leading,
      );
    }
複製程式碼

鐺鐺鐺鐺~這就改好啦,改動的地方是不是很少?哈哈?,其實只要把 leading 為空的限制條件去掉,並且可以傳入我們自己定義的 Widget 就好啦!

這樣一來,我們就不用手動去處理 leadingDrawer 的關聯事件,只需要交給系統幫我們完成就可以啦:

@override
  Widget build(BuildContext context) {
    return new DefaultTabController(
        length: _pages.length,
        child: new Scaffold(
          key: _scaffoldKey,
          appBar: new DrawerAutoBindingAppBar(
            /// 第二種方式
            /// 通過修改[AppBar]原始碼來將普通widget作為icon傳給IconButton
            /// 原始碼中已處理onPressed關聯drawer事件,無需額外處理,詳情見[DrawerAutoBindingAppBar]
            leading: new Container(
                padding: EdgeInsets.all(3.0),
                child: new CircleAvatar(
                    radius: 30.0,
                    backgroundImage: AssetImage("assets/images/moosphon_logo.jpeg")
                )
            ),
            centerTitle: true,
            title: new TabBar(
                isScrollable: true,
                labelPadding: EdgeInsets.all(14),
                indicatorSize: TabBarIndicatorSize.label,
                indicatorColor: Colors.white,
                tabs: _titles.map((String title) =>
                new Tab(text: title)).toList()
            ),
            actions: <Widget>[
              new IconButton(
                  icon: new Icon(Icons.search),
                  tooltip: '搜尋',
                  onPressed: (){
                    NavigatorUtil.intentToPage(context, new SearchPage(), pageName: "SearchPage");
                  }
              )
            ],
          ),
          body: new TabBindingView(),
          drawer: new Drawer(
            child: new HomeLeftDrawerPage(),
          ),
        )
    );
  }

複製程式碼

程式碼就不多做解釋了,效果是和之前一樣的,好啦,這樣我們就更加了解 AppBar 的用法以及 leading 的背後“小彩蛋”啦?。

感謝大家的閱讀,以後我將給大家帶來更多好玩實用的應用型開發技巧,如果大家有任何建議,歡迎留言?。

QQ群: 601924443 一起玩技術~

相關文章