很久很久以前,我分享過一篇文章,介紹了團隊推出的一種異構的自定義 LayoutManger
的實現,它是基於 LinearLayoutManager
擴展實現的,這個項目的名字叫 vlayout,也許你以前聽說過,或者在 github 上看到過,雖然還存在不少 bug 和不足,但能得到不少同學的支持,真是感到欣慰。
關於它的設計思路,其實在文章《Tangram 的基礎 —— vlayout》裏已經有過一些介紹,還有一些關於它的使用、功能介紹:vlayout使用說明(一)、vlayout使用說明(二)。其實它很多細節可以展開介紹,其中可能涉及到 RecyclerView
自身的源碼解讀之類的。這裏我想分享 vlayout 裏其中一種 LayoutHelper
(LayoutHelper
負責具體的佈局邏輯,是 vlayout 裏抽象出的一個層次,可以參考前文鏈接詳細瞭解)的設計與實現。
說到這裏,這篇文章的標題其實應該叫做:vlayout 裏一種自定義 LayoutHelper 的設計與實現,考慮到可能有讀者不明白,所以用『自定義 LayoutManager 的一種設計與實現』代替了一下。
好,下面開始進入主題。
需求場景
在 vlayout 裏,提供了多種類型的 LayoutHelper
來負責佈局邏輯,將不同類型的 LayoutHelper
組合到一個 RecyclerView
裏,實現了在同一個頁面異構的、扁平化的佈局能力。在考慮到一種佈局結構需要對應實現一個 LayoutHelper
的時候,總是要考慮到將 item 扁平化地佈局,這樣才能最大程度發揮 RecyclerView
的回收複用能力。
現在如果有這樣一種需求場景:在組件 A 以兩列布局模式的數據裏流,以 4 個一組爲單位,插入一塊其他佈局類型的組件,比如說是 3 列布局的組件 B。按照原先的做法,可能需要按照視覺樣式,將 4 個一組的組件 A,包裝到一個 GridLayoutHelper
裏,然後將中插的每一塊組件 B 區域,包裝到另一個 GridLayoutHerlper
裏,這兩種 GridLayoutHerlper
的主要區別在於列數不同。
這樣子做有一個小問題在於,從產生數據列表到 UI 展示列表的鏈路裏,總有一個環節需要按照視覺樣式來對數據進行切割分組操作。將這種數據切割的操作暴露給業務方,總是讓人難受的,而且很容易出錯。在更加複雜的業務場景下,數據來源方可能是多種多樣的,它只關心數據的吐出,而不是按照 UI 樣式或者某一特定框架的協議來轉換數據。
因此有必要側重在端上進行設計,如果進一步考慮這個需求,可以將這種結構描述成一種樹狀結構。以上圖爲例,也就說處於根節點的的組件 A 列表,都是用 2 列結構的 GridLayoutHelper
來佈局的,而根節點的組件列表裏某些位置,插入一個組件 B 的列表,它們是用 3 列結構的 GridLayoutHelper
來佈局的。這種描述可能有點抽象,以普通場景下、非 RecyclerView
裏實現場景爲例,也就是說假如要寫一個自定義佈局來繪製上述界面,其實就是寫一個能進行 2 或 3 列布局的 ViewGroup,然後按照想要的結構自由組織就行了,然後最終我們就能得到一個 View 的樹。但是這種嵌套的結構 View 在 RecyclerView
只能作爲一個整體來進行回收複用,還不夠扁平化,回收複用的粒度就達不到我們的要求,所以就提出了上述的邏輯上具備嵌套能力的樹狀結構。有了這樣的邏輯結構來描述,就可以提供更加普適性的佈局能力。解決這個問題的 LayoutHelper
就是本文要介紹的內容,它可以接收帶邏輯上帶嵌套結構的數據描述,同時又在最終佈局的時候將每一個 item 組件扁平化地、直接地掛載到 RecyclerView
下。
image
實現思路與簡介
有了描述佈局的結構,接下來就是要按照設計來實現佈局能力,如果是普通的自定義 ViewGroup
,情況還比較容易,但是要結合到 RecyclerView
裏,必須時時牢記扁平化實現,在 vlayout 的場景裏,就是要新建一種 LayoutHelper
來實現。
之前有做過幾次這樣的嘗試。第一種思路是像正常 View 層級一樣寫一個大的自定義 ViewGroup
作爲整體的一個 RecyclerView
的組件,內部在做回收複用的分發處理,這樣其實沒有做到真正的扁平化,而且需要維護內部的子 View
佈局高度消耗,以及與 RecyclerView
佈局機制的協同,過程會比較麻煩,稍加嘗試之後放棄。
第二種方式是實現一種 LayoutHelper
,讓它像系統 View
一樣具備嵌套描述的能力。一開始將它想象的比較複雜,可以按照任意層次結構去嵌套、擺放,結果導致設計與實現都非常複雜。
嘗試了前兩種方案,實現成本和結果都不太理想,於是來重新審視最初的目標。並做了以下幾點思考:1. 要在一定領域內解決問題,限定邊界,不能單純追求更大的靈活性而提升複雜度。2. 將問題簡化爲行級佈局,因爲本身 vlayout 裏每一種 LayoutHelper
都是按行來佈局的,LayoutHelper
內部每一次佈局都是填滿一整行的空間,而不同 LayoutHelper
之間也都是按行劃分的,不會出現同一行內兩個不同的 LayoutHelper
混搭。
於是,基於前面第二種方案進行簡化,還是實現一種自定義 LayoutHelper
,在它引入了一種叫 RangeStyle
的結構來描述每一塊區域的相對父節點起始位置以及它的樣式,RangeStyle
可以按照設計上的邏輯嵌套結構來嵌套描述。這樣最初設計上的邏輯樹狀結構就有了實體來承載。而在佈局的時候,自定義 LayoutHelper
會獲取到當前將要佈局的 position,通過這個 position 來它所對應的 RangeStyle
節點信息,通過它提供的樣式,比如 margin、padding、spanCount 等來控制當前 LayoutHelper
的行爲。這樣每次佈局的組件就像在其他 LayoutHelepr
裏的一樣是直接掛載到 RecyclerView
下的,也達到了嵌套的描述、扁平化的實現的預設目標。
基於這樣的思路,思考起來就非常清晰,與整體的 vlayout 設計本身就契合的非常好,實現起來也比較順利。當然實現起來還是有一些細節要調測,比如計算整體的 margin、padding 需要累加 RangeStyle
樹裏節點下的相同位置的邊距;每一塊區域的背景色也要像真的一層嵌套結構那樣按照預期的層級堆疊排放。
我將它稱之爲 RangeGridLayoutHelper
,主要是目因爲前支持用來做這種嵌套的流式佈局的實現。它的詳細源碼可以參考:RangeGridLayoutHelper。
如果直接使用 vlayout,RangeGridLayoutHelper
的使用代碼看起來可能是這樣的:
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));
最佳實踐
vlayout 雖然提供了異構佈局的能力,但是我也承認,目前是接口(主要是 DelegateAdapter
以及各種 LayoutHelper
提供的接口)並不易用,開發者很難拋開那些具體的細節然後快速寫出頁面,在 Github 上也有同學反饋過這個問題。之所以這樣其實是因爲:我們團隊自己也並不是直接使用 vlayout 進行開發,而是通過 Tangram 庫來間接使用 vlayout,在 Tangram 主要是通過 JSON 數據來描述整體頁面的結構,並封裝了一個自定義的 Adater,它接收 Tangram 協議 JSON 數據,來自動創建、維護各種 LayoutHelper
的內部信息,這樣就屏蔽了 vlayout 這些複雜的細節,而不是在使用 DelegateAdapter
的時候手動維護各個 LayoutHelper
。建議到 Tangram 工程下進一步瞭解詳細信息,對於原來使用 vlayout 開發的 app 來說,理論上都可以遷移到 Tangram 架構,這樣整個頁面的渲染就可以由數據來驅動,提升頁面的動態性。
image
那麼說到動態性,Tangram 解決了頁面結構的問題,至於每一個 RecyclerView
裏的 item,也可以稱之爲組件,它的動態性,我們有另外一個方案—— VirtualView,它是通過自定義 XML 來描述組件的佈局結構,然後由自定義引擎解析 XML 數據並渲染出界面的方案。就好比在 Android 裏寫 XML 佈局文件然後渲染展示,當動態下發 XML 數據的時候,組件樣式也就能動態更新了。有興趣的也可以進一步瞭解一下:
有了這兩件利器,當下一次 PD 跑過來問你線上 XXX 能不能調整一下樣式結構的時候,你就可以回答說『可以』,而不是等到下一次發版。而且我們的重點功能、日常迭代,也主要是圍繞 Tangram + VirtualView 來進行,這樣可以更快用上最新特性。
更多關於 RecyclerView
的資料
最後,想說一點的是,整個 RecyclerView
體系的設計雖然非常強大、擴展性更好,但對於使用方來說,想要擴展一個自定義的 LayoutManager
還是比較麻煩的,這要求開發者深入理解 RecyclerView
體系的設計及原理,這裏收集了部分之前閱讀過的資料,對於大家深入理解 RecyclerView
或者 vlayout 都有好處:
- RecyclerView Animations and Behind the Scenes (Android Dev Summit 2015)
- RecyclerView ins and outs - Google I/O 2016
- Yigit Boyar: Pro RecyclerView
- Droidcon NYC 2016 - Radical RecyclerView
- Android ListView與RecyclerView對比淺析--緩存機制
- RecyclerView剖析
- RecyclerView剖析——續一
- RecyclerView 源碼分析
- 談談RecyclerView的LayoutManager-LinearLayoutManager源碼分析
- 手摸手第二彈,可視化 RecyclerView 緩存機制