Flutter實(shí)戰(zhàn) Scaffold、TabBar、底部導(dǎo)航

2021-03-08 10:41 更新

Material 組件庫(kù)提供了豐富多樣的組件,本節(jié)介紹一些常用的組件,其余的讀者可以自行查看文檔或 Flutter Gallery 中 Material 組件部分的示例。

Flutter Gallery 是 Flutter 官方提供的 Flutter Demo,源碼位于 flutter 源碼中的 examples 目錄下,筆者強(qiáng)烈建議用戶將 Flutter Gallery 示例跑起來(lái),它是一個(gè)很全面的 Flutter 示例應(yīng)用,是非常好的參考 Demo,也是筆者學(xué)習(xí) Flutter 的第一手資料。

#5.6.1 Scaffold

一個(gè)完整的路由頁(yè)可能會(huì)包含導(dǎo)航欄、抽屜菜單(Drawer)以及底部Tab導(dǎo)航菜單等。如果每個(gè)路由頁(yè)面都需要開發(fā)者自己手動(dòng)去實(shí)現(xiàn)這些,這會(huì)是一件非常麻煩且無(wú)聊的事。幸運(yùn)的是,F(xiàn)lutter Material組件庫(kù)提供了一些現(xiàn)成的組件來(lái)減少我們的開發(fā)任務(wù)。Scaffold是一個(gè)路由頁(yè)的骨架,我們使用它可以很容易地拼裝出一個(gè)完整的頁(yè)面。

#示例

我們實(shí)現(xiàn)一個(gè)頁(yè)面,它包含:

  1. 一個(gè)導(dǎo)航欄
  2. 導(dǎo)航欄右邊有一個(gè)分享按鈕
  3. 有一個(gè)抽屜菜單
  4. 有一個(gè)底部導(dǎo)航
  5. 右下角有一個(gè)懸浮的動(dòng)作按鈕

最終效果如圖5-18、圖5-19所示:

圖5-18 圖5-19

實(shí)現(xiàn)代碼如下:

class ScaffoldRoute extends StatefulWidget {
  @override
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}


class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _selectedIndex = 1;


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar( //導(dǎo)航欄
        title: Text("App Name"), 
        actions: <Widget>[ //導(dǎo)航欄右側(cè)菜單
          IconButton(icon: Icon(Icons.share), onPressed: () {}),
        ],
      ),
      drawer: new MyDrawer(), //抽屜
      bottomNavigationBar: BottomNavigationBar( // 底部導(dǎo)航
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
          BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
          BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
      floatingActionButton: FloatingActionButton( //懸浮按鈕
          child: Icon(Icons.add),
          onPressed:_onAdd
      ),
    );
  }
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _onAdd(){
  }
}

上面代碼中我們用到了如下組件:

組件名稱 解釋
AppBar 一個(gè)導(dǎo)航欄骨架
MyDrawer 抽屜菜單
BottomNavigationBar 底部導(dǎo)航欄
FloatingActionButton 漂浮按鈕

下面我們來(lái)分別介紹一下它們。

#5.6.2 AppBar

AppBar是一個(gè) Material 風(fēng)格的導(dǎo)航欄,通過(guò)它可以設(shè)置導(dǎo)航欄標(biāo)題、導(dǎo)航欄菜單、導(dǎo)航欄底部的Tab標(biāo)題等。下面我們看看 AppBar 的定義:

AppBar({
  Key key,
  this.leading, //導(dǎo)航欄最左側(cè)Widget,常見為抽屜菜單按鈕或返回按鈕。
  this.automaticallyImplyLeading = true, //如果leading為null,是否自動(dòng)實(shí)現(xiàn)默認(rèn)的leading按鈕
  this.title,// 頁(yè)面標(biāo)題
  this.actions, // 導(dǎo)航欄右側(cè)菜單
  this.bottom, // 導(dǎo)航欄底部菜單,通常為Tab按鈕組
  this.elevation = 4.0, // 導(dǎo)航欄陰影
  this.centerTitle, //標(biāo)題是否居中 
  this.backgroundColor,
  ...   //其它屬性見源碼注釋
})

如果給Scaffold添加了抽屜菜單,默認(rèn)情況下Scaffold會(huì)自動(dòng)將AppBarleading設(shè)置為菜單按鈕(如上面截圖所示),點(diǎn)擊它便可打開抽屜菜單。如果我們想自定義菜單圖標(biāo),可以手動(dòng)來(lái)設(shè)置leading,如:

