RecyclerView 里的自定義 LayoutManager 的一種設(shè)計(jì)與實(shí)現(xiàn)

原文鏈接:http://pingguohe.net/2018/02/06/layouthelper-of-vlayout.html

很久很久以前,我分享過(guò)一篇文章,介紹了團(tuán)隊(duì)推出的一種異構(gòu)的自定義 LayoutManger 的實(shí)現(xiàn),它是基于 LinearLayoutManager 擴(kuò)展實(shí)現(xiàn)的,這個(gè)項(xiàng)目的名字叫 vlayout,也許你以前聽(tīng)說(shuō)過(guò),或者在 github 上看到過(guò),雖然還存在不少 bug 和不足,但能得到不少同學(xué)的支持,真是感到欣慰。

image

關(guān)于它的設(shè)計(jì)思路,其實(shí)在文章《Tangram 的基礎(chǔ) —— vlayout》里已經(jīng)有過(guò)一些介紹,還有一些關(guān)于它的使用、功能介紹:vlayout使用說(shuō)明(一)、vlayout使用說(shuō)明(二)。其實(shí)它很多細(xì)節(jié)可以展開(kāi)介紹,其中可能涉及到 RecyclerView 自身的源碼解讀之類(lèi)的。這里我想分享 vlayout 里其中一種 LayoutHelperLayoutHelper 負(fù)責(zé)具體的布局邏輯,是 vlayout 里抽象出的一個(gè)層次,可以參考前文鏈接詳細(xì)了解)的設(shè)計(jì)與實(shí)現(xiàn)。

說(shuō)到這里,這篇文章的標(biāo)題其實(shí)應(yīng)該叫做:vlayout 里一種自定義 LayoutHelper 的設(shè)計(jì)與實(shí)現(xiàn),考慮到可能有讀者不明白,所以用『自定義 LayoutManager 的一種設(shè)計(jì)與實(shí)現(xiàn)』代替了一下。

好,下面開(kāi)始進(jìn)入主題。

需求場(chǎng)景

在 vlayout 里,提供了多種類(lèi)型的 LayoutHelper 來(lái)負(fù)責(zé)布局邏輯,將不同類(lèi)型的 LayoutHelper 組合到一個(gè) RecyclerView 里,實(shí)現(xiàn)了在同一個(gè)頁(yè)面異構(gòu)的、扁平化的布局能力。在考慮到一種布局結(jié)構(gòu)需要對(duì)應(yīng)實(shí)現(xiàn)一個(gè) LayoutHelper 的時(shí)候,總是要考慮到將 item 扁平化地布局,這樣才能最大程度發(fā)揮 RecyclerView 的回收復(fù)用能力。

現(xiàn)在如果有這樣一種需求場(chǎng)景:在組件 A 以?xún)闪胁季帜J降臄?shù)據(jù)里流,以 4 個(gè)一組為單位,插入一塊其他布局類(lèi)型的組件,比如說(shuō)是 3 列布局的組件 B。按照原先的做法,可能需要按照視覺(jué)樣式,將 4 個(gè)一組的組件 A,包裝到一個(gè) GridLayoutHelper 里,然后將中插的每一塊組件 B 區(qū)域,包裝到另一個(gè) GridLayoutHerlper 里,這兩種 GridLayoutHerlper 的主要區(qū)別在于列數(shù)不同。

image

這樣子做有一個(gè)小問(wèn)題在于,從產(chǎn)生數(shù)據(jù)列表到 UI 展示列表的鏈路里,總有一個(gè)環(huán)節(jié)需要按照視覺(jué)樣式來(lái)對(duì)數(shù)據(jù)進(jìn)行切割分組操作。將這種數(shù)據(jù)切割的操作暴露給業(yè)務(wù)方,總是讓人難受的,而且很容易出錯(cuò)。在更加復(fù)雜的業(yè)務(wù)場(chǎng)景下,數(shù)據(jù)來(lái)源方可能是多種多樣的,它只關(guān)心數(shù)據(jù)的吐出,而不是按照 UI 樣式或者某一特定框架的協(xié)議來(lái)轉(zhuǎn)換數(shù)據(jù)。

