🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

==================================================================

在最近發布的 Flutter 3.43.0-0.1.pre 這個 Beta 版本裡,官方在 Framework 層面對 ScrollView / Viewport / ShrinkWrappingViewport 做了一個比較有意思的修改:

  • 引入 ScrollCacheExtent,廢棄 cacheExtent + cacheExtentStyle
  • 修復 RenderShrinkWrappingViewport 在無約束下 cacheExtent 可能變成 NaN 的問題
  • 重構 Viewport cache 計算路徑

這次修改涉及 rendering 層核心程式碼,屬於 Viewport 底層重構,暫時看來修改的影響是正向的,應該不至於引起類似之前《Flutter 3.41 iOS 鍵盤負優化:一個程式碼潔癖引發的負優化》的問題。

根據 #181092 的修改內容,這次修改範圍主要涉及:

rendering/viewport.dart
widgets/scroll_view.dart
widgets/page_view.dart
widgets/list_view.dart
widgets/grid_view.dart

對應原始碼的影響有:

RenderViewportBase
RenderViewport
RenderShrinkWrappingViewport
Viewport
ShrinkWrappingViewport
ScrollView
ListView
PageView

所以,雖然看起來只是個小 feature 和一個 bug fix,但其實這個調整並不是 Widget 層的小改動,而是 Viewport 渲染路徑修改

所以才會需要挑出來聊一聊。

首先是 ScrollCacheExtent,在之前的實作裡,Viewport 的 cache 主要由這兩個欄位控制:

double cacheExtent
CacheExtentStyle cacheExtentStyle

相關邏輯為:

switch (cacheExtentStyle) {
  case CacheExtentStyle.pixel:
    calculatedCacheExtent = cacheExtent;
  case CacheExtentStyle.viewport:
    calculatedCacheExtent = mainAxisExtent * cacheExtent;
}

涉及的關鍵變數是:

mainAxisExtent = viewport size

而問題就出現在這裡,因為 ShrinkWrappingViewport 的特殊性,當 ScrollView 設定 shrinkWrap = true 的時候,ScrollView.buildViewport 就會建立 ShrinkWrappingViewport

ScrollView.buildViewport
 -> ShrinkWrappingViewport
 -> RenderShrinkWrappingViewport

ShrinkWrappingViewport 的特點就是 viewport size 由子節點決定,而不是由父約束,這就意味著 mainAxisExtent 可能不是一個有限的值,也就是類似以下場景:

SingleChildScrollView
  -> ListView(shrinkWrap: true)

Column
  -> ListView(shrinkWrap: true)

這些情況下父佈局在主軸方向是 unbounded,所以 ShrinkWrappingViewport 會得到 constraints.maxExtent = infinity 的情況,也就是最終:

mainAxisExtent = infinity

這乍看沒什麼問題,但舊的 cacheExtent 邏輯沒有考慮到這種情況,因為在舊邏輯裡:

viewport cache mode = cacheExtentStyle.viewport

也就是

calculatedCacheExtent = mainAxisExtent * cacheExtent

如果這時候 mainAxisExtent = infinity,那就會 infinity * 0.5 = infinity,以致於在後續佈局計算裡 paintExtent / layoutOffset / scrollOffset 都可能出現 infinity - infinity,也就是結果為 NaN。例如:

SingleChildScrollView(
  child: ListView.builder(
    shrinkWrap: true,
    cacheExtent: 0.5,
    cacheExtentStyle: CacheExtentStyle.viewport,
    itemBuilder: ...
  ),
)

而在新的 API 下,cacheExtentcacheExtentStyle 現在合併成 ScrollCacheExtent,並且內部做了適配,所以這種情況現在不會再報錯:

SingleChildScrollView(
  child: ListView.builder(
    shrinkWrap: true,
    scrollCacheExtent: ScrollCacheExtent.viewport(0.5),
  ),
)

所以這裡的 ScrollCacheExtent 不是簡單地把兩個參數合成一個,而是在內部做了重構,首先是在 viewport.dart 內部提供了:

ScrollCacheExtent.pixels()
ScrollCacheExtent.viewport()

對應內部實作了新的 Viewport 計算邏輯:

_calculateCacheOffset(mainAxisExtent)

_calculatedCacheExtent =
  _scrollCacheExtent._calculateCacheOffset(mainAxisExtent)

這個情況下 cache 統一由一個位置計算,並且避免 style + value 分離。其中「NaN 修復」的關鍵在於 RenderShrinkWrappingViewport,對應的核心修改為:

if (!mainAxisExtent.isFinite)
  cacheExtent = 0

因為對於 infinite viewport 來說,實際上已經會建立所有子項(already builds all children),所以根本不需要 cache,而這個修改也會影響 PageView / ListView / GridView / CustomScrollView 等常用控制項。

所以這也是一個相對昂貴的效能配置選項。

所以這個 ScrollCacheExtent 的修改,本質上是:

  • 重構 Viewport cache API
  • 修復 ShrinkWrappingViewport 在無約束下 cacheExtent 計算出現 NaN 的問題
  • 統一 ScrollView / Viewport / RenderViewport 的快取邏輯

雖然邏輯改動看起來好像不多,但涉及的檔案和地方還是蠻多的,從長遠來看,這個修改還是比較有意義的,至少之前經常遇到的 NaN 問題終於不用自己處理了。

連結


原文出處:https://juejin.cn/post/7618098747672051766


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬9   ❤️1
429
🥈
我愛JS
📝2   💬9   ❤️1
75
🥉
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付