本篇主要幫助剖析理解 Flutter 里的列表和滑動的組成,用比較通俗易懂的方式,從常見的 ListView 到 NestedScrollView 的內(nèi)部實(shí)現(xiàn),幫助你更好理解和運(yùn)用 Flutter 里的滑動列表。
本篇不是教你如何使用 API ,而是一些日常開發(fā)中不常接觸,但是很重要的內(nèi)容。
Flutter 滑動列表
在 Flutter 里我們常見的滑動列表場景,簡單地說其實(shí)是由三部分組成:
-
Viewport: 它是一個 MultiChildRenderObjectWidget 的控件 ,它提供的是一個“視窗”的作用,也就是列表所在的可視區(qū)域大??; -
Scrollable:它主要通過對手勢的處理來實(shí)現(xiàn)滑動效果 ,比如VerticalDragGestureRecognizer 和 HorizontalDragGestureRecognizer; -
Sliver: 準(zhǔn)確來說應(yīng)該是 RenderSliver, 它主要是用于在 Viewport 里面布局和渲染內(nèi)容;
以 ListView 為例,如上圖所示是 ListView 滑動過程的變化,其中:
- 綠色的
Viewport就是我們看到的列表窗口大??; - 紫色部分就是處理手勢的
Scrollable,讓黃色部分SliverList在Viewport里產(chǎn)生滑動; - 黃色的部分就是
SliverList, 當(dāng)我們滑動時其實(shí)就是它在Viewport里的位置發(fā)生了變化;
了解完這個基礎(chǔ)理念后,就可以知道一般情況下 Viewport 和 Scrollable 的實(shí)現(xiàn)都是很通用的,所以一般在 Flutter 里要實(shí)現(xiàn)不同的滑動列表,就是通過自定義和組合不同的 Sliver 來完成布局。
準(zhǔn)確說是完成
RenderSliver的performLayout過程,通過SliverConstraints來得到對應(yīng)的SliverGeometry。
所以在 Flutter 里:
-
ListView使用的是SliverFixedExtentList或者SliverList; -
GridView使用的是SliverGrid; -
PageView使用的是SliverFillViewport;
當(dāng)然這里有一個特殊的是
SingleChildScrollView, 因?yàn)樗菃蝹€child的可滑動控件,它并沒有使用RenderSliver,而是直接自定義了一個RenderObject(RenderBox) ,并且在performLayout時直接調(diào)整child的offset來達(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ì)算,繪制時主要也是通過 offset 和 clip 等來完成移動效果,這樣的實(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)。
以 ListView 為例,如上圖所示是一個高為 701 的 ListView ,實(shí)際布局渲染之后,對于 SliverList 輸出的 SliverGeometry 而言:
- 設(shè)定里每個 item 的高度為 114;
-
scrollExtent是 2353,也就是整體可滑動距離等于 2353; -
paintExtent是 701 , 因?yàn)?ListView的Viewport是 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 還有:SliverPadding 、SliverFillRemaining 、SliverFillViewport 、SliverPersistentHeader 、SliverAppbar 等等。
NestedScrollView
為什么會把 NestedScrollView 單獨(dú)拿出來說呢?這是因?yàn)?NestedScrollView 和前面介紹的滑動列表實(shí)現(xiàn)不大一樣。
內(nèi)部組成
如上圖所示,NestedScrollView 內(nèi)部主要是通過繼承 CustomScrollView ,然后自定義一個 NestedScrollViewViewport 來實(shí)現(xiàn)聯(lián)動的效果。
那這有什么特別的呢?如下代碼所示,這是使用 NestedScrollView 常用的模式,那有看出什么特別的地方了嗎?
代碼里 NestedScrollView 的 body 嵌套的是 ListView , 前面我們介紹了 ListView 本身就是 Viewport + Scrollable + SliverList 組合,而 NestedScrollView 本身也有 NestedScrollViewViewport。
所以 NestedScrollView 的實(shí)現(xiàn)本質(zhì)上其實(shí)就是 Viewport 嵌套 Viewport,會有兩個 Scrollable 的存在 ,并且嵌套的 ListView 是被放在了 NestedScrollView 的 Sliver 里面,大致如下圖所示。
這里面有幾個關(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:屬于_NestedScrollViewCustomScrollView的 controller ,也就是它自己 controller; -
_innerController:屬于body的 controller;
在
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)用到NestedScrollView和body);_NestedScrollPosition內(nèi)部將Drag等手勢操作傳遞回_NestedScrollCoordinator里。最后在
_NestedScrollCoordinator的drag和applyUserOffset等方法里進(jìn)行內(nèi)外滾動的分配;
SliverPersistentHeader
了解完 NestedScrollView 的布局和聯(lián)動實(shí)現(xiàn)之外,最后簡單介紹一下 SliverPersistentHeader , 因?yàn)榻?jīng)常在 NestedScrollView 里使用的 SliverAppBar,本質(zhì)上 SliverAppBar 的實(shí)現(xiàn)靠的就是 SliverPersistentHeader。
SliverPersistentHeader 主要是具備 floating 和 pinned 兩個屬性,它們的區(qū)別主要在于使用了不同的 RenderSliver 實(shí)現(xiàn),而最終不同的地方其實(shí)就是輸出 SliverGeometry 的不同。
以第一個 _SliverFloatingPinnedPersistentHeader 和最后一個 _SliverScrollingPersistentHeader 之間的對比為例子,如下代碼所示,在需要 floating 和 pinned 的 Sliver 上,可以看到 paintExtent 和 layoutExtent 都有一個最小值。
所以 Sliver 被固定住的原理,其實(shí)就是 Viewport 得到了它的 paintExtent 和 layoutExtent 并不為 0,所以會繼續(xù)為這個 Sliver 繪制對應(yīng)區(qū)域的內(nèi)容。
最后需要注意的是,當(dāng)你使用 SliverPersistentHeader 去固定住頭部的時候,作為 body 的列表是不知道頂部有個固定區(qū)域。 所以如果這時候不額外做一些處理,那么對于 body 而言,它的 paintOrigin 還是從最頂部開始而不是固定區(qū)域的下方。
如上動圖所示,可以看到 item0 并沒有在橙色區(qū)域停止滑動,而是繼續(xù)往上滑動,這就是因?yàn)樽鳛?
body的列表不知道頂部有固定區(qū)域。
這時候就可以通過使用 SliverOverlapAbsorber + SliverOverlapInjector 的組合來解決這個問題:
在
SliverPersistentHeader的外層嵌套一個SliverOverlapAbsorber用于吸收SliverPersistentHeader的高度;使用
SliverOverlapInjector將這個高度配置到body列表中,讓列表知道頂部存在一個固定高度的區(qū)域;
這部分例子可見:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/sliver_list_demo_page.dart
好了,本篇關(guān)于 Flutter 滑動列表的實(shí)現(xiàn)原理就介紹完了,如果你還有什么想說的,歡迎留言討論。