不一樣角度帶你了解 Flutter 中的滑動列表實(shí)現(xiàn)

本篇主要幫助剖析理解 Flutter 里的列表和滑動的組成,用比較通俗易懂的方式,從常見的 ListViewNestedScrollView 的內(nèi)部實(shí)現(xiàn),幫助你更好理解和運(yùn)用 Flutter 里的滑動列表。

本篇不是教你如何使用 API ,而是一些日常開發(fā)中不常接觸,但是很重要的內(nèi)容

Flutter 滑動列表

在 Flutter 里我們常見的滑動列表場景,簡單地說其實(shí)是由三部分組成:

  • Viewport : 它是一個 MultiChildRenderObjectWidget 的控件 ,它提供的是一個“視窗”的作用,也就是列表所在的可視區(qū)域大??;
  • Scrollable它主要通過對手勢的處理來實(shí)現(xiàn)滑動效果 ,比如VerticalDragGestureRecognizerHorizontalDragGestureRecognizer;
  • Sliver : 準(zhǔn)確來說應(yīng)該是 RenderSliver, 它主要是用于在 Viewport 里面布局和渲染內(nèi)容;
image

ListView 為例,如上圖所示是 ListView 滑動過程的變化,其中:

  • 綠色的 Viewport 就是我們看到的列表窗口大??;
  • 紫色部分就是處理手勢的 Scrollable,讓黃色部分 SliverListViewport 里產(chǎn)生滑動;
  • 黃色的部分就是 SliverList , 當(dāng)我們滑動時其實(shí)就是它在 Viewport 里的位置發(fā)生了變化;

了解完這個基礎(chǔ)理念后,就可以知道一般情況下 ViewportScrollable 的實(shí)現(xiàn)都是很通用的,所以一般在 Flutter 里要實(shí)現(xiàn)不同的滑動列表,就是通過自定義和組合不同的 Sliver 來完成布局

準(zhǔn)確說是完成 RenderSliverperformLayout 過程,通過 SliverConstraints 來得到對應(yīng)的 SliverGeometry。

所以在 Flutter 里:

  • ListView 使用的是 SliverFixedExtentList 或者 SliverList;
  • GridView 使用的是 SliverGrid;
  • PageView 使用的是 SliverFillViewport

當(dāng)然這里有一個特殊的是 SingleChildScrollView , 因?yàn)樗菃蝹€ child 的可滑動控件,它并沒有使用 RenderSliver,而是直接自定義了一個 RenderObject(RenderBox) ,并且performLayout 時直接調(diào)整 childoffset 來達(dá)到滑動效果。

RenderSliver

我們都知道 Flutter 中的整體渲染流程是 Widget -> Element -> RenderObejct -> Layer 這樣的過程,而 Flutter 里的布局和繪制邏輯都在 RenderObejct。

而事實(shí)上 RenderObejct 也可以分為兩大基礎(chǔ)子類:

  • RenderBox : 我們常用的布局控件都是基于 RenderBox 來實(shí)現(xiàn)布局;
  • RenderSliver主要用在 Viewport 里實(shí)現(xiàn)布局, Viewport 里的直屬 children 也需要是 RenderSliver

那到這里你可能會有一個疑問:既然前面 SingleChildScrollView 里沒有使用 RenderSliver ,直接使用 RenderBox 也可以實(shí)現(xiàn)滑動,為什么還要用 Viewport + RenderSliver 的方式來實(shí)現(xiàn)列表滑動?

RenderBox

SingleChildScrollView 內(nèi)部使用的是 RenderBox ,那么在布局過程中自然而然會把整個 child 都進(jìn)行布局和計(jì)算,繪制時主要也是通過 offsetclip 等來完成移動效果,這樣的實(shí)現(xiàn)當(dāng) child 比較復(fù)雜或者過長時,性能就會變差

RenderSliver