Scaffold(
  appBar: AppBar(
    title: Text("App Name"),
    leading: Builder(builder: (context) {
      return IconButton(
        icon: Icon(Icons.dashboard, color: Colors.white), //自定義圖標(biāo)
        onPressed: () {
          // 打開抽屜菜單  
          Scaffold.of(context).openDrawer(); 
        },
      );
    }),
    ...  
  )  

代碼運(yùn)行效果如圖5-20所示:

可以看到左側(cè)菜單已經(jīng)替換成功。

代碼中打開抽屜菜單的方法在ScaffoldState中,通過(guò)Scaffold.of(context)可以獲取父級(jí)最近的Scaffold 組件的State對(duì)象。

#TabBar

下面我們通過(guò)“bottom”屬性來(lái)添加一個(gè)導(dǎo)航欄底部 Tab 按鈕組,將要實(shí)現(xiàn)的效果如圖5-21所示:

圖5-21

Material 組件庫(kù)中提供了一個(gè)TabBar組件,它可以快速生成Tab菜單,下面是上圖對(duì)應(yīng)的源碼:

class _ScaffoldRouteState extends State<ScaffoldRoute>
    with SingleTickerProviderStateMixin {


  TabController _tabController; //需要定義一個(gè)Controller
  List tabs = ["新聞", "歷史", "圖片"];


  @override
  void initState() {
    super.initState();
    // 創(chuàng)建Controller  
    _tabController = TabController(length: tabs.length, vsync: this);
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        ... //省略無(wú)關(guān)代碼
        bottom: TabBar(   //生成Tab菜單
          controller: _tabController,
          tabs: tabs.map((e) => Tab(text: e)).toList()
        ),
      ),
      ... //省略無(wú)關(guān)代碼


  }

上面代碼首先創(chuàng)建了一個(gè)TabController ,它是用于控制/監(jiān)聽Tab菜單切換的。接下來(lái)通過(guò) TabBar 生成了一個(gè)底部菜單欄,TabBartabs屬性接受一個(gè) Widget 數(shù)組,表示每一個(gè) Tab 子菜單,我們可以自定義,也可以像示例中一樣直接使用Tab 組件,它是 Material 組件庫(kù)提供的 Material 風(fēng)格的 Tab 菜單。

Tab組件有三個(gè)可選參數(shù),除了可以指定文字外,還可以指定Tab菜單圖標(biāo),或者直接自定義組件樣式。Tab組件定義如下:

Tab({
  Key key,
  this.text, // 菜單文本
  this.icon, // 菜單圖標(biāo)
  this.child, // 自定義組件樣式
})

開發(fā)者可以根據(jù)實(shí)際需求來(lái)定制。

#TabBarView

通過(guò)TabBar我們只能生成一個(gè)靜態(tài)的菜單,真正的 Tab 頁(yè)還沒(méi)有實(shí)現(xiàn)。由于Tab菜單和 Tab 頁(yè)的切換需要同步,我們需要通過(guò)TabController去監(jiān)聽 Tab 菜單的切換去切換 Tab 頁(yè),代碼如:

_tabController.addListener((){  
  switch(_tabController.index){
    case 1: ...;
    case 2: ... ;   
  }
});

如果我們 Tab 頁(yè)可以滑動(dòng)切換的話,還需要在滑動(dòng)過(guò)程中更新 TabBar 指示器的偏移!顯然,要手動(dòng)處理這些是很麻煩的,為此,Material 庫(kù)提供了一個(gè)TabBarView組件,通過(guò)它不僅可以輕松的實(shí)現(xiàn) Tab 頁(yè),而且可以非常容易的配合 TabBar 來(lái)實(shí)現(xiàn)同步切換和滑動(dòng)狀態(tài)同步,示例如下:

Scaffold(
  appBar: AppBar(
    ... //省略無(wú)關(guān)代碼
    bottom: TabBar(
      controller: _tabController,
      tabs: tabs.map((e) => Tab(text: e)).toList()),
  ),
  drawer: new MyDrawer(),
  body: TabBarView(
    controller: _tabController,
    children: tabs.map((e) { //創(chuàng)建3個(gè)Tab頁(yè)
      return Container(
        alignment: Alignment.center,
        child: Text(e, textScaleFactor: 5),
      );
    }).toList(),
  ),
  ... // 省略無(wú)關(guān)代碼  
)    

運(yùn)行后效果如圖5-22所示:

圖5-22

現(xiàn)在,無(wú)論是點(diǎn)擊導(dǎo)航欄 Tab 菜單還是在頁(yè)面上左右滑動(dòng),Tab 頁(yè)面都會(huì)切換,并且 Tab 菜單的狀態(tài)和 Tab 頁(yè)面始終保持同步!那它們是如何實(shí)現(xiàn)同步的呢?細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn),上例中TabBarTabBarViewcontroller是同一個(gè)!正是如此,TabBarTabBarView正是通過(guò)同一個(gè)controller來(lái)實(shí)現(xiàn)菜單切換和滑動(dòng)狀態(tài)同步的,有關(guān)TabController的詳細(xì)信息,我們不在本書做過(guò)多介紹,使用時(shí)讀者直接查看 SDK 即可。