因此有必要側(cè)重在端上進(jìn)行設(shè)計(jì),如果進(jìn)一步考慮這個(gè)需求,可以將這種結(jié)構(gòu)描述成一種樹(shù)狀結(jié)構(gòu)。以上圖為例,也就說(shuō)處于根節(jié)點(diǎn)的的組件 A 列表,都是用 2 列結(jié)構(gòu)的 GridLayoutHelper 來(lái)布局的,而根節(jié)點(diǎn)的組件列表里某些位置,插入一個(gè)組件 B 的列表,它們是用 3 列結(jié)構(gòu)的 GridLayoutHelper 來(lái)布局的。這種描述可能有點(diǎn)抽象,以普通場(chǎng)景下、非 RecyclerView 里實(shí)現(xiàn)場(chǎng)景為例,也就是說(shuō)假如要寫(xiě)一個(gè)自定義布局來(lái)繪制上述界面,其實(shí)就是寫(xiě)一個(gè)能進(jìn)行 2 或 3 列布局的 ViewGroup,然后按照想要的結(jié)構(gòu)自由組織就行了,然后最終我們就能得到一個(gè) View 的樹(shù)。但是這種嵌套的結(jié)構(gòu) View 在 RecyclerView 只能作為一個(gè)整體來(lái)進(jìn)行回收復(fù)用,還不夠扁平化,回收復(fù)用的粒度就達(dá)不到我們的要求,所以就提出了上述的邏輯上具備嵌套能力的樹(shù)狀結(jié)構(gòu)。有了這樣的邏輯結(jié)構(gòu)來(lái)描述,就可以提供更加普適性的布局能力。解決這個(gè)問(wèn)題的 LayoutHelper 就是本文要介紹的內(nèi)容,它可以接收帶邏輯上帶嵌套結(jié)構(gòu)的數(shù)據(jù)描述,同時(shí)又在最終布局的時(shí)候?qū)⒚恳粋€(gè) item 組件扁平化地、直接地掛載到 RecyclerView 下。

image

實(shí)現(xiàn)思路與簡(jiǎn)介

有了描述布局的結(jié)構(gòu),接下來(lái)就是要按照設(shè)計(jì)來(lái)實(shí)現(xiàn)布局能力,如果是普通的自定義 ViewGroup,情況還比較容易,但是要結(jié)合到 RecyclerView 里,必須時(shí)時(shí)牢記扁平化實(shí)現(xiàn),在 vlayout 的場(chǎng)景里,就是要新建一種 LayoutHelper 來(lái)實(shí)現(xiàn)。
之前有做過(guò)幾次這樣的嘗試。第一種思路是像正常 View 層級(jí)一樣寫(xiě)一個(gè)大的自定義 ViewGroup 作為整體的一個(gè) RecyclerView 的組件,內(nèi)部在做回收復(fù)用的分發(fā)處理,這樣其實(shí)沒(méi)有做到真正的扁平化,而且需要維護(hù)內(nèi)部的子 View 布局高度消耗,以及與 RecyclerView 布局機(jī)制的協(xié)同,過(guò)程會(huì)比較麻煩,稍加嘗試之后放棄。

第二種方式是實(shí)現(xiàn)一種 LayoutHelper,讓它像系統(tǒng) View 一樣具備嵌套描述的能力。一開(kāi)始將它想象的比較復(fù)雜,可以按照任意層次結(jié)構(gòu)去嵌套、擺放,結(jié)果導(dǎo)致設(shè)計(jì)與實(shí)現(xiàn)都非常復(fù)雜。