RenderSliver 的實(shí)現(xiàn)相對 RenderBox 就復(fù)雜更多,前面介紹過 RenderSliver 就是通過 SliverConstraints 來得到一個 SliverGeometry,其中:

  • SliverConstraints 中有 remainingPaintExtent 可以用來表示剩余的可繪制具體的大??;

  • SliverGeometry 里也有 scrollExtent (可滑動的距離)、paintExtent(可繪制大?。?code>layoutExtent (布局大小范圍)、visible(是否需要繪制)等參數(shù);

所以通過這部分參數(shù),Viewport 里可以實(shí)現(xiàn)動態(tài)管理,節(jié)省資源,根據(jù) SliverGeometry 判斷需要繪制多大區(qū)域的內(nèi)容,還剩多少內(nèi)容可以繪制,需要加載的布局是哪些等等。

簡單地說就是可以實(shí)現(xiàn)“懶加載”,按需繪制,從而得到更流暢的滑動體驗(yàn)。

image

ListView 為例,如上圖所示是一個高為 701 的 ListView ,實(shí)際布局渲染之后,對于 SliverList 輸出的 SliverGeometry 而言:

  • 設(shè)定里每個 item 的高度為 114;
  • scrollExtent 是 2353,也就是整體可滑動距離等于 2353;
  • paintExtent 是 701 , 因?yàn)?ListViewViewport 是 701 ,所以從 SliverConstraints 得到的 remainingPaintExtent 是 701,所以默認(rèn)只需要繪制和布局高度為 701 的部分; (因?yàn)槟J(rèn) paintExtent = layoutExtent )
  • 對 item 多出的藍(lán)色 8-9 部分,這是因?yàn)樵? SliverConstraints 內(nèi)會有一個叫 remainingCacheExtent 的參數(shù),它表示了需要提前緩存的布局區(qū)域, 也就是“預(yù)布局”的區(qū)域,這個區(qū)域默認(rèn)大小是 defaultCacheExtent= 250.0;

ListView 高度為 701,defaultCacheExtent 為默認(rèn)的 250,也就是得到第一次需要布局到底部的距離其實(shí)為 951,按照每個 item 高度是 114 ,那么其實(shí)是有 8.3 個 item 高度,取整數(shù)也就是 9 個 item ,最終得到整體需要處理的區(qū)域大小為 114 * 9 = 1026 ,在 SliverList 內(nèi)部就是 endScrollOffset 參數(shù)

所以根據(jù)以上情況,ListView 會輸出一個 paintExtent 為 701 ,cacheExtent 為 1026 的 SliverGeometry

從這個例子可以看出,RenderSliver 在實(shí)現(xiàn)可滑動列表的開銷和邏輯上,會比直接使用 RenderBox 好和靈活很多,同時也是為什么 Viewport 里需要使用 RenderSliver 而不是 RenderBox 的原因。

??注意,這里比較容易有一個誤區(qū),那就是 ListView 是由 Viewport + Scrollable 和一個RenderSliver 組成,所以在 ListView 里只會有一個 RenderSliver 而不是多個,想使用多個 RenderSliver 需要使用 CustomScrollView

最后順便聊下 CustomScrollView ,事實(shí)上就是一個開放了可自定義配置 RenderSliver 數(shù)組的滑動控件,例如:

  • 通過利用 SliverList + SliverGrid 就可以搭配出多樣化的滑動列表;
  • 通過 CupertinoSliverRefreshControl + SliverList 實(shí)現(xiàn)類似 iOS 原生的下拉刷新列表;

其他可用的內(nèi)置 Sliver 還有:SliverPaddingSliverFillRemaining 、SliverFillViewport 、SliverPersistentHeaderSliverAppbar 等等。

NestedScrollView

為什么會把 NestedScrollView 單獨(dú)拿出來說呢?這是因?yàn)?NestedScrollView 和前面介紹的滑動列表實(shí)現(xiàn)不大一樣。

內(nèi)部組成

image

如上圖所示,NestedScrollView 內(nèi)部主要是通過繼承 CustomScrollView ,然后自定義一個 NestedScrollViewViewport 來實(shí)現(xiàn)聯(lián)動的效果。

那這有什么特別的呢?如下代碼所示,這是使用 NestedScrollView 常用的模式,那有看出什么特別的地方了嗎?

image

代碼里 NestedScrollViewbody 嵌套的是 ListView , 前面我們介紹了 ListView 本身就是 Viewport + Scrollable + SliverList 組合,而 NestedScrollView 本身也有 NestedScrollViewViewport

所以 NestedScrollView 的實(shí)現(xiàn)本質(zhì)上其實(shí)就是 Viewport 嵌套 Viewport,會有兩個 Scrollable 的存在 ,并且嵌套的 ListView 是被放在了 NestedScrollViewSliver 里面,大致如下圖所示。

image