另外,Material 組件庫(kù)也提供了一個(gè)PageView 組件,它和TabBarView功能相似,讀者可以自行了解一下。

#5.6.3 抽屜菜單Drawer

ScaffolddrawerendDrawer屬性可以分別接受一個(gè) Widget 來(lái)作為頁(yè)面的左、右抽屜菜單。如果開發(fā)者提供了抽屜菜單,那么當(dāng)用戶手指從屏幕左(或右)側(cè)向里滑動(dòng)時(shí)便可打開抽屜菜單。本節(jié)開始部分的示例中實(shí)現(xiàn)了一個(gè)左抽屜菜單MyDrawer,它的源碼如下:

class MyDrawer extends StatelessWidget {
  const MyDrawer({
    Key key,
  }) : super(key: key);


  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        //移除抽屜菜單頂部默認(rèn)留白
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 38.0),
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: ClipOval(
                      child: Image.asset(
                        "imgs/avatar.png",
                        width: 80,
                      ),
                    ),
                  ),
                  Text(
                    "Wendux",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: <Widget>[
                  ListTile(
                    leading: const Icon(Icons.add),
                    title: const Text('Add account'),
                  ),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text('Manage accounts'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

抽屜菜單通常將Drawer組件作為根節(jié)點(diǎn),它實(shí)現(xiàn)了 Material 風(fēng)格的菜單面板,MediaQuery.removePadding可以移除 Drawer 默認(rèn)的一些留白(比如 Drawer 默認(rèn)頂部會(huì)留和手機(jī)狀態(tài)欄等高的留白),讀者可以嘗試傳遞不同的參數(shù)來(lái)看看實(shí)際效果。抽屜菜單頁(yè)由頂部和底部組成,頂部由用戶頭像和昵稱組成,底部是一個(gè)菜單列表,用 ListView 實(shí)現(xiàn),關(guān)于 ListView 我們將在后面“可滾動(dòng)組件”一節(jié)詳細(xì)介紹。

#5.6.4 FloatingActionButton

FloatingActionButton是 Material 設(shè)計(jì)規(guī)范中的一種特殊 Button,通常懸浮在頁(yè)面的某一個(gè)位置作為某種常用動(dòng)作的快捷入口,如本節(jié)示例中頁(yè)面右下角的"?"號(hào)按鈕。我們可以通過(guò)ScaffoldfloatingActionButton屬性來(lái)設(shè)置一個(gè)FloatingActionButton,同時(shí)通過(guò)floatingActionButtonLocation屬性來(lái)指定其在頁(yè)面中懸浮的位置,這個(gè)比較簡(jiǎn)單,不再贅述。

#5.6.5 底部Tab導(dǎo)航欄

我們可以通過(guò)ScaffoldbottomNavigationBar屬性來(lái)設(shè)置底部導(dǎo)航,如本節(jié)開始示例所示,我們通過(guò) Material 組件庫(kù)提供的BottomNavigationBarBottomNavigationBarItem兩種組件來(lái)實(shí)現(xiàn) Material 風(fēng)格的底部導(dǎo)航欄。可以看到上面的實(shí)現(xiàn)代碼非常簡(jiǎn)單,所以不再贅述,但是如果我們想實(shí)現(xiàn)如圖5-23所示效果的底部導(dǎo)航欄應(yīng)該怎么做呢?

圖5-23

Material組件庫(kù)中提供了一個(gè)BottomAppBar 組件,它可以和FloatingActionButton配合實(shí)現(xiàn)這種“打洞”效果,源碼如下:

bottomNavigationBar: BottomAppBar(
  color: Colors.white,
  shape: CircularNotchedRectangle(), // 底部導(dǎo)航欄打一個(gè)圓形的洞
  child: Row(
    children: [
      IconButton(icon: Icon(Icons.home)),
      SizedBox(), //中間位置空出
      IconButton(icon: Icon(Icons.business)),
    ],
    mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部導(dǎo)航欄橫向空間
  ),
)

可以看到,上面代碼中沒(méi)有控制打洞位置的屬性,實(shí)際上,打洞的位置取決于FloatingActionButton的位置,上面FloatingActionButton的位置為:

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

所以打洞位置在底部導(dǎo)航欄的正中間。

BottomAppBarshape屬性決定洞的外形,CircularNotchedRectangle實(shí)現(xiàn)了一個(gè)圓形的外形,我們也可以自定義外形,比如,F(xiàn)lutter Gallery 示例中就有一個(gè)“鉆石”形狀的示例,讀者感興趣可以自行查看。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)