Flutter之可滾動組件實例詳解

正文

當內容超過顯示視口(ViewPort)時,如果沒有特殊處理,Flutter則會提示Overflow錯誤。為此,Flutter提供瞭多種可滾動widget(Scrollable Widget)用於顯示列表和長佈局。

Flutter中有兩種佈局模型:

  • 基於 RenderBox 的盒模型佈局。
  • 基於 Sliver ( RenderSliver ) 按需加載列表佈局。

通常可滾動組件的子組件可能會非常多、占用的總高度也會非常大;如果要一次性將子組件全部構建出將會非常昂貴!為此,Flutter中提出一個Sliver(中文為“薄片”的意思)概念,Sliver 可以包含一個或多個子組件。Sliver 的主要作用是配合:加載子組件並確定每一個子組件的佈局和繪制信息,如果 Sliver 可以包含多個子組件時,通常會實現按需加載模型。

隻有當Sliver 出現在視口中時才會去構建它,這種模型也稱為“基於Sliver的列表按需加載模型”。可滾動組件中有很多都支持基於Sliver的按需加載模型,如ListViewGridView,但是也有不支持該模型的,如SingleChildScrollView

Flutter 中的可滾動主要由三個角色組成:Scrollable、Viewport 和 Sliver:

  • Scrollable :用於處理滑動手勢,確定滑動偏移,滑動偏移變化時構建 Viewport 。
  • Viewport:顯示的視窗,即列表的可視區域;
  • Sliver:視窗裡顯示的元素。

具體佈局過程:

  • Scrollable 監聽到用戶滑動行為後,根據最新的滑動偏移構建 Viewport 。
  • Viewport 將當前視口信息和配置信息通過 SliverConstraints 傳遞給 Sliver。
  • Sliver 中對子組件(RenderBox)按需進行構建和佈局,然後確認自身的位置、繪制等信息,保存在 geometry 中(一個 SliverGeometry 類型的對象)

比如有一個 ListView,大小撐滿屏幕,假設它有 100 個列表項(都是RenderBox)且每個列表項高度相同,結構如下:

圖中白色區域為設備屏幕,也是 Scrollable 、 Viewport 和 Sliver 所占用的空間,三者所占用的空間重合,父子關系為:Sliver 父組件為 Viewport,Viewport的 父組件為 Scrollable 。註意ListView 中隻有一個 Sliver,在 Sliver 中實現瞭子組件的按需加載。

其中頂部和底部灰色的區域為 cacheExtent,它表示預渲染的高度,需要註意這是在可視區域之外,如果 RenderBox 進入這個區域內,即使它還未顯示在屏幕上,也是要先進行構建的,預渲染是為瞭後面進入 Viewport 的時候更絲滑。cacheExtent 的默認值是 250,在構建可滾動列表時我們可以指定這個值,這個值最終會傳給 Viewport。

Scrollable

用於處理滑動手勢,確定滑動偏移,滑動偏移變化時構建 Viewport,我們看一下其關鍵的屬性:

Scrollable({
  ...
  this.axisDirection = AxisDirection.down,
  this.controller,
  this.physics,
  required this.viewportBuilder, //後面介紹
})
  • axisDirection 滾動方向。
  • physics:此屬性接受一個ScrollPhysics類型的對象,它決定可滾動組件如何響應用戶操作,比如用戶滑動完抬起手指後,繼續執行動畫;或者滑動到邊界時,如何顯示。默認情況下,Flutter會根據具體平臺分別使用不同的ScrollPhysics對象,應用不同的顯示效果,如當滑動到邊界時,繼續拖動的話,在 iOS 上會出現彈性效果,而在 Android 上會出現微光效果。如果你想在所有平臺下使用同一種效果,可以顯式指定一個固定的ScrollPhysics,Flutter SDK中包含瞭兩個ScrollPhysics的子類,他們可以直接使用:
AlwaysScrollableScrollPhysics:總是可以滑動
NeverScrollableScrollPhysics:禁止滾動
BouncingScrollPhysics :內容超過一屏 上拉有回彈效果
ClampingScrollPhysics :包裹內容 不會有回彈
  • controller:此屬性接受一個ScrollController對象。ScrollController的主要作用是控制滾動位置和監聽滾動事件。默認情況下,Widget樹中會有一個默認的PrimaryScrollController,如果子樹中的可滾動組件沒有顯式的指定controller,並且primary屬性值為true時(默認就為true),可滾動組件會使用這個默認的PrimaryScrollController。這種機制帶來的好處是父組件可以控制子樹中可滾動組件的滾動行為,例如,Scaffold正是使用這種機制在iOS中實現瞭點擊導航欄回到頂部的功能。
  • viewportBuilder:構建 Viewport 的回調。當用戶滑動時,Scrollable 會調用此回調構建新的 Viewport,同時傳遞一個 ViewportOffset 類型的 offset 參數,該參數描述 Viewport 應該顯示那一部分內容。註意重新構建 Viewport 並不是一個昂貴的操作,因為 Viewport 本身也是 Widget,隻是配置信息,Viewport 變化時對應的 RenderViewport 會更新信息,並不會隨著 Widget 進行重新構建。