嘗試了前兩種方案,實(shí)現(xiàn)成本和結(jié)果都不太理想,于是來(lái)重新審視最初的目標(biāo)。并做了以下幾點(diǎn)思考:1. 要在一定領(lǐng)域內(nèi)解決問(wèn)題,限定邊界,不能單純追求更大的靈活性而提升復(fù)雜度。2. 將問(wèn)題簡(jiǎn)化為行級(jí)布局,因?yàn)楸旧?vlayout 里每一種 LayoutHelper 都是按行來(lái)布局的,LayoutHelper 內(nèi)部每一次布局都是填滿(mǎn)一整行的空間,而不同 LayoutHelper 之間也都是按行劃分的,不會(huì)出現(xiàn)同一行內(nèi)兩個(gè)不同的 LayoutHelper 混搭。

于是,基于前面第二種方案進(jìn)行簡(jiǎn)化,還是實(shí)現(xiàn)一種自定義 LayoutHelper,在它引入了一種叫 RangeStyle 的結(jié)構(gòu)來(lái)描述每一塊區(qū)域的相對(duì)父節(jié)點(diǎn)起始位置以及它的樣式,RangeStyle 可以按照設(shè)計(jì)上的邏輯嵌套結(jié)構(gòu)來(lái)嵌套描述。這樣最初設(shè)計(jì)上的邏輯樹(shù)狀結(jié)構(gòu)就有了實(shí)體來(lái)承載。而在布局的時(shí)候,自定義 LayoutHelper 會(huì)獲取到當(dāng)前將要布局的 position,通過(guò)這個(gè) position 來(lái)它所對(duì)應(yīng)的 RangeStyle 節(jié)點(diǎn)信息,通過(guò)它提供的樣式,比如 margin、padding、spanCount 等來(lái)控制當(dāng)前 LayoutHelper 的行為。這樣每次布局的組件就像在其他 LayoutHelepr 里的一樣是直接掛載到 RecyclerView 下的,也達(dá)到了嵌套的描述、扁平化的實(shí)現(xiàn)的預(yù)設(shè)目標(biāo)。

基于這樣的思路,思考起來(lái)就非常清晰,與整體的 vlayout 設(shè)計(jì)本身就契合的非常好,實(shí)現(xiàn)起來(lái)也比較順利。當(dāng)然實(shí)現(xiàn)起來(lái)還是有一些細(xì)節(jié)要調(diào)測(cè),比如計(jì)算整體的 margin、padding 需要累加 RangeStyle 樹(shù)里節(jié)點(diǎn)下的相同位置的邊距;每一塊區(qū)域的背景色也要像真的一層嵌套結(jié)構(gòu)那樣按照預(yù)期的層級(jí)堆疊排放。

我將它稱(chēng)之為 RangeGridLayoutHelper,主要是目因?yàn)榍爸С钟脕?lái)做這種嵌套的流式布局的實(shí)現(xiàn)。它的詳細(xì)源碼可以參考:RangeGridLayoutHelper。

如果直接使用 vlayout,RangeGridLayoutHelper 的使用代碼看起來(lái)可能是這樣的:

RangeGridLayoutHelper layoutHelper = new RangeGridLayoutHelper(4);
layoutHelper.setBgColor(Color.GREEN);
layoutHelper.setWeights(new float[]{20f, 26.665f});
layoutHelper.setPadding(15, 15, 15, 15);
layoutHelper.setMargin(15, 15, 15, 15);
layoutHelper.setHGap(10);
layoutHelper.setVGap(10);
GridRangeStyle rangeStyle = new GridRangeStyle();
rangeStyle.setBgColor(Color.RED);
rangeStyle.setSpanCount(2);
rangeStyle.setWeights(new float[]{46.665f});
rangeStyle.setPadding(15, 15, 15, 15);
rangeStyle.setMargin(15, 15, 15, 15);
rangeStyle.setHGap(5);
rangeStyle.setVGap(5);
layoutHelper.addRangeStyle(4, 7, rangeStyle);
GridRangeStyle rangeStyle1 = new GridRangeStyle();
rangeStyle1.setBgColor(Color.YELLOW);
rangeStyle1.setSpanCount(2);
rangeStyle1.setWeights(new float[]{46.665f});
rangeStyle1.setPadding(15, 15, 15, 15);
rangeStyle1.setMargin(15, 15, 15, 15);
rangeStyle1.setHGap(5);
rangeStyle1.setVGap(5);
layoutHelper.addRangeStyle(8, 11, rangeStyle1);
adapters.add(new SubAdapter(this, layoutHelper, 16));

