Tangram 的基礎 —— vlayout(Android)

前言

vlayout 是手機天貓 Android 版內廣泛使用的一個基礎 UI 框架項目 提供了一個用於RecyclerView的自定義的LayoutManger,可以實現不同佈局格式的混排,目標是支撐客戶端native頁面的快速開發。它也是 Tangram 框架的基礎模塊,現已開源,歡迎移步到 github 上指教。

簡介

背景

Android中UI性能消耗主要來自於兩個方面:

  1. 佈局層次嵌套導致多重measure/layout
  2. View控件的創建和銷燬

除了從在實踐中注意消除嵌套佈局,Android官方也提供了ListView/GirdView/RecyclerView等基礎空間來處理View的回收與複用。

但很多時候我們都會碰到視覺需要在一個長列表下做多種類型的佈局來分配各種元素, 特別是電商業務各類首頁,頻道等頁面,元素結構複雜多樣。

這種時候實現的選擇有不用複用,直接用各個組件進行拼接,但這樣會損失性能;選擇一個主要的複用容器, 如ListView或者RecyclerView+LinearLayoutManager等,然後在其中使用嵌套等方式對其他的佈局方式進行處理,這樣一個是減少了複用的能力,另一個是如果需要嵌套無法兼容的佈局的時候,需要處理嵌套滑動的情況。

既然RecyclerView提供了基礎的回收複用功能,也支持LayoutManager的擴展,那麼能不能用一個LayoutManager就完成所有的佈局類型呢? 感覺的這是一個不錯的方向,目前在 github 上也能找到類似的項目,但是這些之前也埋有不少bug, 大部分都是因爲在一些特殊場景下和RecyclerView相關的其他的類一起使用時出現問題。 爲了避免掉入bug大坑,我們決定基於LinearLayoutManager來做改造。

特性

  1. 自定義了一個VirtualLayoutManager,它繼承自 LinearLayoutManager;引入了 LayoutHelper 的概念,它負責具體的佈局邏輯;VirtualLayoutManager管理了一系列LayoutHelper,將具體的佈局能力交給LayoutHelper來完成,每一種LayoutHelper提供一種佈局方式,框架內置提供了幾種常用的佈局類型,包括:網格佈局、線性佈局、瀑布流佈局、懸浮佈局、吸邊佈局等。這樣實現了混合佈局的能力,並且支持擴展外部,註冊新的LayoutHelper,實現特殊的佈局方式。
  2. 每一種LayoutHelper負責佈局一批組件範圍內的組件,不同組件範圍內的組件之間,如果類型相同,可以在滑動過程中回收複用。因此回收粒度比較細,且可以跨佈局類型複用。
  3. 提供了自定義的佈局樣式,可以滿足多樣化的佈局需求,比如每一個組件範圍內的佈局支持一個背景顏色、背景圖片;網格佈局裏,可以支持1列、2列、3列、4列、5列共5種樣式,每一列的寬度默認平均分配屏幕寬度,也可以指定按比例分配列寬。吸邊佈局支持吸到屏幕底部、屏幕頂部、屏幕左邊、屏幕右邊。這些都是系統默認的LayoutManager不支持的。

架構

