實作 Flutter 多變有趣的滾動效果CustomScrollView!
這篇寫於 2022 年。工具和 API 變化很快,部分步驟可能已經不適用。
與眾不同的ScrollView列表
為什麼需要CustomScrollView?#
通常如果使用ListView放置在ListView裡會出錯,原因是在Layout時父ListView不能判斷子ListView的高度,但可以設置子ListView的 shrinkWrap = true 來解決,但動態計算會導致性能很差,不太推薦。
不過CustomScrollView可以解決此問題,它支援複雜列表的嵌套和滾動組合!
Sliver 佈局流程#
Viewport將目前的配置資訊通過SliverConstraints傳遞給SliverSliver根據Viewport的約束,確定自己的大小、位置、繪製等資訊,保存在SliverGeometry中,回傳給ViewportViewport讀取SliverGeometry中的資訊來對Sliver子原件進行安排和繪製
Viewport → SliverConstraints → Sliver → SliverGeometry → Viewport → Sliver
以下說明幾個重要角色
ViewPort#
- 一個顯示範圍、窗口,裡面包含多個Sliver元件
- 寬高是固定的,它內部元件的的總寬高是可以大於本身的
- 為了提高性能採用懶加載機制,它只會繪製可視區域的Widget
- 只繪製、顯示ScrollView中一部分內容來實現滾動效果,大大降低記憶體消耗
Sliver#
- 自定義滾動效果的滾動View,達到更準確的控制
- 使用
CustomScrollView搭配各種Sliver元件,為所有子Sliver提供一個共享的Scrollable,然後統一處理指定滑動方向的滑動事件,統一滾動效果 - 在 Viewport 中實現滾動效果的 RenderObject,使用
RenderSliver作為基礎類別 - 在佈局的過程,Sliver 獲取Viewport給予的
SliverConstraints並計算對應的SliverGeometry(描述 Sliver佔得空間大小與相關資訊) - CustomScrollView和ListView、GridView、PageView 一樣,都是完整的可滾動元件(同時擁有Viewport、Sliver、Scrollable)
SliverConstraints#
- 和RenderBox佈局使用BoxConstraints類似,Sliver佈局採用SliverConstraints作為約束
- 當列表滑動時,如果某個Sliver已經進入Viewport需要創建的區域,則列表會將SliverConstraints傳遞給該Sliver,Sliver就可以根據這些資訊來確定自身的繪製資訊
SliverGeometry
SliverGeometry#
- Sliver在滾動列表的空間繪製資訊
- 在佈局的最後Sliver通過回傳SliverGeometry給Viewport,計算出Sliver的位置,更新UI
常見的幾個Sliver元件,在開發時很常使用到,熟悉他們對我們很有幫助。
⚡︎SliverAppBar#
- 根據滾動偏移量改變高度
- 基於
SliverPersistentHeader的實現 expandedHeight:設置內容展開顯示的最大高度flexibleSpace:靈活展開的內容,以動畫效果顯示出來。最常見是顯示圖片,例如商品、食物等pinned:如果為true,會將AppBar固定在頂部,始終渲染一個固定在頂部的收起狀態AppBarfloating:如果為true,向下滾動時,AppBar會顯示,否則須回到頂端。而只有當AppBar展開時才會繼續滾動ListViewsnap:如果為true,向下滾動時,直接全展開,效果會比floating的速度還快,始終保持展開和收起的狀態。且只有在floating為true時才可以設置,才會有效
FlexibleSpaceBar的stretchModes,設置下拉的伸縮、伸展模式#
zoomBackground:縮放內容blurBackground:模糊內容- 注意:SliverAppBar的
stretch設置需為 true,才會有伸縮效果
⚡︎SliverPersistentHeader#
- 自定義效果的 header bar,實現sticky吸頂的效果
- 可以製作一個複雜的子標題,當向下滾動時它會展開,但在向上滾動時會保持最小高度,顯示基本內容
pinned:為 true 時,header會固定在可視區域的頂部。為 false 時,header可以滑出可視區域,當用戶再次向下滑動時,此時不管header 已經被滑出了多遠,它都會立即出現在可視區域頂部並固定住,直到繼續下滑到原來的位置時,header才會重新回到原來的位置(不再固定在頂部)floating:為 true 時,只要往下滑header就會根據偏移量延展,不需要回到頂部
SliverPersistentHeaderDelegate#
自定義的header元件需要實作 ,包含4個地方
minExtent:收起狀態下元件的高度maxExtent:展開狀態下元件的高度。如果我們想讓header高度固定,則將maxExtent和minExtent指定為同樣的值即可shouldRebuild():根據某些資料或條件改變來決定是否要更新build():創建渲染內容
build()
shrinkOffset:偏移量,目前位置到最大的距離,展開和收縮都會變動overlapsContent:是否在下拉、延伸時有覆蓋和重疊到其他Sliver
⚡︎SliverList#
- 清單列表
delegate#
SliverChildListDelegate:已知的多個元件,全部一起渲染SliverChildBuilderDelegate:按照資料來源來產生和渲染對應數量的子元件,提高性能(建議使用)
⚡︎SliverFixedExtentList#
- 每個item在主軸方向上具有固定的高度/寬度,通過
itemExtent設定 - 效率比SliverList高,因為不用通過item的layout過程就可以知道每個item的範圍
⚡︎SliverGrid#
- 網格清單
- 僅實例化那些通過ViewPort(滾動視圖)的可見child widgets,提升性能
gridDelegate#
網格參數,以任意二維排列對元件排版
SliverGridDelegateWithFixedCrossAxisCount:根據次軸元素的數量SliverGridDelegateWithMaxCrossAxisExtent:根據次軸元素的固定大小
delegate#
SliverChildBuilderDelegate:按照資料來源來產生和渲染對應數量的子元件,提高性能(建議使用)
⚡︎SliverFillRemaining#
- 填滿視圖上的剩餘空間,通常用於slivers的最後一個
hasScrollBody#
- 默認為true,決定child是否可滾動,根據拖拉的範圍來佔滿空間
- 設置為false,根據child來決定填滿空間的大小
⚡︎SliverFillViewport#
- 每一個Sliver元件都佔滿可視區域
- 對應的可滾動元件為 PageView
⚡︎SliverVisibility#
- 設置Sliver元件是否顯示
⚡︎SliverOpacity#
- 設置Sliver元件的透明度
⚡︎SliverPadding#
- 設置Sliver元件的Padding
- 注意:不要用它來包裹
SliverPersistentHeader,因為它會使pinned失效,可以在自定義的delegate內部使用Padding
⚡︎SliverToBoxAdapter#
- 包裹一般元件,並非所有元件都有Sliver版本
- 可以將RenderBox適配為RenderSliver
- 方便與CustomScrollView結合,產生精美的滾動體驗
範例#
其他文章#
- 如何在Flutter使用 Makefile 節省你的時間?
- Easily understand StatefulWidget LifeCycle of Flutter
- “freezed” makes model class strong and easily
- 提高Flutter性能的小技巧!(一)
- 提高Flutter性能的小技巧!(二)
- 提高Flutter性能的小技巧!(三)
- What are Async and Isolates in Flutter?
- LoadBalancer is optimization for Isolates in Flutter
- Flutter 3 有什麼新東西?
- Riverpod 輕鬆學,原來這麼好用!
- Riverpod 輕鬆學(二),一些進階用法!
關於我#
- Github: chyiiiiiiiiiiii
- Instagram: flutterluvr.yii
- Linkedin: yiichenhi
- Youtube: Yii
- Youtube: 一起饅頭(美食頻道)
- Email: ab20803@gmail.com
贊助#
謝謝你!花費時間看完,非常感謝。
如果覺得文章不錯的話可以贊助我,讓我有更多動力和熱情分享學習紀錄和生活!請我喝一杯咖啡吧~
最後#
希望有幫助到你/妳,歡迎追蹤我,方便瀏覽最新的文章,也請別忘記給我拍手哦 (最多可以達到50次)!
本文原刊登於 Medium。