天天看點

UITableView在Flutter中是什麼?

前面我們學習了文本、圖檔和按鈕這些基本元素,這些基本元素需要進行排列組合,才能構成我們看到的UI視圖。那麼,當這些基本元素的排列布局超過螢幕顯示尺寸(即超過一屏)時,我們就需要引入清單控件來展示視圖的完整内容,并根據元素的多少進行自适應滾動展示。

這樣的需求,在iOS中是用UITableView實作的;而在Flutter中,實作這種需求的則是清單控件ListView。

ListView

在Flutter中,ListView可以沿一個方向(垂直或者水準方向)來排列其所有子Widget,是以常被用于需要展示一組連續視圖元素的場景,比如通訊錄、優惠券、商家清單等。

我們先來看看ListView怎麼用。ListView提供了一個預設構造函數ListView,我們可以通過設定它的 children 參數,很友善地将所有的子Widget包含到ListView中。

不過,這種建立方式要求提前将所有子Widget一次性建立好,而不是等到他們真正在螢幕上顯示的時候才會建立,是以有一個很明顯的缺點,就是性能不好。是以,這種方式僅适用于清單中含有少量元素的場景。

如下所示,我定義了一組清單項元件,并将他們放在了垂直滾動的ListView中:

ListView(
      children: <Widget>[
        //設定ListView元件的标題與圖示
        ListTile(leading: Icon(Icons.map), title: Text("map")),
        ListTile(leading: Icon(Icons.mail), title: Text("mail")),
        ListTile(leading: Icon(Icons.message), title: Text("message")),
      ],
    );           

複制

備注:ListTile是Flutter提供的用于快速建構清單項元素的一個小元件單元,用于1~3行(leading、title、subtitle)展示文本、圖示等視圖元素的場景,通常與ListView配合使用。

運作效果如下:

UITableView在Flutter中是什麼?

除了預設的垂直方向布局之外,ListView還可以通過設定 scrollDirection 參數支援水準方向布局。如下所示,我定義了一組不同顔色背景的元件,将他們的寬度設定為140,并包在了水準布局的ListView中,讓它們可以橫向滾動:

ListView(
      itemExtent: 140,//item延展尺寸(寬度)
      children: <Widget>[
        Container(color: Colors.black),
        Container(color: Colors.red),
        Container(color: Colors.blue),
        Container(color: Colors.green),
        Container(color: Colors.yellow),
        Container(color: Colors.orange),
      ],
      scrollDirection: Axis.horizontal,
    );           

複制

運作效果如下圖所示:

UITableView在Flutter中是什麼?

在這個例子中,我們一次性建立了6個子Widget。但是從上圖的運作效果可以看到,由于螢幕的寬高有限,同一時間使用者隻能看到3個Widget。也就是說,是否一次性提前建構出所有要展示的子Widget,于使用者而言并沒有什麼視覺上的差異。

是以,考慮到建立子Widget産生的性能問題,更好的方法是抽象出建立子Widget的方法,交由ListView統一管理,在真正需要展示該子Widget時再去建立。

ListView的另一個構造函數ListView.builder,則适用于子Widget比較多的場景,這個構造函數有兩個關鍵參數:

  • itemBuilder,是清單項的建立方法。當清單滾動到相應位置時,ListView會調用該方法建立對應的子Widget。
  • itemCount,表示清單項的數量,如果為空,則表示ListView為無限清單。

下面我通過一個案例與你說明itemBuilder與itemCount這兩個參數的具體用法。

我定義了一個擁有100個清單元素的ListView,在清單項的建立方法中,分别将index的值設定為ListTile的标題與子标題。比如,第一行清單項會展示title 0 body 0:

ListView.builder(
      itemCount: 100, //元素個數
      itemExtent: 66, //清單項高度
      itemBuilder: (context, index) {
        return ListTile(
          title: Text("title$index"),
          subtitle: Text("body$index"),
        );
      },
);           

複制

需要注意的是,itemExtent并不是一個必填參數。但,對于定高的清單項元素,最好是提前設定好這個參數的值。

因為如果這個參數為null,ListView會動态地根據子Widget建立完成的結果,決定自身的視圖高度,以及子Widget在ListView中的相對位置。在滾動發生變化而清單項又很多時,這樣的計算就會非常頻繁。

如果提前設定好itemExtent,ListView則可以計算好每一個清單項元素的相對位置,以及自身的視圖高度,省去了無謂的計算。

是以,在ListView中,指定itemExtent比讓子Widget自己決定自身高度會更有效。

運作這個示例,效果如下:

UITableView在Flutter中是什麼?

可能你已經發現了,我們的清單還缺少分割線。在ListView中,有兩種方式支援分割線:

  • 一種是,在itemBuilder中,根據index的值動态建立分割線,也就是将分割線視為清單項的一部分;
  • 另一種是,使用ListView的另一個構造方法,單獨設定分割線的樣式。

第一種方式實際上是試圖的結合,之前已經聊了很多了,這裡不做過多介紹。接下來,我示範一下如何使用ListView.separated設定分割線。

