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

Longerian發表於2018-02-06

原文連結

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

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

關於它的設計思路,其實在文章《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 的主要區別在於列數不同。

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

這樣子做有一個小問題在於,從產生資料列表到 UI 展示列表的鏈路裡,總有一個環節需要按照視覺樣式來對資料進行切割分組操作。將這種資料切割的操作暴露給業務方,總是讓人難受的,而且很容易出錯。在更加複雜的業務場景下,資料來源方可能是多種多樣的,它只關心資料的吐出,而不是按照 UI 樣式或者某一特定框架的協議來轉換資料。

因此有必要側重在端上進行設計,如果進一步考慮這個需求,可以將這種結構描述成一種樹狀結構。以上圖為例,也就說處於根節點的的元件 A 列表,都是用 2 列結構的 GridLayoutHelper 來佈局的,而根節點的元件列表裡某些位置,插入一個元件 B 的列表,它們是用 3 列結構的 GridLayoutHelper 來佈局的。這種描述可能有點抽象,以普通場景下、非 RecyclerView 裡實現場景為例,也就是說假如要寫一個自定義佈局來繪製上述介面,其實就是寫一個能進行 2 或 3 列布局的 ViewGroup,然後按照想要的結構自由組織就行了,然後最終我們就能得到一個 View 的樹。但是這種巢狀的結構 View 在 RecyclerView 只能作為一個整體來進行回收複用,還不夠扁平化,回收複用的粒度就達不到我們的要求,所以就提出了上述的邏輯上具備巢狀能力的樹狀結構。有了這樣的邏輯結構來描述,就可以提供更加普適性的佈局能力。解決這個問題的 LayoutHelper 就是本文要介紹的內容,它可以接收帶邏輯上帶巢狀結構的資料描述,同時又在最終佈局的時候將每一個 item 元件扁平化地、直接地掛載到 RecyclerView 下。

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

實現思路與簡介

有了描述佈局的結構,接下來就是要按照設計來實現佈局能力,如果是普通的自定義 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 架構,這樣整個頁面的渲染就可以由資料來驅動,提升頁面的動態性。

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

那麼說到動態性,Tangram 解決了頁面結構的問題,至於每一個 RecyclerView 裡的 item,也可以稱之為元件,它的動態性,我們有另外一個方案—— VirtualView,它是通過自定義 XML 來描述元件的佈局結構,然後由自定義引擎解析 XML 資料並渲染出介面的方案。就好比在 Android 裡寫 XML 佈局檔案然後渲染展示,當動態下發 XML 資料的時候,元件樣式也就能動態更新了。有興趣的也可以進一步瞭解一下:

有了這兩件利器,當下一次 PD 跑過來問你線上 XXX 能不能調整一下樣式結構的時候,你就可以回答說『可以』,而不是等到下一次發版。而且我們的重點功能、日常迭代,也主要是圍繞 Tangram + VirtualView 來進行,這樣可以更快用上最新特性。

更多關於 RecyclerView 的資料

最後,想說一點的是,整個 RecyclerView 體系的設計雖然非常強大、擴充套件性更好,但對於使用方來說,想要擴充套件一個自定義的 LayoutManager 還是比較麻煩的,這要求開發者深入理解 RecyclerView 體系的設計及原理,這裡收集了部分之前閱讀過的資料,對於大家深入理解 RecyclerView 或者 vlayout 都有好處:

相關文章