RecyclerView 裏的自定義 LayoutManager 的一種設計與實現

很久很久以前,我分享過一篇文章,介紹了團隊推出的一種異構的自定義 LayoutManger 的實現,它是基於 LinearLayoutManager 擴展實現的,這個項目的名字叫 vlayout,也許你以前聽說過,或者在 github 上看到過,雖然還存在不少 bug 和不足,但能得到不少同學的支持,真是感到欣慰。

 

關於它的設計思路,其實在文章《Tangram 的基礎 —— vlayout》裏已經有過一些介紹,還有一些關於它的使用、功能介紹:vlayout使用說明(一)vlayout使用說明(二)。其實它很多細節可以展開介紹,其中可能涉及到 RecyclerView 自身的源碼解讀之類的。這裏我想分享 vlayout 裏其中一種 LayoutHelperLayoutHelper 負責具體的佈局邏輯,是 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 都有好處:


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章