Tangram 的基礎 —— vlayout(Android)

xfct 7年前發布 | 12K 次閱讀 Android開發 移動開發 RecyclerView

前言

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,閱讀使用文檔和樣式屬性說明文檔。

 

來自:http://pingguohe.net/2017/02/28/vlayout-design.html

 

 本文由用戶 xfct 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!