VirtualLayout下載
Github:https://github.com/alibaba/vlayout
VirtualLayout簡介
VirtualLayout是一個針對RecyclerView的LayoutManager擴展, 主要提供一整套布局方案和布局間的組件復用的問題。
VirtualLayout設計思路
通過定制化的LayoutManager,接管整個RecyclerView的布局邏輯;LayoutManager管理了一系列LayoutHelper,LayoutHelper負責具體布局邏輯實現的地方;每一個LayoutHelper負責頁面某一個范圍內的組件布局;不同的LayoutHelper可以做不同的布局邏輯,因此可以在一個RecyclerView頁面里提供異構的布局結構,這就能比系統自帶的LinearLayoutManager、GridLayoutManager等提供更加豐富的能力。同時支持擴展LayoutHelper來提供更多的布局能力。
VirtualLayout主要功能
默認通用布局實現,解耦所有的View和布局之間的關系: Linear, Grid, 吸頂, 浮動, 固定位置等。
① LinearLayoutHelper: 線性布局
②GridLayoutHelper: Grid布局, 支持橫向的colspan
③FixLayoutHelper: 固定布局,始終在屏幕固定位置顯示
④ScrollFixLayoutHelper: 固定布局,但之后當頁面滑動到該圖片區域才 顯示, 可以用來做返回頂部或其他書簽等
⑤FloatLayoutHelper:浮動布局,可以固定顯示在屏幕上,但用戶可以拖拽其位置
⑥ColumnLayoutHelper: 欄格布局,都布局在一排,可以配置不同列之間的寬度比值
⑦SingleLayoutHelper: 通欄布局,只會顯示一個組件View
⑧OnePlusNLayoutHelper: 一拖N布局,可以配置1-5個子元素
⑨StickyLayoutHelper: stikcy布局, 可以配置吸頂或者吸底
⑩StaggeredGridLayoutHelper:瀑布流布局,可配置間隔高度/寬度
上述默認實現里可以大致分為兩類:一是非fix類型布局,像線性、Grid、欄格等,它們的特點是布局在整個頁面流里,隨頁面滾動而滾動;另一類就是fix類型的布局,它們的子節點往往不隨頁面滾動而滾動。
所有除布局外的組件復用,VirtualLayout將用來管理大的模塊布局組合,擴展了RecyclerView,使得同一RecyclerView內的組件可以復用,減少View的創建和銷毀過程。
VLayout使用
// gradle
compile ('com.alibaba.android:vlayout:1.0.4@aar') { ? ?transitive =true}
①初始化LayoutManager
final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
final VirtualLayoutManager layoutManager =newVirtualLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
②設置回收復用池大小,(如果一屏內相同類型的 View 個數比較多,需要設置一個合適的大小,防止來回滾動時重新創建 View):
RecyclerView.RecycledViewPool ?viewPool = new RecyclerView.RecycledViewPool();
recyclerView.setRecycledViewPool(viewPool);
viewPool.setMaxRecycledViews(0,10);
③加載數據時有兩種方式:
第 一種:使用 DelegateAdapter, 可以像平常一樣寫繼承自DelegateAdapter.Adapter的Adapter, 只比之前的Adapter需要多重載onCreateLayoutHelper方法。 其他的和默認Adapter一樣。
DelegateAdapter delegateAdapter=newDelegateAdapter(layoutManager, hasStableItemType);
recycler.setAdapter(delegateAdapter);// 之后可以通過 setAdapters 或 addAdapter方法添加
DelegateAdapter.AdapterdelegateAdapter.setAdapters(adapters);
//OR
CustomAdapter adapter=newCustomAdapter(data,newGridLayoutHelper());
delegateAdapter.addAdapter(adapter);// 如果數據有變化,調用自定義
adapter 的 notifyDataSetChanged()adapter.notifyDataSetChanged();
第二種:當業務有自定義的復雜需求的時候, 可以繼承自VirtualLayoutAdapter, 實現自己的Adapter
public class ? ?MyAdapter extends ?VirtualLayoutAdapter{
......
}
MyAdapter myAdapter =newMyAdapter(layoutManager);//構造 layoutHelper 列表List helpers =newLinkedList<>();
GridLayoutHelper gridLayoutHelper =newGridLayoutHelper(4);
gridLayoutHelper.setItemCount(25);
helpers.add(gridLayoutHelper);
GridLayoutHelper gridLayoutHelper2 =newGridLayoutHelper(2);
gridLayoutHelper2.setItemCount(25);
helpers.add(gridLayoutHelper2);//將 layoutHelper 列表傳遞給 adapter
myAdapter.setLayoutHelpers(helpers);//將 adapter 設置給 recyclerViewrecycler.setAdapter(myAdapter);
在這種情況下,需要使用者注意在當LayoutHelpers的結構或者數據數量等會影響到布局的元素變化時,需要主動調用setLayoutHepers去更新布局模式。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?VLayout運行效果
VirtualLayout組件復用的問題
比如碰到卡頓、類型轉換異常等等,都有可能是復用的問題引起的。
在使用 DelegateAdapter 的時候,每一個 LayoutHelper 都對應于一個 DelegateAdapter.Adapter,一般情況下使用方只需要提供自定義的 DelegateAdapter.Adapter,然后按照正常的使用方式使用。
但這里有個問題,不同的 DelegateAdapter.Adapter 之間,他們的 itemType 是不是一樣的?這里有一個選擇:在 DelegateAdapter 的構造函數里有個 hasConsistItemType 參數(默認是 false ):
當hasConsistItemType=false的時候,即使不同 DelegateAdapter.Adapter 里返回的相同的 itemType,對于 DelegateAdapter 也會將它轉換成不同的值,對于 RecyclerView 來說它們是不同的類型。
當hasConsistItemType=true 的時候,不同的 DelegateAdapter.Adapter 之間返回相同的 itemType 的時候,他們之間是可以復用的。
因此如果沒有處理好這一點,會導致 ViewHolder 的類型轉換異常等 bug。有一篇更加詳細的資料可參考:PairFunction
補充:后來發現一個 bug,當 hasConsistItemType=true,在同一位置數據變化,前后構造了不一樣的 Adapter,它們返回的 itemType 一樣,也會導致類型轉換出錯,詳見:#182,目前采用人工保證返回不同的 itemType 來規避。
VirtualLayout設置每種類型回收復用池的大小
在 README 里寫了這么一段 demo:viewPool.setMaxRecycledViews(0, 10);,很多人誤以為只要這么設置就可以了,實際上有多少種類型的 itemType,就得為它們分別設置復用池大小。比如:
viewPool = new RecyclerView.RecycledViewPool();
recyclerView.setRecycledViewPool(viewPool);
viewPool.setMaxRecycledViews(0,5);
viewPool.setMaxRecycledViews(1,5);
viewPool.setMaxRecycledViews(2,5);
viewPool.setMaxRecycledViews(3,10);
viewPool.setMaxRecycledViews(4,10);
viewPool.setMaxRecycledViews(5,10);
...
VirtualLayout混淆問題
如果碰到release包(混淆過)無法正常運行,debug包(一般未混淆)可正常運行,檢查一下混淆配置是否完整:
-keepattributesInnerClasses-keepclasscom.alibaba.android.vlayout.ExposeLinearLayoutManagerEx{ *;}-keepclassandroid.support.v7.widget.RecyclerView$LayoutParams{ *;}-keepclassandroid.support.v7.widget.RecyclerView$ViewHolder{ *;}-keepclassandroid.support.v7.widget.ChildHelper{ *;}-keepclassandroid.support.v7.widget.ChildHelper$Bucket{ *;}-keepclassandroid.support.v7.widget.RecyclerView$LayoutManager{ *;}
VirtualLayout下拉刷新和加載更多
VLayout 只負責布局,下拉刷新和加載更多需要業務方自己處理,當然可能存在和一些下拉刷新控件不兼容的 bug。
個人推薦一個刷新控件:SmartRefreshLayout
下拉刷新,有很多框架是通過判斷 RecyclerView 的第一個 view 的 top 是否為 0 來觸發下拉動作。VLayout 里在處理背景、懸浮態的時候加入了一些對 LayoutManager 不可見的 View,但又真實存在與 RecyclerView 的視圖樹里,建議使用 layoutManager.getChildAt(0) 來獲取第一個 view。
加載更多,可以通過 recyclerView 的滾動狀態來觸發 load-more 事件,需要使用方注冊一個 OnScrollListener:
RecyclerView.OnScrollListener onScrollListener =newRecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView recyclerView,intnewState) {}
public void onScrolled(RecyclerView recyclerView,intdx,intdy) {
//hasMore: status of current page, means if there's more data, you have to maintain this status
if(hasMore) { ? ? ? ? ?
?VirtualLayoutManager lm = (VirtualLayoutManager)recyclerView.getLayoutManager();
intfirst=0, last=0, total=0;? ? ? ? ? ? ? ??
first = ((LinearLayoutManager)lm).findFirstVisibleItemPosition(); ? ? ? ??
last = ((LinearLayoutManager)lm).findLastVisibleItemPosition(); ? ??
total = recyclerView.getAdapter().getItemCount();if(last >0&& last >= total? - earlyCountForAutoLoad) {
//earlyCountForAutoLoad: help to trigger load more listener earlier
//TODO trigger loadmore listener
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? }? ? ? ? ?
? ? ?}? ? ??
? }
VirtualLayout橫向滑動
沒有實現橫向滾動的 LayoutHelper,因為 LayoutHelper 目前只能做靜態的布局,對于跟數據綁定的動態橫向滾動布局,比如 ViewPager 或者 RecyclerView ,建議使用組件的形式提供。也就是一個 LinearLayoutHelper 包一個 Item,這個 Item 是 ViewPager 或者橫向滾動的 RecyclerView,且它們是可以和整個頁面的 RecyclerView 共用一個回收復用池的。
VirtualLayout設置背景圖后觸發循環布局
給 LayoutHelper 設置背景圖的時候,由于這個過程是在布局 view 的階段,設置了圖片會觸發一次新的 layout,從而又導致觸發一次背景圖設置,最終進入死循環,因此需要使用方在設置背景圖的時候判斷當前圖片是否已經加載過一次并且成功,如果綁定過一次就不需要再設置圖片了,阻斷死循環的路徑。 具體做法是:在 BaseLayoutHelper.LayoutViewBindListener 的 onBind() 方法里判斷是否成功綁定過該背景圖。在BaseLayoutHelper.LayoutViewUnBindListener 的 onUnbind() 方法里清楚綁定成功與否的狀態。 在使用方的圖片加載成功回調函數里設置一下圖片加載成功的狀態,可以自行維護一個 map 或者給 View 設置一個 tag 標記。
VirtualLayout在可滾動區域里嵌套使用 vlayout 的 RecyclerView
不太建議嵌套滾動,除非手勢不沖突;如果要完全展開 vlayout 里的內容,犧牲滾動復用,可以調用 VirtualLayoutManager 的 setNoScrolling(true); 方法設置一下。
VirtualLayout為 GridLayoutHelper 的設置自定義 SpanSizeLookup
在 SpanSizeLookup 中,public int getSpanSize(int position) 方法參數的 position 是整個頁面的 position 信息,需要獲取當前 layoutHelper 內的相對位置,需要減去一個偏移量,即 position - getStartPosition()。
VirtualLayout獲取 DelegateAdapter 里數據的相對位置
在 DelegateAdapter 里有 findOffsetPosition(int absolutePosition) 方法,傳入整個頁面的絕對位置,獲取相對位置。 或者用
public static abstract class Adapter<VH extends RecyclerView.ViewHolder>extendsRecyclerView.Adapter{
public abstract LayoutHelper onCreateLayoutHelper();
protectedvoidonBindViewHolderWithOffset(VH holder,intposition,intoffsetTotal) {? ? ?
?}
??}
中的 onBindViewHolderWithOffset() 方法代替傳統的 onBindViewHolder() 方法,其中的 position 參數也是相對位置。