生活随笔
收集整理的這篇文章主要介紹了
苹果核 - Tangram 的基础 —— vlayout(Android)
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
1. 前言
天貓沉淀了3年的一套界面解決方案已經(jīng)開始陸續(xù)進(jìn)入開源流程:首先開源的是Android基礎(chǔ)組件VLayout
vlayout 是手機(jī)天貓 Android 版內(nèi)廣泛使用的一個(gè)基礎(chǔ) UI 框架項(xiàng)目 提供了一個(gè)用于RecyclerView的自定義的LayoutManger,可以實(shí)現(xiàn)不同布局格式的混排,目標(biāo)是支撐客戶端native頁(yè)面的快速開發(fā)。它也是 Tangram 框架的基礎(chǔ)模塊,現(xiàn)已開源,歡迎移步到 github 上指教。
2. 簡(jiǎn)介
2.1 背景
Android中UI性能消耗主要來自于兩個(gè)方面:
布局層次嵌套導(dǎo)致多重measure/layoutView控件的創(chuàng)建和銷毀
除了從在實(shí)踐中注意消除嵌套布局,Android官方也提供了ListView/GirdView/RecyclerView等基礎(chǔ)空間來處理View的回收與復(fù)用。
但很多時(shí)候我們都會(huì)碰到視覺需要在一個(gè)長(zhǎng)列表下做多種類型的布局來分配各種元素, 特別是電商業(yè)務(wù)各類首頁(yè),頻道等頁(yè)面,元素結(jié)構(gòu)復(fù)雜多樣。
這種時(shí)候?qū)崿F(xiàn)的選擇有不用復(fù)用,直接用各個(gè)組件進(jìn)行拼接,但這樣會(huì)損失性能;選擇一個(gè)主要的復(fù)用容器, 如ListView或者RecyclerView+LinearLayoutManager等,然后在其中使用嵌套等方式對(duì)其他的布局方式進(jìn)行處理,這樣一個(gè)是減少了復(fù)用的能力,另一個(gè)是如果需要嵌套無法兼容的布局的時(shí)候,需要處理嵌套滑動(dòng)的情況。
既然RecyclerView提供了基礎(chǔ)的回收復(fù)用功能,也支持LayoutManager的擴(kuò)展,那么能不能用一個(gè)LayoutManager就完成所有的布局類型呢? 感覺的這是一個(gè)不錯(cuò)的方向,目前在 github 上也能找到類似的項(xiàng)目,但是這些之前也埋有不少bug, 大部分都是因?yàn)樵谝恍┨厥鈭?chǎng)景下和RecyclerView相關(guān)的其他的類一起使用時(shí)出現(xiàn)問題。 為了避免掉入bug大坑,我們決定基于LinearLayoutManager來做改造。
2.2 特性
自定義了一個(gè)VirtualLayoutManager,它繼承自 LinearLayoutManager;引入了 LayoutHelper 的概念,它負(fù)責(zé)具體的布局邏輯;VirtualLayoutManager管理了一系列LayoutHelper,將具體的布局能力交給LayoutHelper來完成,每一種LayoutHelper提供一種布局方式,框架內(nèi)置提供了幾種常用的布局類型,包括:網(wǎng)格布局、線性布局、瀑布流布局、懸浮布局、吸邊布局等。這樣實(shí)現(xiàn)了混合布局的能力,并且支持?jǐn)U展外部,注冊(cè)新的LayoutHelper,實(shí)現(xiàn)特殊的布局方式。每一種LayoutHelper負(fù)責(zé)布局一批組件范圍內(nèi)的組件,不同組件范圍內(nèi)的組件之間,如果類型相同,可以在滑動(dòng)過程中回收復(fù)用。因此回收粒度比較細(xì),且可以跨布局類型復(fù)用。提供了自定義的布局樣式,可以滿足多樣化的布局需求,比如每一個(gè)組件范圍內(nèi)的布局支持一個(gè)背景顏色、背景圖片;網(wǎng)格布局里,可以支持1列、2列、3列、4列、5列共5種樣式,每一列的寬度默認(rèn)平均分配屏幕寬度,也可以指定按比例分配列寬。吸邊布局支持吸到屏幕底部、屏幕頂部、屏幕左邊、屏幕右邊。這些都是系統(tǒng)默認(rèn)的LayoutManager不支持的。
3. 架構(gòu)
整體的設(shè)計(jì)方案和思路如下:
RecyclerView是整個(gè)頁(yè)面的主體,它的運(yùn)行需要綁定一個(gè)Adapter和LayoutManager,在我們的設(shè)計(jì)里自定義了VirtualLayoutAdapter和VirtualLayoutManager來綁定到RecyclerView。VirtualLayoutAdapter繼承自系統(tǒng)的Adaper,它除了提供系統(tǒng)要求創(chuàng)建組件、綁定數(shù)據(jù)到組件的功能,定義了兩個(gè)接口:getLayoutHelper()——用于返回某個(gè)位置組件對(duì)應(yīng)的一個(gè)LayoutHelper;setLayoutHelpers()——業(yè)務(wù)方調(diào)用此方法設(shè)置整個(gè)頁(yè)面所需要的一系列LayoutHelper。不過這兩個(gè)方法的具體實(shí)現(xiàn)都委托給VirtualLayoutManager來完成。VirtualLayoutManager繼承自系統(tǒng)的 LinearLayoutManager,在RecyclerView加載組件或者滑動(dòng)的時(shí)候,會(huì)調(diào)用VirtualLayoutManager,告訴它當(dāng)前還有哪些空白區(qū)域可以用來擺放組件,也就是調(diào)用了架構(gòu)圖中所示的layoutChunk方法。VirtualLayoutManager會(huì)持有一個(gè)LayoutHelperFinder,當(dāng)layoutChunck被調(diào)用的時(shí)候,會(huì)傳入一個(gè)位置參數(shù),告訴LayoutManager當(dāng)前要布局第幾個(gè)組件,LayoutHelperFinder就通過這個(gè)位置找到當(dāng)前這個(gè)位置對(duì)應(yīng)的LayoutHelper,因?yàn)槊總€(gè)LayoutHelper都會(huì)綁定它負(fù)責(zé)的布局區(qū)域的起始位置和結(jié)束位置。LayoutHelper負(fù)責(zé)具體的布局邏輯,它有一系列子模塊,其中基類LayoutHelper定義了一系列接口,用來和VirtualLayoutManager通信,包括isOutOfRange()——告訴VirtualLayoutManager它所傳遞過來位置是否在當(dāng)前LayoutHelper的布局區(qū)域內(nèi);setRange()——設(shè)置當(dāng)前LayoutHelper負(fù)責(zé)的布局區(qū)域;beforeLayout()——在真正布局之前做一些前置工作;doLayout()——真正的布局邏輯接口;afterLayout()——在布局完成之后做一些后置工作;MarginLayoutHelper稍微擴(kuò)展LayoutHelper,提供了布局常用的內(nèi)邊距padding、外邊距margin的計(jì)算功能;BaseLayoutHelper是第一層具體實(shí)現(xiàn),實(shí)現(xiàn)了當(dāng)前LayoutHelper在屏幕范圍內(nèi)的具體區(qū)域,用于填充對(duì)這一區(qū)域填充背景色、背景圖等邏輯。而剩下的LinearLayoutHelper、GridLayoutHelper等負(fù)責(zé)了具體的布局邏輯,它們都重點(diǎn)實(shí)現(xiàn)了beforeLayout()、doLayout()、afterLayout()方法,特別是在doLayout()方法里,會(huì)獲取一個(gè)一組件,按照各自的協(xié)議對(duì)組件進(jìn)行尺寸計(jì)算、界面布局??蚣軆?nèi)置了以下幾種重要的 LayoutHelper:
- LinearLayoutHelper,實(shí)現(xiàn)簡(jiǎn)單的線性布局;
- GridLayoutHelper,實(shí)現(xiàn)網(wǎng)格布局,支持1-5列的網(wǎng)格,支持配置列間距、行間距,支持不等寬的網(wǎng)格;
- StaggeredLayoutHelper,實(shí)現(xiàn)瀑布流式的布局;
- FloatLayoutHelper,負(fù)責(zé)懸浮效果,處于該布局中的組件會(huì)懸浮在整個(gè)頁(yè)面上方,并且可拖拽,不隨頁(yè)面滾動(dòng)而滾動(dòng);
- FixedLayoutHelper,負(fù)責(zé)固定位置的布局,它可固定在屏幕某個(gè)位置,不可拖拽,不隨頁(yè)面滾動(dòng)而滾動(dòng);
- StickyLayoutHelper,它是一種吸邊的布局,當(dāng)它包含的組件處于屏幕可見范圍內(nèi)的時(shí)候,像正常的組件一樣隨頁(yè)面滾動(dòng)而滾動(dòng),當(dāng)組件將要被滑出屏幕返回的時(shí)候,可以吸到屏幕的頂部或者底部,實(shí)現(xiàn)一種吸住的效果;
4. 工作流程
4.1 初始化
在使用vlayout的時(shí)候,首先做初始化工作,對(duì)業(yè)務(wù)使用方來說,和使用普通的 RecyclerView + LayoutManager 初始化流程基本一致。對(duì)于框架流程上來說,前前后后涉及了6個(gè)角色,基本流程如下:
vlayout的業(yè)務(wù)使用方初始化RecyclerView對(duì)象。創(chuàng)建一個(gè)VirtualLayoutAdapter對(duì)象,實(shí)現(xiàn)相關(guān)接口。初始化一個(gè)VirtualLayoutManager對(duì)象。在初始化VirtualLayoutAdapter的時(shí)候,內(nèi)部也初始化了一個(gè)RangeLayoutFinder對(duì)象,用來后續(xù)的LayoutHelper查找。業(yè)務(wù)使用方需要將VirtualLayoutAdapter和VirtualLayoutManager都綁定到RecyclerView里。獲取數(shù)據(jù)列表,這個(gè)數(shù)據(jù)就是要顯示到頁(yè)面上的源數(shù)據(jù),它可以是同步獲取,也可以是異步從本地磁盤或者遠(yuǎn)程服務(wù)器獲取。最關(guān)鍵的地方在用這個(gè)數(shù)據(jù)列表要包含一組布局和位置信息,能夠用來識(shí)別數(shù)據(jù)列表中從第m個(gè)位置到第n個(gè)位置的數(shù)據(jù)它們是該用那種布局方式進(jìn)行布局。這個(gè)布局和位置信息的數(shù)據(jù)結(jié)構(gòu)并不做強(qiáng)制限制,只要能提供足夠的信息,用來快速方便地完成下述第6步。根據(jù)數(shù)據(jù)列表和源數(shù)據(jù)提供的布局位置信息,生成LayoutHelper列表,每個(gè)LayoutHelper對(duì)象會(huì)被知道它負(fù)責(zé)的源數(shù)據(jù)位置范圍、源數(shù)據(jù)的個(gè)數(shù)等信息。將生成的LayoutHelper列表傳遞給VirtualLayoutAdapter。VirtualLayoutAdapter進(jìn)一步將LayoutHelper列表給VirtualLayoutManager。VirtualLayoutManager也進(jìn)一步將LayoutHelper列表傳遞給RangeLayoutHelperFinder。RangeLayoutHelperFinder真正開始處理這些LayoutHelper列表,它會(huì)根據(jù)每個(gè)LayoutHelper負(fù)責(zé)布局的起始位置和結(jié)束位置,對(duì)LayoutHelper做索引,這樣當(dāng)后續(xù)VirtualLayoutManager傳入一個(gè)位置參數(shù)讓RangeLayoutHelperFinder查找一個(gè)對(duì)應(yīng)的LayoutHelper時(shí),RangeLayoutHelperFinder會(huì)通過二分查找的方式返回一個(gè)LayoutHelper。接下來還要將數(shù)據(jù)列表也傳遞給VirtualLayoutAdapter。至此,整個(gè)初始化流程就完成,這里暴露給業(yè)務(wù)方的主要是VirtualLayoutAdapter,它接收數(shù)據(jù)列表和LayoutHelper列表,內(nèi)部在傳遞給RecyclerView和VirtualLayoutManager進(jìn)行后續(xù)的工作。
4.2 布局過程
當(dāng)完成前面的初始化工作,將數(shù)據(jù)和LayoutHelper都綁定到vlayout內(nèi)部之后,緊接著就可以開始布局流程了。這里無論是剛打開頁(yè)面第一次布局,還是用戶滑動(dòng)頁(yè)面,進(jìn)行一次新的布局,流程都是一致的。
RecyclerView內(nèi)部會(huì)維護(hù)一個(gè)狀態(tài),計(jì)算當(dāng)前是否存在未填充滿組件的區(qū)域,區(qū)域還有多大。如果發(fā)現(xiàn)有空白區(qū)域,就將頁(yè)面狀態(tài)傳給LayoutManager——在我們的框架里——就是VirtualLayoutManager,告訴它要進(jìn)行組件的填充布局。VirtualLayoutManager能獲取到的信息有當(dāng)前可見的第一個(gè)組件的位置,當(dāng)前可見的最后一個(gè)組件的位置,當(dāng)前空白區(qū)域的大小,這些信息都是RecyclerView提供的,后面才開始真正vlayout發(fā)揮作用的時(shí)候。VirtualLayoutManager先去遍歷所有LayoutHelper,告訴它們當(dāng)前可視范圍的位置信息,不在范圍之內(nèi)的LayoutHelper可以做一些清理工作,比如將綁定過背景的LayoutHelper要清理背景。VirtualLayoutManager獲取到下一個(gè)要填充的組件的位置信息。通過RangeLayoutHelperFinder找到下一個(gè)組件對(duì)應(yīng)的LayoutHelper。LayoutHelper開始真正布局一個(gè)或者多個(gè)組件, 注意一個(gè)LayoutHelper一次布局在寬度上會(huì)布局滿一整行的區(qū)域,對(duì)于LinearLayoutHelper、FixedLayoutHelper等LayoutHelper,一個(gè)組件就占一整行,這個(gè)時(shí)候就布局一個(gè)組件就行了;而GridLayoutHelper、StaggeredLayoutHelper等一行可能會(huì)擺多個(gè)組件,它們一次布局會(huì)將盡可能多的組件都獲取到填充滿一行寬度。至于能填充多少高度,那就根據(jù)組件自己占用的高度來決定了。LayoutHelper會(huì)從讓RecyclerView返回一個(gè)組件,RecyclerView會(huì)嘗試從回收池里獲取一個(gè)被緩存的組件,如果存在緩存組件,就直接返回給LayoutHelper使用,如果不存在,則要調(diào)用Adapter——在vlayout框架里——就是VirtualLayoutAdapter去生成一個(gè)新的 組件實(shí)例。這個(gè)邏輯是RecyclerView的固有邏輯,也就組件復(fù)用的能力。當(dāng)RecyclerView內(nèi)部不存在一個(gè)類型的組件緩存時(shí),VirtualLayoutAdapter生成一個(gè)組件,一步一步返回給LayoutHelper。LayoutHelper獲取到了下一個(gè)要布局的組件,開始布局。布局之前先對(duì)組件進(jìn)行一次寬、高的測(cè)量計(jì)算,寬度是LayoutHelper通過布局信息、樣式等條件計(jì)算得到的,限定了當(dāng)前這個(gè)組件只能這么寬,而高度不由LayoutHelper決定,而是通過測(cè)量組件的高度來獲取。有了組件的寬高信息,結(jié)合一些樣式,比如內(nèi)邊距、外邊距、組件間間距等信息,LayoutHelper開始布局當(dāng)前組件的位置。當(dāng)布局完一行組件之后,要再去遍歷所有LayoutHelper,告訴它們當(dāng)前可視范圍的位置信息,做一些后置工作,比如新布局的區(qū)域是不是有背景要綁定,有的話要做背景的設(shè)置。懸浮類布局要根據(jù)位置做吸頂或者吸底的特殊處理,在可見范圍內(nèi)的懸浮類布局對(duì)組件做正常布局等。通過前面布局過程中組件的高度計(jì)算,那么也就知道當(dāng)前一次布局消耗了多少的空白區(qū)域。這個(gè)空白區(qū)域進(jìn)一步反饋給RecyclerView。RecyclerView會(huì)進(jìn)行狀態(tài)跟更新,如果空白區(qū)域都被填充滿了,那么就結(jié)束一次布局了,如果還有,就要觸發(fā)下一個(gè)位置的布局,在重復(fù)上述流程。
5. 效果
demo動(dòng)效
實(shí)戰(zhàn)效果
6. 總結(jié)
本文著重介紹 vlayout 的設(shè)計(jì)思路和原理,如果要進(jìn)一步熟悉其細(xì)節(jié),最好是到 github 上下載源碼閱讀,結(jié)合本文的說明,效果會(huì)更佳。如果想要嘗試使用 vlayout 搭建頁(yè)面,也可以到 github 上下載 demo,閱讀使用文檔和樣式屬性說明文檔。
7. 相關(guān)文章
- vlayout使用說明(一)
- vlayout使用說明(二)
原文鏈接:http://pingguohe.net/2017/02/28/vlayout-design.html
總結(jié)
以上是生活随笔為你收集整理的苹果核 - Tangram 的基础 —— vlayout(Android)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。