分頁工具列 TabBar


在〈頂端工具列 AppBar〉中的範例出現了 TabBar,也就是 Flutter 中提供的分頁元件,當時只是隨便塞到 bottom,實際上你按下各個 Tab 元件是不會動作的,因為沒有指定各個 Tab 要顯示哪個 Widget 作為分頁。

TabBar 可以透過 tabs,設置多個 Tab,至於各個 Tab 對應的 Widget,可以用 TabBarView 來組織,例如,若有三個分頁內容:

TabBarView(
  children: [
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
      name: 'Java SE 14 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
      name: 'Python 3.7 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
      name: 'JavaScript 技術手冊',
    ),
  ],
)

接著就看你要將 TabBarView 擺在哪了,如果使用 Scaffold,通常會是擺到 body。

TabBar 的 tabs 與 TabBarView 的 children 要能對應起來,必須透過 TabController,如果應用程式的 UI 很單純,只會有一個 TabBar,最簡單的方式就是使用 DefaultTabController,這是個可以被子節點共用的 TabController,例如:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: TabBar(
            tabs: [
              Tab(text: 'Java'),
              Tab(text: 'Python'),
              Tab(text: 'JavaScript'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            Book(
              imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
              name: 'Java SE 14 技術手冊',
            ),
            Book(
              imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
              name: 'Python 3.7 技術手冊',
            ),
            Book(
              imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
              name: 'JavaScript 技術手冊',
            ),
          ],
        ),
      ),
    )
  )
);

class Book extends StatelessWidget {
  final String imgSrc;
  final String name;

  Book({this.imgSrc, this.name});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Center(
          child: Image.network(imgSrc),
        ),
        Text(name)
      ],
    );
  }
}

在這個例子中,因為 AppBar 的 title 接受的是 Widget,我就直接把 TabBar 擺上去了,DefaultTabController 要設置 length,表示可用來管理三個分頁,來看一下切換效果:

分頁工具列 TabBar

如果不想共用 TabController 的話,也可以自行建立 TabController,例如以下的程式實作,執行結果同上,然而是自行建立 TabController:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Home()
  )
);

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomeState();
}

// mixin SingleTickerProviderStateMixin 的實作
// 讓這個類別能提供 Ticker,這是做動畫時需要的元件
class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
  // 宣告 TabController
  TabController tabController;

  @override
  void initState() {
    // 建立 TabController,vsync 接受的型態是 TickerProvider
    tabController = new TabController(length: 3, vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TabBar(
          tabs: [
            Tab(text: 'Java'),
            Tab(text: 'Python'),
            Tab(text: 'JavaScript'),
          ],
          controller: tabController,  // 指定 TabController
        ),
      ),
      body: TabBarView(
        controller: tabController,    // 指定 TabController
        children: [
          Book(
            imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
            name: 'Java SE 14 技術手冊',
          ),
          Book(
            imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
            name: 'Python 3.7 技術手冊',
          ),
          Book(
            imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
            name: 'JavaScript 技術手冊',
          ),
        ],
      ),
    );
  }
}

class Book extends StatelessWidget {
  同前一個範...略
}

基本上需注意的地方,範例程式碼中都加上了註解,至於建立 TabController 時,為什麼要指定 TickerProvider?TickerProvider 其實是給 TabController 內部的 AnimationController 使用的,要詳細談的話,是與動畫操作有關,如果不關心動畫,就像範例這麼撰寫就可以了。

如果想知道原理的話,之後談動畫會再說明,這邊先簡單地說一下,Flutter 基本上希望能在畫面顯示時,提供每秒 60 個畫框(frame)的更新,而 TickerProvider 可以提供 Ticker 實例,每次 Flutter 在更新畫框(Frame),會呼叫指定給 Ticker 的函式,這個函式中可以操作 AnimationController,例如更新它的 value,動畫相關的邏輯依 AnimationController 的 value 等資訊來產生動畫。

也就是說,每次更新畫框時要做什麼,是由 Ticker 來決定,而 TickerProvider 會提供 Ticker,TabController 需要 TickerProvider,只是為了能實現切換分頁的動畫罷了,從這點來看,曝露其實是個有點奇怪的設計,畢竟你可能並不怎麼關心動畫,覺得預設的就足夠了,然而實作上還是要撰寫 with SingleTickerProviderStateMixin 之類的程式碼。