整體的設計方案和思路如下: 

  1. RecyclerView是整個頁面的主體,它的運行需要綁定一個Adapter和LayoutManager,在我們的設計裏自定義了VirtualLayoutAdapter和VirtualLayoutManager來綁定到RecyclerView。
  2. VirtualLayoutAdapter繼承自系統的Adaper,它除了提供系統要求創建組件、綁定數據到組件的功能,定義了兩個接口:getLayoutHelper()——用於返回某個位置組件對應的一個LayoutHelper;setLayoutHelpers()——業務方調用此方法設置整個頁面所需要的一系列LayoutHelper。不過這兩個方法的具體實現都委託給VirtualLayoutManager來完成。
  3. VirtualLayoutManager繼承自系統的 LinearLayoutManager,在RecyclerView加載組件或者滑動的時候,會調用VirtualLayoutManager,告訴它當前還有哪些空白區域可以用來擺放組件,也就是調用了架構圖中所示的layoutChunk方法。
  4. VirtualLayoutManager會持有一個LayoutHelperFinder,當layoutChunck被調用的時候,會傳入一個位置參數,告訴LayoutManager當前要佈局第幾個組件,LayoutHelperFinder就通過這個位置找到當前這個位置對應的LayoutHelper,因爲每個LayoutHelper都會綁定它負責的佈局區域的起始位置和結束位置。
  5. LayoutHelper負責具體的佈局邏輯,它有一系列子模塊,其中基類LayoutHelper定義了一系列接口,用來和VirtualLayoutManager通信,包括isOutOfRange()——告訴VirtualLayoutManager它所傳遞過來位置是否在當前LayoutHelper的佈局區域內;setRange()——設置當前LayoutHelper負責的佈局區域;beforeLayout()——在真正佈局之前做一些前置工作;doLayout()——真正的佈局邏輯接口;afterLayout()——在佈局完成之後做一些後置工作;MarginLayoutHelper稍微擴展LayoutHelper,提供了佈局常用的內邊距padding、外邊距margin的計算功能;BaseLayoutHelper是第一層具體實現,實現了當前LayoutHelper在屏幕範圍內的具體區域,用於填充對這一區域填充背景色、背景圖等邏輯。而剩下的LinearLayoutHelper、GridLayoutHelper等負責了具體的佈局邏輯,它們都重點實現了beforeLayout()、doLayout()、afterLayout()方法,特別是在doLayout()方法裏,會獲取一個一組件,按照各自的協議對組件進行尺寸計算、界面佈局。框架內置了以下幾種重要的 LayoutHelper:
    • LinearLayoutHelper,實現簡單的線性佈局;
    • GridLayoutHelper,實現網格佈局,支持1-5列的網格,支持配置列間距、行間距,支持不等寬的網格;
    • StaggeredLayoutHelper,實現瀑布流式的佈局;
    • FloatLayoutHelper,負責懸浮效果,處於該佈局中的組件會懸浮在整個頁面上方,並且可拖拽,不隨頁面滾動而滾動;
    • FixedLayoutHelper,負責固定位置的佈局,它可固定在屏幕某個位置,不可拖拽,不隨頁面滾動而滾動;
    • StickyLayoutHelper,它是一種吸邊的佈局,當它包含的組件處於屏幕可見範圍內的時候,像正常的組件一樣隨頁面滾動而滾動,當組件將要被滑出屏幕返回的時候,可以吸到屏幕的頂部或者底部,實現一種吸住的效果;

工作流程

初始化

在使用vlayout的時候,首先做初始化工作,對業務使用方來說,和使用普通的 RecyclerView + LayoutManager 初始化流程基本一致。對於框架流程上來說,前前後後涉及了6個角色,基本流程如下:

  1. vlayout的業務使用方初始化RecyclerView對象。
  2. 創建一個VirtualLayoutAdapter對象,實現相關接口。
  3. 初始化一個VirtualLayoutManager對象。在初始化VirtualLayoutAdapter的時候,內部也初始化了一個RangeLayoutFinder對象,用來後續的LayoutHelper查找。
  4. 業務使用方需要將VirtualLayoutAdapter和VirtualLayoutManager都綁定到RecyclerView裏。
  5. 獲取數據列表,這個數據就是要顯示到頁面上的源數據,它可以是同步獲取,也可以是異步從本地磁盤或者遠程服務器獲取。最關鍵的地方在用這個數據列表要包含一組佈局和位置信息,能夠用來識別數據列表中從第m個位置到第n個位置的數據它們是該用那種佈局方式進行佈局。這個佈局和位置信息的數據結構並不做強制限制,只要能提供足夠的信息,用來快速方便地完成下述第6步。
  6. 根據數據列表和源數據提供的佈局位置信息,生成LayoutHelper列表,每個LayoutHelper對象會被知道它負責的源數據位置範圍、源數據的個數等信息。
  7. 將生成的LayoutHelper列表傳遞給VirtualLayoutAdapter。
  8. VirtualLayoutAdapter進一步將LayoutHelper列表給VirtualLayoutManager。
  9. VirtualLayoutManager也進一步將LayoutHelper列表傳遞給RangeLayoutHelperFinder。
  10. RangeLayoutHelperFinder真正開始處理這些LayoutHelper列表,它會根據每個LayoutHelper負責佈局的起始位置和結束位置,對LayoutHelper做索引,這樣當後續VirtualLayoutManager傳入一個位置參數讓RangeLayoutHelperFinder查找一個對應的LayoutHelper時,RangeLayoutHelperFinder會通過二分查找的方式返回一個LayoutHelper。
  11. 接下來還要將數據列表也傳遞給VirtualLayoutAdapter。
  12. 至此,整個初始化流程就完成,這裏暴露給業務方的主要是VirtualLayoutAdapter,它接收數據列表和LayoutHelper列表,內部在傳遞給RecyclerView和VirtualLayoutManager進行後續的工作。

