大家好,今天又带来了项目中具体遇到的需求。做一个首界面,该首界面有很多功能块,同时这些功能块是动态的,因为登录的人的权限的不同,会显示不同的功能块,因为功能模块的数量不一定,所以当功能块多的时候,整个界面是可以上下滑动的。其实类似有点像淘宝的首界面。如下图所示。
首先我说的不是最优的。可能有更好的解决方式。我这里也只是尝试了新的方式而已。
我先说下我最刚开始想到的最传统的想法:
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实现一种简单的处理。然后再实现更加通用的处理!一定要看完最后的通用处理哦!)
简单处理:
我们首先整个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>
是不是变的干净简洁了!!!
然后我们要使用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中处理即可。