與 ListView.builder 抽離出了子Widget的建構方法 itemBuilder 類似,ListView.separated 抽離出了分割線的建構方法 separatorBuilder ,以便根據 index 設定不同樣式的分割線。

如下所示,我針對 index 為偶數的場景, 建立了綠色的分割線,而針對 index 為奇數的場景,建立了紅色的分割線:

ListView.separated(
      itemCount: 66,
      itemBuilder: (context, index){
        return ListTile(
          title: Text("title$index"),
          subtitle: Text("subtitle$index"),
        );
      },//建立子Widget
      separatorBuilder: (context, index){
        return Divider(
          color: index%2==0 ? Colors.green : Colors.red,
        );
      },//index為偶數,建立綠色分割線;index為奇數,建立紅色分割線。
    );           

複制

運作效果,如下圖所示:

UITableView在Flutter中是什麼?

好了,我已經與你分享完了ListView的常見構造函數。接下來,我準備了一張表格,總結了ListView常見的構造方法及其适用場景,供你參考,以便了解與記憶:

UITableView在Flutter中是什麼?

CustomScrollView

好了,ListView實作了單一視圖下可滾動Widget的互動模型,同時也包含了UI顯示相關的控制邏輯和布局模型。但是,對于某些特殊互動場景,比如多個效果關聯、嵌套滾動、精細滑動、視圖跟随手勢操作等,還需要嵌套多個ListView來實作。這時,各自視圖的滾動和布局模型就是互相獨立、分離的,就很難保證整個頁面統一一緻的滑動效果。

那麼,Flutter是如何解決多ListView嵌套時,頁面滑動效果不一緻的問題的呢?

在Flutter中,有一個專門的控件CustomScrollView,用來處理多個需要自定義滾動效果的Widget。在CustomScrollView中,這些彼此獨立的、可滾動的Widget被稱為Sliver。

比如,ListView的Sliver實作為SliverList,AppBar的Sliver實作為SliverAppBar。這些Sliver不再維護各自的滾動狀态,而是交由CustomScrollView統一管理,最終實作滑動效果的一緻性。

接下來,我通過一個滾動視差的例子,與你示範CustomScrollView的使用方法。

視差滾動是指讓多層背景以不同的速度移動,在形成立體滾動效果的同時,還能保證良好的視覺體驗。作為移動應用互動設計的熱點趨勢,越來越多的移動應用使用了這項技術。

以一個有着封面頭圖的清單為例,我們希望封面頭圖和清單這兩層視圖的滾動關聯起來,當使用者滾動清單時,頭圖會根據使用者的滾動手勢,進行縮小與展開。

經分析得出,要實作這樣的需求,我們需要兩個Sliver:作為頭圖的SliverAppBar,與作為清單的SliverList。具體的實作思路是:

  • 在建立SliverAppBar時,把 flexibleSpace 參數設定為懸浮頭圖背景。flexibleSpace 可以讓背景圖顯示在SliverAppBar下方,高度和SliverAppBar一樣;
  • 而在建立SliverList時,通過 SliverChildBuilderDelegate 參數實作清單項元素的建立;
  • 最後,将它們一并交由CustomScrollView的Slivers參數統一管理。

具體的示例代碼如下所示:

CustomScrollView(
      slivers: <Widget>[
        SliverAppBar(//SliverAppBar 作為頭圖控件
          title: Text("CustomScrollView Demo"),//标題
          flexibleSpace: Image.network("http://b-ssl.duitang.com/uploads/item/201509/03/20150903094844_WYjsH.jpeg"),//設定懸浮頭圖背景
          floating: true,//設定懸浮樣式
          expandedHeight: 300,//頭圖控件高度
        ),
        SliverList(//SliverList 作為清單控件
          delegate: SliverChildBuilderDelegate(
            (context, index){//清單項建立方法
              return ListTile(
                title: Text("title$index"),
              );
            },
            childCount: 66,//清單元素個數
          ),
        )
      ],
    );           

複制

運作一下,視差滾動效果如下所示:

UITableView在Flutter中是什麼?

ScrollController與ScrollNotification

現在,你應該已經知道如何實作滾動視圖的視覺和互動效果了。接下來我們考慮一個更加複雜的問題:在某些情況下,我們希望擷取視圖的滾動資訊,并進行相應的控制。比如,清單是否已經滑到底(頂)了?如何快速回到清單頂部?清單頂部是否已經開始,或者是否已經停下來了?

對于前兩個問題,我們可以使用ScrollController進行滾動資訊的監聽,以及相應的滾動控制;而最後一個問題,則需要接收ScrollNotification通知進行滾動事件的擷取。下面我将分别與你介紹。

在Flutter中,因為Widget并不是渲染到螢幕的最終視覺元素(RenderObject才是),是以我們無法像原生的iOS或Android一樣,向持有的Widget對象擷取或設定最終渲染相關的視覺資訊,而必須通過對應的元件控制器才能實作。

