Flutter瀑布流及通用列表解决方案
作者:閑魚技術(shù)-夜瀾
背景
目前閑魚業(yè)務(wù)中無論是首頁還是搜索頁都有大量可以落地瀑布流的場景,而在Flutter原生中只提供了ListView, GridView,無法提供自定義布局的能力。
而在社區(qū)中,一般瀑布流的解決方案都是基于SliverMultiBoxAdaptor對其performLayout進行定制,主要存在的問題是缺乏復(fù)用機制,并且在很多情形下容易出現(xiàn)重復(fù)布局,在線上業(yè)務(wù)的復(fù)雜場景下容易出現(xiàn)幀數(shù)偏低的問題, 閃屏的問題。同時對于Child生命周期,打點曝光等一系列基礎(chǔ)功能的支持還是一片空白的狀態(tài)。
所以,我們迫切需要一個更為通用的可以解決復(fù)雜布局過程同時能夠?qū)A(chǔ)能力進行擴充的列表視圖解決方案。
Flutter中的列表視圖簡介
1. Scrollable
Scrollable是一個StatefulWidget, 職責是監(jiān)聽用戶的手勢輸入。其State的build方法會返回一個含有Listener和RawGestureDetector的ViewportScrollPosition用于描述其位置信息,并在其內(nèi)部定義了 onStart, onUpdate, onEnd等回調(diào)。Scrollable中的每一次滑動的開始到結(jié)束都對應(yīng)于一個Darg對象,并且會發(fā)送滑動的通知。而Viewport則負責對通知進行監(jiān)聽。
2. Sliver
Flutter有兩種布局體系 Box, Sliver。在layout的過程中,每個Sliver 都接收 SliverConstraints 計算返回一個 SliverGeometry,可以類比于RenderBox 接收 BoxConstraints 返回一個 Size。Sliver由Viewport統(tǒng)一來負責進行管理。
3. Viewport
A widget that is bigger on the inside.
Viewport持有一個或多個Sliver。Scrollable將offset傳遞給Viewport, 由Viewport決定哪些Sliver應(yīng)該是Visible。Viewport本質(zhì)上是一個MultiChildRenderObjectWidget,也就是整個滾動視圖的主要渲染邏輯都在Viewport中完成。
而在performLayout中,_attemptLayout會以center為中心,先布局leading方向的child,再布局trailing方向的child。其中只有dirty的child會被布局。
do {correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);if (correction != 0.0) {offset.correctBy(correction);} else {if (offset.applyContentDimensions(math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),))break;}count += 1; } while (count < _maxLayoutCycles);如果_attemptLayout返回了一個非0的correction, 就會打斷當前布局的過程,需要對offset進行調(diào)整后重新開始布局,最多只能連續(xù)打斷10次(_maxLayoutCycles)。
correction用于調(diào)整,舉個🌰,比如targetScrollOffset很遠,而在scroll的過程中child用完了,就需要讓Sliver通知Viewport, 同時進行修正。但是Flutter并不是通過不斷對child進行l(wèi)ayout來改變child位置實現(xiàn)的滑動效果,這樣的重繪過程顯然效率太低,顯然RenderObject不需要被改變,是可以復(fù)用的。但是布局一般只發(fā)生在添加新child的過程中,而滑動效果則發(fā)生在paint過程中。
void _paintWithContext(PaintingContext context, Offset offset) {// 重新布局就不需要調(diào)整offset了.if (_needsLayout)return;_needsPaint = false;paint(context, offset); }Viewport通過PaintingContext間接持有Canvas進行繪制。Offset指笛卡爾坐標系下的坐標,與Axis方向無關(guān)。繪制時只需改變對應(yīng)RenderObject的Offset即可實現(xiàn)滾動的效果, 這樣就不必重新創(chuàng)建RenderObject。所以我們?nèi)绻雽崿F(xiàn)性能較高的列表視圖,就要嘗試去減少重新布局Child。在對Flutter的列表布局有了基本了解后,我們再來看瀑布流的實現(xiàn)過程。
瀑布流的實現(xiàn)邏輯
WatetfallFlow的布局過程中需要指定Child的Offset,然后對其進行布局。所以需要繼承SliverMultiBoxAtaptor,依賴于其將SliverConstraints轉(zhuǎn)換為BoxConstraints的能力。我們也可以使用其SliverBoxChildManager, 方便控制Child的懶加載過程。
核心邏輯
在瀑布流中由于同一行(列)的child(大多)具有先后關(guān)系,需要按照順序來進行布局,所以瀑布流相比于GridView更類似于ListView,而瀑布流的布局過程也借鑒了ListView。整個瀑布流的布局邏輯圍繞三個核心展開:
其中核心的數(shù)據(jù)結(jié)構(gòu)是ParentData.
ParentData位于Child中,Child將其傳遞給Sliver,Sliver又將其傳遞至上層,其中儲存了全部的布局信息(在笛卡爾坐標系下)。在performLayout中,child在調(diào)用layout時所使用的布局信息就來自ParentData。在Child的添加過程中,用一個Manager存儲前后邊緣所有Child的ParentData,在添加時尋找邊緣最靠近可見區(qū)域的Child,對其ParentData進行設(shè)置并替換當前Child.
布局的核心邏輯是對從最開始的Child(對應(yīng)firstIndex)到最末的Child(對應(yīng)targetLastIndex)進行布局。如果_layoutedChilds中已經(jīng)有記錄,則跳過其布局過程。
for (int index = firstIndex; index <= targetLastIndex; ++index) {final SliverGeometry gridGeometry = layout.getGeometryForChildIndex(index);final BoxConstraints childConstraints = gridGeometry.getBoxConstraints(constraints);RenderBox child = childAfter(trailingChildWithLayout);if (child == null || indexOf(child) != index) {// 重新獲取Child.child = _createAndLayoutChildIfNeeded(childConstraints, after: trailingChildWithLayout);if (child != null && indexOf(child) == index) {_layoutedChilds.add(index);}else if (child == null) {// Child已經(jīng)用盡.break;}} else {if (!_layoutedChilds.contains(index)) {_layoutChildIfNeeded(child, parentUsesSize: true);_layoutedChilds.add(index);}}trailingChildWithLayout = child; }對離開視圖的child進行GC,同時記得將數(shù)組中的child清除.
if (firstChild != null) {// 上一次的最先最末Child.final int oldFirstIndex = indexOf(firstChild);final int oldLastIndex = indexOf(lastChild);// 前后需要GC的child數(shù)量final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);// GCcollectGarbage(leadingGarbage, trailingGarbage);_layoutedChilds.sort();_layoutedChilds.removeRange(0, leadingGarbage);_layoutedChilds.removeRange(layoutedChilds.length - 1 - trailingGarbage, layoutedChilds.length - 1); } else {collectGarbage(0, 0); }在開發(fā)過程中出現(xiàn)了幀數(shù)偏低的問題,發(fā)現(xiàn)是Child在performLayout的過程中會出現(xiàn)重復(fù)布局。解決方法是我們不僅記錄leading, trailing邊緣的child。而且用對已經(jīng)layout過的child進行記錄,粗暴直接但是有效,這樣做也可以提供單獨update單個child的Layout能力。在更新Child的布局時也只需從記錄中將對應(yīng)child移除。
相比于原生視圖,我們可以通過獲取所有Child的ParentData信息,可以為上層接口提供實時并且有效的回調(diào).。這樣就可以根據(jù)每個Child的實時位置來提供生命周期,曝光打點的能力。所以可以對每個child的坐標進行監(jiān)聽,從而獲得精準的曝光信息。
從瀑布流到容器
在瀑布流的開發(fā)過程中也暴露出了一些設(shè)計上的問題。
比如瀑布流的具體渲染邏輯都在RenderObject中進行,太過底層顯然是不利于業(yè)務(wù)方根據(jù)業(yè)務(wù)進行定制。
又比如由于沒有復(fù)用的機制,在視圖層級較為復(fù)雜時幀數(shù)會由于重復(fù)渲染而不可避免的降低。
借鑒native思路重新設(shè)計后將整體容器分為3個部分進行設(shè)計。
主要管理child生命周期并響應(yīng)手勢,由于我們可以得到每個可見Child的parentData屬性,所以可在滾動時進行實時的通知。從而對每個Child的位置監(jiān)聽,從開始創(chuàng)建進入緩沖區(qū),到從緩沖區(qū)進入可見區(qū)域。手勢則來自于頂層的Scrollable。
主要負責布局所有的Child。將具體的布局邏輯抽離出,類似于iOS中的UICollectionViewLayout。但是在開發(fā)過程中也出現(xiàn)了一些問題,原因主要來自于Flutter特殊的信息傳遞方式,就是我們不能采用native的方式一次性計算出所有child的布局。因為RenderBox需要接收一個BoxConstraints才能返回一個size。
reuser則在RenderObject層面,對Child進行基于類型的復(fù)用并實現(xiàn)局部更新的操作。需要將SliverMultiBoxAdaptor和其Element拷貝一份進行重寫,改變其mount的邏輯,方案還在探索和調(diào)研之中,希望能在后續(xù)的文章中和大家見面!
性能數(shù)據(jù)
應(yīng)用于主搜索頁進行自動化測試,先前在54.7幀左右,換用瀑布流后為56.2,大概提升了1.5幀。
內(nèi)存上則有略微的升高情況。
展望
目前Flutter的列表視圖中仍然有很多問題需要處理,比如瀑布流中scrollTo(int index)的能力還無法實現(xiàn),內(nèi)存的使用情況等和原生相比仍然有不小的差距, 對于Flutter側(cè)的復(fù)用的穩(wěn)定性和兼容性上還存在問題,閑魚在Flutter化上還有很多路要走。
PS0: 文中代碼基于Flutter 1.12.13。
PS1: 文中譬如Viewport,既代指Widget本身, 又代指其對應(yīng)的RenderObject。
PS2: 文中涉及到的代碼經(jīng)過刪改, 僅供參考。
原文鏈接:https://developer.aliyun.com/article/767965?
版權(quán)聲明:本文中所有內(nèi)容均屬于阿里云開發(fā)者社區(qū)所有,任何媒體、網(wǎng)站或個人未經(jīng)阿里云開發(fā)者社區(qū)協(xié)議授權(quán)不得轉(zhuǎn)載、鏈接、轉(zhuǎn)貼或以其他方式復(fù)制發(fā)布/發(fā)表。申請授權(quán)請郵件developerteam@list.alibaba-inc.com,已獲得阿里云開發(fā)者社區(qū)協(xié)議授權(quán)的媒體、網(wǎng)站,在轉(zhuǎn)載使用時必須注明"稿件來源:阿里云開發(fā)者社區(qū),原文作者姓名",違者本社區(qū)將依法追究責任。 如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,歡迎發(fā)送郵件至:developer2020@service.aliyun.com 進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。總結(jié)
以上是生活随笔為你收集整理的Flutter瀑布流及通用列表解决方案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 让小程序在自有App中启动的技术来了:m
- 下一篇: 黑客与宕机