主軸和縱軸

在可滾動組件的坐標描述中,通常將滾動方向稱為主軸,非滾動方向稱為縱軸。由於可滾動組件的默認方向一般都是沿垂直方向,所以默認情況下主軸就是指垂直方向,水平方向同理。

Viewport

Viewport 比較簡單,用於渲染當前視口中需要顯示 Sliver。

Viewport({
  Key? key,
  this.axisDirection = AxisDirection.down,
  this.crossAxisDirection,
  this.anchor = 0.0,
  required ViewportOffset offset, // 用戶的滾動偏移
  // 類型為Key,表示從什麼地方開始繪制,默認是第一個元素
  this.center,
  this.cacheExtent, // 預渲染區域
  //該參數用於配合解釋cacheExtent的含義,也可以為主軸長度的乘數
  this.cacheExtentStyle = CacheExtentStyle.pixel, 
  this.clipBehavior = Clip.hardEdge,
  List<Widget> slivers = const <Widget>[], // 需要顯示的 Sliver 列表
})

需要註意的是:

  • offset:該參數為Scrollabel 構建 Viewport 時傳入,它描述瞭 Viewport 應該顯示那一部分內容。
  • cacheExtent 和 cacheExtentStyle:CacheExtentStyle 是一個枚舉,有 pixel 和 viewport 兩個取值。當 cacheExtentStyle 值為 pixel 時,cacheExtent 的值為預渲染區域的具體像素長度;當值為 viewport 時,cacheExtent 的值是一個乘數,表示有幾個 viewport 的長度,最終的預渲染區域的像素長度為:cacheExtent * viewport 的積, 這在每一個列表項都占滿整個 Viewport 時比較實用,這時 cacheExtent 的值就表示前後各緩存幾個頁面。

Sliver

Sliver 主要作用是對子組件進行構建和佈局,比如 ListView 的 Sliver 需要實現子組件(列表項)按需加載功能,隻有當列表項進入預渲染區域時才會去對它進行構建和佈局、渲染。

Sliver 對應的渲染對象類型是 RenderSliver,RenderSliver 和 RenderBox 的相同點是都繼承自 RenderObject 類,不同點是在佈局的時候約束信息不同。RenderBox 在佈局時父組件傳遞給它的約束信息對應的是 BoxConstraints,隻包含最大寬高的約束;而 RenderSliver 在佈局時父組件(列表)傳遞給它的約束是對應的是 SliverConstraints。

可滾動組件的通用配置

幾乎所有的可滾動組件在構造時都能指定 scrollDirection(滑動的主軸)、reverse(滑動方向是否反向)、controller、physics 、cacheExtent ,這些屬性最終會透傳給對應的 Scrollable 和 Viewport,這些屬性我們可以認為是可滾動組件的通用屬性.

reverse表示是否按照閱讀方向相反的方向滑動,如:scrollDirection值為Axis.horizontal 時,即滑動發現為水平,如果閱讀方向是從左到右。

reversetrue時,那麼滑動方向就是從右往左。

ScrollController

可滾動組件都有一個 controller 屬性,通過該屬性我們可以指定一個 ScrollController 來控制可滾動組件的滾動,比如可以通過ScrollController來同步多個組件的滑動聯動。

子節點緩存

按需加載子組件在大多數場景中都能有正收益,但是有些時候也會有副作用。比如有一個頁面,它由一個ListView 組成,我們希望在頁面頂部顯示一塊內容, 這部分內容的數據需要在每次頁面打開時通過網絡來獲取,為此我們通一個 Header 組件來實現,它是一個 StatefulWidget ,會在initState 中請求網絡數據,然後將它作為 ListView 的第一個孩子。現在問題來瞭,因為 ListView 是按需加載子節點的,這意味著如果 Header 滑出 Viewport 的預渲染區域之外時就會被銷毀,重新滑入後又會被重新構建,這樣就會發起多次網絡請求,不符合我們期望。

為瞭解決上述問題,可滾動組件提供瞭一種緩存子節點的通用解決方案,它允許開發者對特定的子界限進行緩存.

Scrollbar

Scrollbar是一個Material風格的滾動指示器(滾動條),如果要給可滾動組件添加滾動條,隻需將Scrollbar作為可滾動組件的任意一個父級組件即可,如:

Scrollbar(
  child: SingleChildScrollView(
    ...
  ),
);

ScrollbarCupertinoScrollbar都是通過監聽滾動通知來確定滾動條位置的。

CupertinoScrollbar

CupertinoScrollbar是 iOS 風格的滾動條,如果你使用的是Scrollbar,那麼在iOS平臺它會自動切換為CupertinoScrollbar

總結

本篇介紹瞭可滾動組件的概念和具體的組成,構造。後續會具體介紹一些可滾動組件的使用詳解。如ListView,GridView等,更多關於Flutter 可滾動組件的資料請關註WalkonNet其它相關文章!

推薦閱讀: