大家好,今天又帶來了項目中具體遇到的需求。做一個首界面,該首界面有很多功能塊,同時這些功能塊是動態的,因為登錄的人的權限的不同,會顯示不同的功能塊,因為功能模塊的數量不一定,所以當功能塊多的時候,整個界面是可以上下滑動的。其實類似有點像淘寶的首界面。如下圖所示。
首先我說的不是最優的??赡苡懈玫慕鉀Q方式。我這里也只是嘗試了新的方式而已。
我先說下我最剛開始想到的最傳統的想法:
1.首先因為功能塊多的時候,需要界面能夠滾動,所以我想到最外面用的ScrollView,然后ScrollView中包含了一個豎向排布的LienarLayout。
然后在放入一個ImageView顯示這個頂部圖片:
然后需要二個橫向的LinearLayout,用來顯示這個大的分類標題:
然后再放入二個GridView顯示功能模塊:
OK。我發現我的首界面寫好了之后:
<ScrollView>
<LinearLayout>
<ImageView> <頂部圖片>
<LinearLayout><View/><TextView/><LinearLayout> <我的服務標題欄>
<GridView /> <我的服務功能塊>
<LinearLayout><View/><TextView/><LinearLayout> <我的功能標題欄>
<GridView /> <我的功能功能塊>
<LinearLayout>
<ScrollView/>
1.布局的內容非常之多。維護很不方便
2.定制化功能差了很多,如果我下次想在《我的服務》和《我的功能》大功能分類中,再多加一個《我的售后》,又的去布局中查找相應的位置,然后去去添加新的布局代碼,或者是我想刪除模塊功能了,我還得去布局中找出來,然后去刪除它。反正就是很麻煩。
3.當前這個界面還算簡單的,畢竟功能塊都是以類似九宮格的形式呈現,如果哪天多了個《我的售后》,然后這個《我的售后》不是以這種九宮格的形式呈現,整個界面中有各種各樣的布句呈現,管理會變的十分麻煩。
有沒有什么好的辦法呢。
上面說到過。我們的界面有沒有像淘寶的首界面,各種布句雜糅在一起,然后又可以上下滾動,沒錯,那我就模仿淘寶的首頁一樣寫個不就行了么。
這時候我的思路就變了:整個界面就使用一個RecycleView來完成。
然后里面的不同布局方式使用不同的LayoutManager不就可以了么。當然因為前面講了我們可以模仿淘寶的首頁來寫,那我們當然是使用阿里巴巴開源的vlayout。
這時候介紹一下我們的主角:vlayout
vlayout is a powerfull LayoutManager extension for RecyclerView, it provides a group of layouts for RecyclerView. Make it able to handle a complicate situation when grid, list and other layouts in the same recyclerview.
我們可以看到,vlayout是一個強大的RecycleView的LayoutManager,它可以幫我在RecycleView中呈現多種布局方式。
正式起航:
首先,vlayout的基本使用方法,其他大神寫的很多也很好。我也不會浪費時間再寫一遍:
請看這篇,基本就能夠對Vlayout有所了解及使用了:
重要的事情說三遍!!!大家一定要看一遍再使用。
Android開源庫V - Layout:淘寶、天貓都在用的UI框架,趕緊用起來吧!
Android開源庫V - Layout:淘寶、天貓都在用的UI框架,趕緊用起來吧!
Android開源庫V - Layout:淘寶、天貓都在用的UI框架,趕緊用起來吧!
我們回頭再來看我們上面的具體的項目需求:
(我會先用VLayout實現一種簡單的處理。然后再實現更加通用的處理!一定要看完最后的通用處理哦?。?/h3>
簡單處理:
我們首先整個activity的布局變為了:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/work_recycleview"
android:background="@android:color/white"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
>
</android.support.v7.widget.RecyclerView>
是不是變的干凈簡潔了?。。?/p>
然后我們要使用Vlayout來設置我們RecycleView中的各種布局。
RecycleView workRecycleview = (RecycleView)findViewById(R.id.work_recycleview);
//建立我們的委托LayoutManger
VirtualLayoutManager layoutManager = new VirtualLayoutManager(this);
workRecycleview.setLayoutManager(layoutManager);
//通過這個layoutManager來管理一系列的LayoutHelper
//所以我們先建立一個List來存放等會要用到的LayoutHelper.
List<LayoutHelper> helperList = new LinkedList<>();
//1.
//因為第一個底部圖片就這一項,所以我們就直接使用SingleLayoutHelper
SingleLayoutHelper bannerLayoutHelper = new SingleLayoutHelper();
bannerLayoutHelper.setItemCount(1);
helperList.add(bannerLayoutHelper);
//2.
//因為大標題欄是一個橫向的LinearLayout,所以使用LinearLayoutHelper
LinearLayoutHelper personTitleHelper = new LinearLayoutHelper();
personTitleHelper.setItemCount(1);
helperList.add(personTitleHelper);
//3.
//因為功能塊目前是九宮格,所以使用的是GridLayoutHelper
GridLayoutHelper personGridHelper = new GridLayoutHelper(3);
personGridHelper.setAutoExpand(false);
personGridHelper.setWeights(new float[]{33, 33, 33});
//設置登錄時候獲取到的該用戶權限下顯示的功能數量。
personGridHelper.setItemCount(mPersonFunctions.size());
helperList.add(personGridHelper);
//4.
//同2界面
LinearLayoutHelper companyTitleHelper = new LinearLayoutHelper();
companyTitleHelper.setItemCount(1);
helperList.add(companyTitleHelper);
//5.
//同3界面
GridLayoutHelper companyGridHelper = new GridLayoutHelper(3);
companyGridHelper.setWeights(new float[]{33, 33, 33});
companyGridHelper.setAutoExpand(false);
//設置登錄時候獲取到的該用戶權限下顯示的功能數量。
companyGridHelper.setItemCount(mCompanyFunctions.size());
helperList.add(companyGridHelper);
如果我們需要增加新的布局控制。我們只需要添加新的LayoutHelper,按順序添加到我們的helperList中即可。
目前的LayoutHelper有以下幾種:
- LinearLayoutHelper: 線性布局
- GridLayoutHelper: Grid布局, 支持橫向的colspan
- FixLayoutHelper: 固定布局,始終在屏幕固定位置顯示
- ScrollFixLayoutHelper: 固定布局,但之后當頁面滑動到該圖片區域才顯示, 可以用來做返回頂部或其他書簽等
- FloatLayoutHelper: 浮動布局,可以固定顯示在屏幕上,但用戶可以拖拽其位置
- ColumnLayoutHelper: 欄格布局,都布局在一排,可以配置不同列之間的寬度比值
- SingleLayoutHelper: 通欄布局,只會顯示一個組件View
- OnePlusNLayoutHelper: 一拖N布局,可以配置1-5個子元素
- StickyLayoutHelper: stikcy布局, 可以配置吸頂或者吸底
- StaggeredGridLayoutHelper: 瀑布流布局,可配置間隔高度/寬度
既然用到RecycleView ,那怎么可以沒有Adapter呢,上面的準備工作做了一部分后,我們開始寫我們的Adapter。
我們這里選擇繼承了VirtualLayoutAdapter:
我們在構造函數中傳入我們二個九宮格功能塊對應的List進來。
public class WorkAdapter extends VirtualLayoutAdapter {
int oneFuncs, twoFuncs;//
public List<FunctionBean> oneFunctions;
public List<FunctionBean> twoFunctions;
public static final int BANNER_VIEW_TYPE = 0;
public static final int DIVIDER_VIEW_TYPE = 1;
public static final int FUN_VIEW_TYPE = 2;
public WorkAdapter(@NonNull VirtualLayoutManager layoutManager, List<FunctionBean> oneFunctions, List<FunctionBean> twoFunctions,funcItemOnClickListener listener) {
super(layoutManager);
this.oneFunctions = oneFunctions;
this.twoFunctions = twoFunctions;
this.listener = listener;
oneFuncs = oneFunctions.size();
twoFuncs = twoFunctions.size();
}
}
我們來分別看Adapter中每個方法具體的復寫:
@Override
public int getItemCount() {
int totalCount = 0;
List<LayoutHelper> helpers = getLayoutHelpers();
if (helpers == null) {
return 0;
}
for (int i = 0; i < helpers.size(); i++) {
totalCount += helpers.get(i).getItemCount();
}
return totalCount;
}
我們可以看到在getItemCount()
方法中,我們通過遍歷了LayoutHelper,分別取每個LayoutHelper中我們剛設置的個數。然后加起來,作為整個RecycleView 的個數。
@Override
public int getItemViewType(int position) {
if (position == 0) {
return BANNER_VIEW_TYPE;
} else if (position == 1 || position == (2 + oneFuncs)) {
return DIVIDER_VIEW_TYPE;
} else {
return FUN_VIEW_TYPE;
}
}
我們可以看到。我們在getItemViewType
方法中肯定position的值,返回不同的type,這樣等會在onCreateViewHolder
方法中就可以返回不同的ViewHolder了。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case BANNER_VIEW_TYPE:
return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_work_banner, parent, false));
case DIVIDER_VIEW_TYPE:
return new DividerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_work_divider, parent, false));
case FUN_VIEW_TYPE:
return new FuncViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_work_func, parent, false));
default:
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof DividerViewHolder) {
if (position == 1) {
((DividerViewHolder) holder).dividerTitle.setText("我的服務");
((DividerViewHolder) holder).colorView.setBackgroundColor(Color.parseColor("#F9C025"));
} else {
((DividerViewHolder) holder).dividerTitle.setText("我的功能");
((DividerViewHolder) holder).colorView.setBackgroundColor(Color.parseColor("#35A7FF"));
}
} else if (holder instanceof FuncViewHolder) {
if(position > 1 && position < 2+ oneFuncs){
...
...
...
}else if(position > 2+ oneFuncs){
...
...
...
}
}
}
就這樣我們就具體的實現了多個布局的設置,而且當你要再加一個新的也很方便。但是也許你這時候會發現,如果我們的布局很長,有很多九宮格,或者真的像淘寶一樣,這個界面有各種功能塊。那我們剛寫的
@Override
public int getItemViewType(int position) {
if (position == 0) {
return BANNER_VIEW_TYPE;
} else if (position == 1 || position == (2 + oneFuncs)) {
return DIVIDER_VIEW_TYPE;
} else {
return FUN_VIEW_TYPE;
}
}
這里你判斷的position就會很多。你可能就要有很多的if-else 來控制返回不同的type.
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof DividerViewHolder) {
if (position == 1) {
((DividerViewHolder) holder).dividerTitle.setText("我的服務");
((DividerViewHolder) holder).colorView.setBackgroundColor(Color.parseColor("#F9C025"));
} else {
((DividerViewHolder) holder).dividerTitle.setText("我的功能");
((DividerViewHolder) holder).colorView.setBackgroundColor(Color.parseColor("#35A7FF"));
}
} else if (holder instanceof FuncViewHolder) {
if(position > 1 && position < 2+ oneFuncs){
...
...
...
}else if(position > 2+ oneFuncs){
...
...
...
}
}
}
然后在onBindViewHolder
方法里面也要有很多的if-else,到后面維護又會變的很麻煩,所以這樣寫相對簡單,適合模塊不多的情況。
復雜的并且更通用的實現:
我們前面是把很多LayoutHelper加入到了Adapter中,然后RecycleView直接設置該Adapter,
我們這次就不這么做了。而且中間多設置一步,就是先每個LayoutHelper設置一個Adapter,成為<子的Adapter>,然后把這些Adapter再統一放入到一個<總的Adapter>中,再把這個<總的Adapter>賦值給RecycleView即可。
因為這樣在<總的Adapter>中,對于每個Helper就可以知道個數,并且ViewHolder等賦值都被分配到了那些<子的Adapter>中處理了。我們不用再if-else的寫很多情況了。
我們直接來看這個<總的Adapter> 是如何實現的:
(不過我不會完全很仔細的講解,代碼我也不會貼全部,就貼一些主要的地方,講主要的部分。)
public class DelegateAdapter extends VirtualLayoutAdapter<RecyclerView.ViewHolder> {
private int mTotal = 0;
private int mIndex = 0;
private SparseArray<Adapter> mItemTypeAry = new SparseArray<>();
@NonNull
private final List<Pair<AdapterDataObserver, Adapter>> mAdapters = new ArrayList<>();
private final SparseArray<Pair<AdapterDataObserver, Adapter>> mIndexAry = new SparseArray<>();
}
我們先定義了幾個參數,
mIndex
用來等于標記各個加入的<子的Adapter>的序號
一個用來存<子的Adapter>的Map(別問我為什么Map類型里面只填了一個Adapter,不是Key-Value? 可以補下SparseArray和ArrayMap的知識了,Android中用來替換HashMap的類。)
一個存放了Pair<AdapterDataObserver, Adapter>
的List集合(Pair如果也不知道,也可以去補充下,就簡單理解為一個有二個屬性的對象,第一個屬性是AdapterDataObserver,第二個是Adapter)
一個存放了Pair<AdapterDataObserver, Adapter>
的Map集合。
那我們知道肯定要有個方法把這些子的Adapter給加進來:
public void setAdapters(@Nullable List<Adapter> adapters) {
clear();//把相關的參數都重新置空,這里不寫出來了。
if (adapters == null) {
adapters = Collections.emptyList();
}
List<LayoutHelper> helpers = new LinkedList<>();
mTotal = 0;
Pair<AdapterDataObserver, Adapter> pair;
for (Adapter adapter : adapters) {
// every adapter has an unique index id
//自定義類AdapterDataObserver ,繼承于RecyclerView.AdapterDataObserver
AdapterDataObserver observer = new AdapterDataObserver(mTotal, mIndex++);
adapter.registerAdapterDataObserver(observer);
//子的Adapter中自定義的方法:onCreateLayoutHelper(),用來返回子的Adapter中的LayoutHelper
LayoutHelper helper = adapter.onCreateLayoutHelper();
//而且這些子的Adapter中的LayoutHelper的個數,就是這些子的Adapter的個數
helper.setItemCount(adapter.getItemCount());
//總數為每個LayoutHelper的個數之和,也就是每個子的Adapter的個數之和
mTotal += helper.getItemCount();
helpers.add(helper);
pair = Pair.create(observer, adapter);
//這里的mIndexAry存放了以加入的順序mIndex為Key的Pair<AdapterObserver,Adapter>
mIndexAry.put(observer.mIndex, pair);
//同時mAdapters的List集合中也存放了Pair<AdapterObserver,Adapter>
mAdapters.add(pair);
}
super.setLayoutHelpers(helpers);
}
我們看下AdapterDataObser的部分代碼:
protected class AdapterDataObserver extends RecyclerView.AdapterDataObserver {
int mStartPosition;
int mIndex = -1;
public AdapterDataObserver(int startPosition, int index) {
this.mStartPosition = startPosition;
this.mIndex = index;
}
}
我們可以看到我們剛在new每個AdapterDataObser的時候傳入的構造函數參數是
mTotal
,mIndex++
,這樣是不是正好每個Adapter中的AdapterDataObserver中的mStartPosition
參數就是你的這個Adapter在所有整個RecycleView中的開始的position值。而mIndex
又說明了這個AdapterDataObserver是第幾個,也就是這個Adapter是所有的<子的Adapter>中的第幾個。
@Override
public int getItemCount() {
return mTotal;
}
總數就是返回上面我們的mTotal參數。
@Override
public int getItemViewType(int position) {
Pair<AdapterDataObserver, Adapter> p = findAdapterByPosition(position);
if (p == null) {
return RecyclerView.INVALID_TYPE;
}
int subItemType = p.second.getItemViewType(position - p.first.mStartPosition);
if (subItemType < 0) {
// negative integer, invalid, just return
return subItemType;
}
if (mHasConsistItemType) {
mItemTypeAry.put(subItemType, p.second);
return subItemType;
}
int index = p.first.mIndex;
return (int) getCantor(subItemType, index);
}
我們一步步來看這個比較關鍵的地方,我們之所以不用我們最剛開始第一次講的Vlayout使用的方法,就是因為我們的LayoutHelper多了之后,在getItemViewType()
方法中返回不同的ViewType需要很多if-else來處理。所以這里我們看他是如何自動處理的。
第一步:
Pair<AdapterDataObserver, Adapter> p = findAdapterByPosition(position);
我們看findAdapterByPosition方法的具體實現:
@Nullable
public Pair<AdapterDataObserver, Adapter> findAdapterByPosition(int position) {
//獲取我們上面的mAdapter集合,里面存的是Pair<AdapterObserver,Adapter>
final int count = mAdapters.size();
if (count == 0) {
return null;
}
int s = 0, e = count - 1, m;
Pair<AdapterDataObserver, Adapter> rs = null;
// binary search range
while (s <= e) {
m = (s + e) / 2;
rs = mAdapters.get(m);
int endPosition = rs.first.mStartPosition + rs.second.getItemCount() - 1;
if (rs.first.mStartPosition > position) {
e = m - 1;
} else if (endPosition < position) {
s = m + 1;
} else if (rs.first.mStartPosition <= position && endPosition >= position) {
break;
}
rs = null;
}
return rs;
}
通過這個方法的字面意思我們不難理解:通過這個<總的Adapter>返回的item的position,來知道這個position是屬于我們存了Adapter集合中的哪個Adapter的。
先獲取我們上面已經保存了各個Pair<AdapterObserver,Adapter>
的mAdapters集合,然后判斷個數,為0就直接返回了。不為0,我們就通過二分法查找的方式來進行查找。我們前面已經在每個AdapterDataObserver中存了相對于的Adapter的起始的Position,我們只需要不停的判斷現在傳給這個方法的position是在(子的Adapter 的起始position) 與 (子的Adapter 的起始position + 子的Adapter的個數)之間,如果是,就說明是屬于這個Adapter,我們就在mAdapters集合中取出相應的Pair<AdapterObserver,Adapter>
。
第二步:
int subItemType = p.second.getItemViewType(position - p.first.mStartPosition);
if (subItemType < 0) {
// negative integer, invalid, just return
return subItemType;
}
if (mHasConsistItemType) {
mItemTypeAry.put(subItemType, p.second);
return subItemType;
}
int index = p.first.mIndex;
return (int) getCantor(subItemType, index);
這里的
(position - p.first.mStartPosition)
其實就是這個<總的Adapter>的處于position的這一項,在這個<子的Adapter>里面的具體的position值。最后通過這個<子的Adapter>的getItemViewType
來得到<子的Adapter>的ViewType。這樣就自動幫我們判斷了在<總的Adapter>中的某個position值的Item的所屬的<子的Adapter>的ViewType了。而不用寫很多if-else來判斷了。
這里又要分二種情況,也就是一個boolean值mHasConsistItemType
來控制:
在這個<總的Adapter>構造函數中傳入,它的作用是whether sub adapters itemTypes are consistent
,就是我們的所有的<子的Adapter>的itemType都是一樣的。因為如果你在<子的Adapter>中沒有覆寫getItemViewType
方法的話,默認都是返回0,即:
public int getItemViewType(int position) {
return 0;
}
我們也知道,RecycleView在運行的時候,執行順序是:
getItemViewType ->onCreateViewHolder ->
getItemViewType ->onCreateViewHolder ->
getItemViewType ->onCreateViewHolder ->...
如果我們的mHasConsistItemType設置為true的話:
所以我們如果所有的<子的Adapter>中的要用同一個viewType的話,比如這里是0,我們就在getItemViewType
方法中執行mItemTypeAry.put(subItemType, p.second);
,這樣當前的這個<子的Apdater>就存在了key為0的集合中了,然后我們在onCreateViewHolder
方法中通過Adapter adapter = mItemTypeAry.get(viewType);
取出來就行了,這時候因為viewType為0,就正好取出來我們剛存的Adapter,然后再進入下一次的getItemViewType
的時候,就用新的adapter覆蓋了key為0的value值,然后再拿到onCreateViewHolder
方法里面使用。
如果我們的mHasConsistItemType設置為false的話:
那這時候就用了另外一種方法,首先,因為子的Adapter默認拿到的ViewType都是0,所以我們用了要設置一個可逆算法,比如A方法和還原的B方法,A方法中我們每次傳入viewType和另外一個值(這里選定了上面我們拿到的Pair<AdapterDataObserver, Adapter>
中的AdapterDataObserver的index值),因為每個<子的Adapter>的index值不同,所以生成的ViewType也不同,然后我們在onCreateViewHolder
方法里面,用還原的B方法,獲取到index值,然后通過這個index再找回<子的Adapter>,這時候我們就可以調用<子的Adapter>的onCreateViewHolder
方法了。
A方法:
private static long getCantor(long k1, long k2) {
return (k1 + k2) * (k1 + k2 + 1) / 2 + k2;
}
B方法:(具體看onCreateViewHolder方法中)
// reverse Cantor Function
int w = (int) (Math.floor(Math.sqrt(8 * viewType + 1) - 1) / 2);
int t = (w * w + w) / 2;
int index = viewType - t;
int subItemType = w - index;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// reverse Cantor Function
int w = (int) (Math.floor(Math.sqrt(8 * viewType + 1) - 1) / 2);
int t = (w * w + w) / 2;
int index = viewType - t;
int subItemType = w - index;
Adapter adapter = findAdapterByIndex(index);
if (adapter == null) {
return null;
}
return adapter.onCreateViewHolder(parent, subItemType);
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Pair<AdapterDataObserver, Adapter> pair = findAdapterByPosition(position);
if (pair == null) {
return;
}
pair.second.onBindViewHolder(holder, position - pair.first.mStartPosition);
pair.second.onBindViewHolderWithOffset(holder, position - pair.first.mStartPosition, position);
}
findAdapterByPosition:(也是二分法查找,和上面的findAdapterByIndex方法一樣,不介紹了。)
@Nullable
public Pair<AdapterDataObserver, Adapter> findAdapterByPosition(int position) {
final int count = mAdapters.size();
if (count == 0) {
return null;
}
int s = 0, e = count - 1, m;
Pair<AdapterDataObserver, Adapter> rs = null;
// binary search range
while (s <= e) {
m = (s + e) / 2;
rs = mAdapters.get(m);
int endPosition = rs.first.mStartPosition + rs.second.getItemCount() - 1;
if (rs.first.mStartPosition > position) {
e = m - 1;
} else if (endPosition < position) {
s = m + 1;
} else if (rs.first.mStartPosition <= position && endPosition >= position) {
break;
}
rs = null;
}
return rs;
}
看了上面我們發現了,最后我們雖然給RecycleView賦值了一個<總的Adapter>,但是實際上的onCreateViewHolder
方法和onBindViewHolder
方法都是調用了每個具體的<子的Adapter>的。
所以我們最終在我們的Activity中的使用
final DelegateAdapter delegateAdapter = new DelegateAdapter(layoutManager, true);
recyclerView.setAdapter(delegateAdapter);
List<DelegateAdapter.Adapter> adapters = new LinkedList<>();
adapters.add(XXXXX);//添加不同的子Adapter.
...
//比如這樣:
GridLayoutHelper layoutHelper;
layoutHelper = new GridLayoutHelper(4);
layoutHelper.setMargin(0, 10, 0, 10);
layoutHelper.setHGap(3);
layoutHelper.setAspectRatio(4f);
adapters.add(new ASubAdapter(this, layoutHelper, 8));
...
...
delegateAdapter.setAdapters(adapters);
如果我想新加一個功能塊,只要新建一個針對這個功能塊的Adapter,然后添加到adapters集合中就可以了。完全不用修改原來的代碼。只需要在這個新加的功能塊的Adapter中處理即可。