最佳實(shí)踐

vlayout 雖然提供了異構(gòu)布局的能力,但是我也承認(rèn),目前是接口(主要是 DelegateAdapter 以及各種 LayoutHelper 提供的接口)并不易用,開(kāi)發(fā)者很難拋開(kāi)那些具體的細(xì)節(jié)然后快速寫(xiě)出頁(yè)面,在 Github 上也有同學(xué)反饋過(guò)這個(gè)問(wèn)題。之所以這樣其實(shí)是因?yàn)椋何覀儓F(tuán)隊(duì)自己也并不是直接使用 vlayout 進(jìn)行開(kāi)發(fā),而是通過(guò) Tangram 庫(kù)來(lái)間接使用 vlayout,在 Tangram 主要是通過(guò) JSON 數(shù)據(jù)來(lái)描述整體頁(yè)面的結(jié)構(gòu),并封裝了一個(gè)自定義的 Adater,它接收 Tangram 協(xié)議 JSON 數(shù)據(jù),來(lái)自動(dòng)創(chuàng)建、維護(hù)各種 LayoutHelper 的內(nèi)部信息,這樣就屏蔽了 vlayout 這些復(fù)雜的細(xì)節(jié),而不是在使用 DelegateAdapter 的時(shí)候手動(dòng)維護(hù)各個(gè) LayoutHelper。建議到 Tangram 工程下進(jìn)一步了解詳細(xì)信息,對(duì)于原來(lái)使用 vlayout 開(kāi)發(fā)的 app 來(lái)說(shuō),理論上都可以遷移到 Tangram 架構(gòu),這樣整個(gè)頁(yè)面的渲染就可以由數(shù)據(jù)來(lái)驅(qū)動(dòng),提升頁(yè)面的動(dòng)態(tài)性。

image

那么說(shuō)到動(dòng)態(tài)性,Tangram 解決了頁(yè)面結(jié)構(gòu)的問(wèn)題,至于每一個(gè) RecyclerView 里的 item,也可以稱(chēng)之為組件,它的動(dòng)態(tài)性,我們有另外一個(gè)方案—— VirtualView,它是通過(guò)自定義 XML 來(lái)描述組件的布局結(jié)構(gòu),然后由自定義引擎解析 XML 數(shù)據(jù)并渲染出界面的方案。就好比在 Android 里寫(xiě) XML 布局文件然后渲染展示,當(dāng)動(dòng)態(tài)下發(fā) XML 數(shù)據(jù)的時(shí)候,組件樣式也就能動(dòng)態(tài)更新了。有興趣的也可以進(jìn)一步了解一下:

有了這兩件利器,當(dāng)下一次 PD 跑過(guò)來(lái)問(wèn)你線(xiàn)上 XXX 能不能調(diào)整一下樣式結(jié)構(gòu)的時(shí)候,你就可以回答說(shuō)『可以』,而不是等到下一次發(fā)版。而且我們的重點(diǎn)功能、日常迭代,也主要是圍繞 Tangram + VirtualView 來(lái)進(jìn)行,這樣可以更快用上最新特性。

更多關(guān)于 RecyclerView 的資料

最后,想說(shuō)一點(diǎn)的是,整個(gè) RecyclerView 體系的設(shè)計(jì)雖然非常強(qiáng)大、擴(kuò)展性更好,但對(duì)于使用方來(lái)說(shuō),想要擴(kuò)展一個(gè)自定義的 LayoutManager 還是比較麻煩的,這要求開(kāi)發(fā)者深入理解 RecyclerView 體系的設(shè)計(jì)及原理,這里收集了部分之前閱讀過(guò)的資料,對(duì)于大家深入理解 RecyclerView 或者 vlayout 都有好處:

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

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

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