ListView的元件控制器是ScrollController,我們可以通過它來擷取視圖的滾動資訊,更新視圖的滾動位置。

一般而言,擷取視圖的滾動資訊往往是為了進行界面的狀态控制,是以ScrollController的初始化、監聽及銷毀需要與StatefulWidget的狀态保持同步。

如下代碼所示,我們聲明了一個有着100個元素的清單項,當滾動視圖到特定位置後,使用者可以點選按鈕傳回到清單頂部:

  • 首先,我們在State的初始化方法裡,建立了ScrollController,并通過_controller.addListener方法注冊了滾動監聽方法回調,根據目前視圖的滾動位置,判斷目前是否需要展示“Top”按鈕。
  • 随後,在視圖建構方法build中,我們将ScrollController對象與ListView進行了關聯,并且在RaisedButton中注冊了對應的回調方法,可以在點選按鈕時通過_controller.animateTo方法傳回到清單頂部。
  • 最後,在State的銷毀方法中,我們對ScrollController進行了資源釋放。
class MyAPPState extends State<MyApp> {
  ScrollController _controller;//ListView 控制器
  bool isToTop = false;// 标示目前是否需要啟用 "Top" 按鈕
  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(() {// 為控制器注冊滾動監聽方法
      if(_controller.offset > 1000) {// 如果 ListView 已經向下滾動了 1000,則啟用 Top 按鈕
        setState(() {isToTop = true;});
      } else if(_controller.offset < 300) {// 如果 ListView 向下滾動距離不足 300,則禁用 Top 按鈕
        setState(() {isToTop = false;});
      }
    });
    super.initState();
  }

  Widget build(BuildContext context) {
    return MaterialApp(
        ...
        // 頂部 Top 按鈕,根據 isToTop 變量判斷是否需要注冊滾動到頂部的方法
        RaisedButton(onPressed: (isToTop ? () {
                  if(isToTop) {
                    _controller.animateTo(.0,
                        duration: Duration(milliseconds: 200),
                        curve: Curves.ease
                    );// 做一個滾動到頂部的動畫
                  }
                }:null),child: Text("Top"),)
        ...
        ListView.builder(
                controller: _controller,// 初始化傳入控制器
                itemCount: 100,// 清單元素總數
                itemBuilder: (context, index) => ListTile(title: Text("Index : $index")),// 清單項構造方法
               )      
        ...   
    );

  @override
  void dispose() {
    _controller.dispose(); // 銷毀控制器
    super.dispose();
  }
}
           

複制

ScrollController的運作效果如下所示:

UITableView在Flutter中是什麼?

介紹完了如何通過ScrollController來監聽ListView滾動資訊,以及怎樣進行滾動控制之後,接下來我們再來看看如何擷取ScrollNotification通知,進而感覺ListView的各類滾動事件。

在Flutter中,ScrollNotification通知的擷取是通過NotificationListener來實作的。與ScrollController不同的是,NotificationListener是一個Widget,為了監聽滾動類型的事件,我們需要将NotificationListener添加為ListView的父容器,進而捕獲ListView中的通知。而這些通知,需要通過onNotification回調函數實作監聽邏輯:

Widget build(BuildContext context) {
  return MaterialApp(
    title: 'ScrollController Demo',
    home: Scaffold(
      appBar: AppBar(title: Text('ScrollController Demo')),
      body: NotificationListener<ScrollNotification>(// 添加 NotificationListener 作為父容器
        onNotification: (scrollNotification) {// 注冊通知回調
          if (scrollNotification is ScrollStartNotification) {// 滾動開始
            print('Scroll Start');
          } else if (scrollNotification is ScrollUpdateNotification) {// 滾動位置更新
            print('Scroll Update');
          } else if (scrollNotification is ScrollEndNotification) {// 滾動結束
            print('Scroll End');
          }
        },
        child: ListView.builder(
          itemCount: 30,// 清單元素個數
          itemBuilder: (context, index) => ListTile(title: Text("Index : $index")),// 清單項建立方法
        ),
      )
    )
  );
}
           

複制

相比于ScrollController隻能和具體的ListView關聯後才可以監聽到滾動資訊;通過NotificationListener則可以監聽其子Widget中的任意ListView,不僅可以得到這些ListView的目前滾動位置資訊,還可以擷取目前的滾動事件資訊。

總結

在處理展示一組連續、可滾動的視圖元素的場景中,Flutter提供了比原生Android、iOS系統更為強大的清單元件ListView與CustomScrollView。

ListView元件,同時支援垂直方向和水準方向滾動,不僅提供了少量一次性建立子視圖的預設構造方法,也提供了大量按需建立子視圖的ListView.builder機制,并且支援自定義分割線。為了節省性能,對于定高的清單項視圖,提前指定itemExtent比讓子Widget自己決定要更高效。

ScrollController與ListView綁定,進行滾動資訊的監聽,進行相應的滾動控制;NotificationListener,通過将ListView納入子Widget,實作滾動事件的擷取。

以上。