佈局過程

當完成前面的初始化工作,將數據和LayoutHelper都綁定到vlayout內部之後,緊接着就可以開始佈局流程了。這裏無論是剛打開頁面第一次佈局,還是用戶滑動頁面,進行一次新的佈局,流程都是一致的。

  1. RecyclerView內部會維護一個狀態,計算當前是否存在未填充滿組件的區域,區域還有多大。
  2. 如果發現有空白區域,就將頁面狀態傳給LayoutManager——在我們的框架裏——就是VirtualLayoutManager,告訴它要進行組件的填充佈局。VirtualLayoutManager能獲取到的信息有當前可見的第一個組件的位置,當前可見的最後一個組件的位置,當前空白區域的大小,這些信息都是RecyclerView提供的,後面纔開始真正vlayout發揮作用的時候。
  3. VirtualLayoutManager先去遍歷所有LayoutHelper,告訴它們當前可視範圍的位置信息,不在範圍之內的LayoutHelper可以做一些清理工作,比如將綁定過背景的LayoutHelper要清理背景。
  4. VirtualLayoutManager獲取到下一個要填充的組件的位置信息。
  5. 通過RangeLayoutHelperFinder找到下一個組件對應的LayoutHelper。
  6. LayoutHelper開始真正佈局一個或者多個組件, 注意一個LayoutHelper一次佈局在寬度上會佈局滿一整行的區域,對於LinearLayoutHelper、FixedLayoutHelper等LayoutHelper,一個組件就佔一整行,這個時候就佈局一個組件就行了;而GridLayoutHelper、StaggeredLayoutHelper等一行可能會擺多個組件,它們一次佈局會將儘可能多的組件都獲取到填充滿一行寬度。至於能填充多少高度,那就根據組件自己佔用的高度來決定了。
  7. LayoutHelper會從讓RecyclerView返回一個組件,RecyclerView會嘗試從回收池裏獲取一個被緩存的組件,如果存在緩存組件,就直接返回給LayoutHelper使用,如果不存在,則要調用Adapter——在vlayout框架裏——就是VirtualLayoutAdapter去生成一個新的 組件實例。這個邏輯是RecyclerView的固有邏輯,也就組件複用的能力。
  8. 當RecyclerView內部不存在一個類型的組件緩存時,VirtualLayoutAdapter生成一個組件,一步一步返回給LayoutHelper。
  9. LayoutHelper獲取到了下一個要佈局的組件,開始佈局。
  10. 佈局之前先對組件進行一次寬、高的測量計算,寬度是LayoutHelper通過佈局信息、樣式等條件計算得到的,限定了當前這個組件只能這麼寬,而高度不由LayoutHelper決定,而是通過測量組件的高度來獲取。
  11. 有了組件的寬高信息,結合一些樣式,比如內邊距、外邊距、組件間間距等信息,LayoutHelper開始佈局當前組件的位置。
  12. 當佈局完一行組件之後,要再去遍歷所有LayoutHelper,告訴它們當前可視範圍的位置信息,做一些後置工作,比如新佈局的區域是不是有背景要綁定,有的話要做背景的設置。懸浮類佈局要根據位置做吸頂或者吸底的特殊處理,在可見範圍內的懸浮類佈局對組件做正常佈局等。
  13. 通過前面佈局過程中組件的高度計算,那麼也就知道當前一次佈局消耗了多少的空白區域。
  14. 這個空白區域進一步反饋給RecyclerView。RecyclerView會進行狀態跟更新,如果空白區域都被填充滿了,那麼就結束一次佈局了,如果還有,就要觸發下一個位置的佈局,在重複上述流程。

效果

demo動效

實戰效果

 

總結

本文着重介紹 vlayout 的設計思路和原理,如果要進一步熟悉其細節,最好是到 github 上下載源碼閱讀,結合本文的說明,效果會更佳。如果想要嘗試使用 vlayout 搭建頁面,也可以到 github 上下載 demo,閱讀使用文檔和樣式屬性說明文檔。

相關文章

摘自:Tangram 的基礎 —— vlayout(Android)

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