這里面有幾個關(guān)鍵的對象,其中:

  • SliverFillRemaining :用于充滿 Viewport 的剩余空間,在 NestedScrollView 里面就是充滿 header 之外的剩余空間;

  • NestedScrollViewViewport : 在原 Viewport 的基礎(chǔ)上增加了一個 SliverOverlapAbsorberHandle 參數(shù),SliverOverlapAbsorberHandle 本身是一個 ChangeNotifier , 主要是用來當(dāng) markNeedsLayout 時對外發(fā)出通知,比如對 header 部分;

所以 NestedScrollView 本質(zhì)上兩個 Viewport 之間的嵌套,那他們之間是滑動關(guān)系是如何處理的?這就要說到 NestedScrollView 里的 _NestedScrollCoordinator 對象。

_NestedScrollCoordinator

_NestedScrollCoordinator 的實(shí)現(xiàn)比較復(fù)雜,簡單地說 _NestedScrollCoordinator 內(nèi)部創(chuàng)建了兩個 _NestedScrollController

  • _outerController :屬于 _NestedScrollViewCustomScrollViewcontroller ,也就是它自己 controller;
  • _innerController :屬于 bodycontroller
image

ListView 的父類 ScrollView 內(nèi)部,默認(rèn)情況下使用的就是 PrimaryScrollController.of(context) 這個 controller ,因?yàn)?PrimaryScrollController 是一個 InheritedWidget

而整個聯(lián)動滑動的流程,主要就是 _NestedScrollCoordinator 里和它創(chuàng)建的兩個 _NestedScrollController 有關(guān)系:

  • _NestedScrollController 的主要作用就是使用 _NestedScrollPosition 來替換 ScrollPosition ;

  • _NestedScrollCoordinator 將 _outer 和 _inner 兩個 _NestedScrollController 組合起來(_outer 和 _inner 分別被應(yīng)用到 NestedScrollViewbody);

  • _NestedScrollPosition 內(nèi)部將 Drag 等手勢操作傳遞回 _NestedScrollCoordinator 里。

  • 最后在 _NestedScrollCoordinatordragapplyUserOffset 等方法里進(jìn)行內(nèi)外滾動的分配;

image

SliverPersistentHeader

了解完 NestedScrollView 的布局和聯(lián)動實(shí)現(xiàn)之外,最后簡單介紹一下 SliverPersistentHeader , 因?yàn)榻?jīng)常在 NestedScrollView 里使用的 SliverAppBar,本質(zhì)上 SliverAppBar 的實(shí)現(xiàn)靠的就是 SliverPersistentHeader。

SliverPersistentHeader 主要是具備 floatingpinned 兩個屬性,它們的區(qū)別主要在于使用了不同的 RenderSliver 實(shí)現(xiàn),而最終不同的地方其實(shí)就是輸出 SliverGeometry 的不同。

image

以第一個 _SliverFloatingPinnedPersistentHeader 和最后一個 _SliverScrollingPersistentHeader 之間的對比為例子,如下代碼所示,在需要 floatingpinnedSliver 上,可以看到 paintExtentlayoutExtent 都有一個最小值。

image

所以 Sliver 被固定住的原理,其實(shí)就是 Viewport 得到了它的 paintExtentlayoutExtent 并不為 0,所以會繼續(xù)為這個 Sliver 繪制對應(yīng)區(qū)域的內(nèi)容。

最后需要注意的是,當(dāng)你使用 SliverPersistentHeader 去固定住頭部的時候,作為 body 的列表是不知道頂部有個固定區(qū)域。 所以如果這時候不額外做一些處理,那么對于 body 而言,它的 paintOrigin 還是從最頂部開始而不是固定區(qū)域的下方。

image

如上動圖所示,可以看到 item0 并沒有在橙色區(qū)域停止滑動,而是繼續(xù)往上滑動,這就是因?yàn)樽鳛?body 的列表不知道頂部有固定區(qū)域。

這時候就可以通過使用 SliverOverlapAbsorber + SliverOverlapInjector 的組合來解決這個問題:

  • SliverPersistentHeader 的外層嵌套一個 SliverOverlapAbsorber 用于吸收 SliverPersistentHeader 的高度;

  • 使用 SliverOverlapInjector 將這個高度配置到 body 列表中,讓列表知道頂部存在一個固定高度的區(qū)域;

image

這部分例子可見:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/sliver_list_demo_page.dart

好了,本篇關(guān)于 Flutter 滑動列表的實(shí)現(xiàn)原理就介紹完了,如果你還有什么想說的,歡迎留言討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容