BRAVH是一个recyclerView的adapter,能够快速适配多种类型adapter,可定制,用的人挺多,下面我们就来分析分析他的源码,模拟来写一个我们的adapter。
我们将recyclerView的adapter与自定义viewholder联系在一起,使用了BaseQuickAdapter<T, K extends BaseViewHolder>来作为recycler.adapter
先看BRAVH的BaseViewHolder类
BaseViewHolder extends RecyclerView.ViewHolder
继承自ViewHolder,里面setXXX方法全是由itemview里面的view调用方法实现
存放了一个SparseArray<View> views成员变量用来初始化或者第一次遍历存放引用,添加快捷操作,省去下一次findview的时间
接下来看BaseQuickAdapter类
BaseQuickAdapter<T, K extends BaseViewHolder> extends RecyclerView.Adapter<K>
将K泛型传入给RecyclerView.Adapter作为viewholder
T泛型分析:
传入的数据集List<T>的类型,用来绑定数据
自定义Adapter,Viewholder
我们也来模拟一个adapter类型,使用自定义继承viewHolder的MyViewHolder作为Viewholder
public class BackQuickAdapter<I,VH extends MyViewHolder> extends RecyclerView.Adapter<VH> {
private List<String> datas;
public BackQuickAdapter(List<String> datas) {
this.datas = datas;
}
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list,parent,false);
//实例化VH对象
return createViewHolder(view);
}
@Override
public void onBindViewHolder(VH holder, int position) {
View contentView = holder.itemView;
((TextView)contentView.findViewById(R.id.recycler_list_txt)).setText(datas.get(position));
//do something about contentView
}
@Override
public int getItemCount() {
return datas == null ? 0 : datas.size();
}
}
这样我们就有了一个adapter,里面的Viewholder由外部传进来,我们可以继承该Viewholder自己做快捷操作,让外部继承实现MyViewholder逻辑,同BaseViewHolder,他也只是对itemview引用设置子View参数,这部分基本忽略。我们看onCreateViewHolder->VH createViewHolder(View view)这个方法,这是一个泛型类的实例化,这个直接上代码见git。
给itemView添加加载动画
onViewAttachedToWindow(VH holder)方法:
每次Viewholder添加到window的时候contentView开始动画可以制作item加载效果
想要控制只让itemView进行一次动画,BRAVH里面设置了一个标志位,我们也写一个AnimOnce来做这个标志位,每次加载的时候得到viewholder的位置,并与上一次的加载过的位置比较,如果小,代表当前位置的contentview是新的,需要开启动画,否则如果AnimOnce只要一次,不开启动画,因为动画已经之前被加载过,我们可以这样实现:
/**
* 是否只需要一次item加载动画
*/
private boolean AnimOnce = true;
/**
* 最新加载过动画的item的位置,用于比较下一次item的位置判断是否要加载
*/
private int vaildPos = 0;
@Override
public void onViewAttachedToWindow(VH holder) {
int currentPos = holder.getAdapterPosition();
if(AnimOnce)
if(currentPos > vaildPos){
animateView(holder.itemView);
vaildPos = currentPos;
}
else{
animateView(holder.itemView);
}
super.onViewAttachedToWindow(holder);
}
private void animateView(View root){
root.setAlpha(0.3f);
root.animate().alpha(1).setDuration(2500).start();
}
public void setAnimOnce(boolean animOnce) {
AnimOnce = animOnce;
}
这样,就能添加我们的itemview动画,如果需要外部定制,我们可以修改我们的animateView(View root)方法,给里面添加一个animation动画,并且提供外部接口,比如:
private Animation animation = null;
public void setItemAnimation(Animation animation) {
this.animation = animation;
}
private void animateView(View root){
if(null != animation)
{
root.startAnimation(animation);
}
}
关于动画,可以使用animator或者animation都可以的,只要定制animateView方法
添加头部尾部空数据的布局
我们看BRAVH如何实现,
- 添加header,footer,loading的view
- GetItemCount需要返回的数据量为headercount + datas.size + footercount + loadingcount
- GetItemType如果position < headercount,返回头布局类型,得到普通布局的normal_position = position - headercount如果normal_position < datas.size [List数据集],返回普通列表类型,否则代表有尾布局footer
下面我们也来模拟一个头部尾部布局~
写一个方法来添加头部尾部视图
private int headerLayout;
private int footerLayout;
private boolean hasHeader = false;
private boolean hasFooter = false;
//added first before call setAdapter
public void addHeaderView(@LayoutRes int headerLayout){
this.headerLayout = headerLayout;
hasHeader = true;
}
//added first before call setAdapter
public void addFooterView(@LayoutRes int footerLayout){
this.footerLayout = footerLayout;
hasFooter = true;
}
GetItemType返回对应Type
@Override
public int getItemCount() {
return datas == null ? 0 : datas.size() + (hasFooter ? 1 : 0) + (hasHeader ? 1 : 0);
}
private final static int ITEM_TYPE_HEADER = -1;
private final static int ITEM_TYPE_NORMAL = 0;
private final static int ITEM_TYPE_FOOTER = 1;
@Override
public int getItemViewType(int position) {
int datasize = (datas == null ? 0 : datas.size());
int headerCount = hasHeader ? 1 : 0;
if(position < headerCount)
return ITEM_TYPE_HEADER;
int realIndex = position - headerCount;
if(realIndex < datasize){
return ITEM_TYPE_NORMAL;
}
else{
return ITEM_TYPE_FOOTER;
}
}
根据对于Type构造Viewholder
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
View view = null;
switch (viewType)
{
case ITEM_TYPE_HEADER:
view = LayoutInflater.from(parent.getContext()).inflate(headerLayout, parent, false);
break;
case ITEM_TYPE_FOOTER:
view = LayoutInflater.from(parent.getContext()).inflate(footerLayout, parent, false);
break;
default:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list, parent, false);
}
//实例化VH对象
return createViewHolder(view);
}
根据position位置来绑定viewholder数据
@Override
public void onBindViewHolder(VH holder, int position) {
if(position < (hasHeader ? 1 : 0))
{
// this is header view
return ;
}
int realDataPos = adjustPositionByType(position);
if(realDataPos < datas.size())
{
View contentView = holder.itemView;
((TextView)contentView.findViewById(R.id.recycler_list_txt)).setText(datas.get(realDataPos));
}
else{
//this is footer View
}
}
这样我们的头部尾部布局就添加好了,添加loading布局与空布局也是一个道理,只是多加载了一种类型而已
加载更多的实现
这是在OnBindViewHolder->position判断位置
如果position已经在最后的位置,那么触发加载更多
下面我们可以写一个加载更多的方法:
private void autoLoadMore(int position) {
if(position == getItemCount() -1)
{
Log.i("position","current end is "+String.valueOf(position));
//trigger loading more
}
}
@Override
public int getItemCount() {
return datas == null ? 0 : datas.size() + (hasFooter ? 1 : 0) + (hasHeader ? 1 : 0);
}
实现拖拽,滑动删除
BRAVH是怎么实现呢?官方这样使用:
ItemDragAndSwipeCallback itemDragAndSwipeCallback = new ItemDragAndSwipeCallback(mAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemDragAndSwipeCallback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
// 开启拖拽
mAdapter.enableDragItem(itemTouchHelper, R.id.textView, true);
mAdapter.setOnItemDragListener(onItemDragListener);
// 开启滑动删除
mAdapter.enableSwipeItem();
mAdapter.setOnItemSwipeListener(onItemSwipeListener);
我们可以看到使用的类是ItemDragAndSwipeCallback ,这里面需要传入adapter需要BaseItemDraggableAdapter类型,我们进入ItemDragAndSwipeCallback 发现其实只是BaseItemDraggableAdapter回调用,其实本身并没有用到任何BaseItemDraggableAdapter属性。所以可以做出一个提取的过程,把传入类型BaseItemDraggableAdapter修改成interface,只要RecyclerAdapter实现这个接口就可以了,这样不必非要传入BaseItemDraggableAdapter类型。
这部分也可以直接用原生ItemTouchHelper,复写onMove实现item交换,onSwiped实现Item删除
我们开始写交换逻辑:
- 首先剔除header视图的位置获得在datas数据集中的位置realpos
- 交换数据集中的realpos位置数据
- Notifydatachanged
或许我们可以这样实现
@Override
public void onItemDragMoving(RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
//change item position
int from = getViewHolderPosition(source);
int to = getViewHolderPosition(target);
if (inRange(from) && inRange(to)) {
if (from < to) {
for (int i = from; i < to; i++) {
Collections.swap(datas, i, i + 1);
}
} else {
for (int i = from; i > to; i--) {
Collections.swap(datas, i, i - 1);
}
}
notifyItemMoved(source.getAdapterPosition(), target.getAdapterPosition());
}
}
public int getViewHolderPosition(RecyclerView.ViewHolder viewHolder) {
return viewHolder.getAdapterPosition() - (hasHeader ? 1 : 0);
}
private boolean inRange(int position) {
return position >= 0 && position < datas.size();
}
下面我们实现删除逻辑:
- 首先剔除header视图的位置获得在datas数据集中的位置realpos
- 删除数据集中的realpos位置数据
- NotifyItemRemoved
@Override
public void onItemSwiped(RecyclerView.ViewHolder viewHolder) {
//notify item removed
int position = getViewHolderPosition(viewHolder);
if(position >= 0 && position < datas.size()) {
datas.remove(position);
notifyItemRemoved(getPositionInSets(position));
}
}
public int getPositionInSets(int position) {
return position + (hasHeader ? 1 : 0);
}
自定义使用不同的Item类型
现在默认item类型有header,footer,loading,empty,default。如果想要自定义类型,那么我们可以修改自定义的adapter,在getItemType返回default类型的时候,使用抽象方法让子类实现,修改adapter为抽象类,这将影响:
- getItemtype 使用抽象方法getDefItemViewType(int realDataPos)
- oncreateViewholder 返回的View需要根据自定义itemtype实现自定义view,所以暴露抽象方法onCreateDefViewHolder(ViewGroup parent, int viewType)
- onBindViewHolder 绑定数据时需要根据自定义的ViewHolder来自定视图数据的绑定,所以暴露抽象方法onBindDefViewHolder(VH holder, int realDataPos)
这样我们便能够自定义itemType
getItemtype 使用抽象方法getDefItemViewType(int realDataPos)
public int getItemViewType(int position) {
int datasize = (datas == null ? 0 : datas.size());
int headerCount = hasHeader ? 1 : 0;
if(position < headerCount)
return ITEM_TYPE_HEADER;
int realIndex = position - headerCount;
if(realIndex < datasize){
return getDefItemViewType(realIndex);
}
else{
return ITEM_TYPE_FOOTER;
}
}
//multi item must be override
protected int getDefItemViewType(int realDataPos) {
//datas position item Type
return ITEM_TYPE_NORMAL;
}
暴露抽象方法onCreateDefViewHolder(ViewGroup parent, int viewType)
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
View view = null;
switch (viewType)
{
case ITEM_TYPE_HEADER:
view = LayoutInflater.from(parent.getContext()).inflate(headerLayout, parent, false);
break;
case ITEM_TYPE_FOOTER:
view = LayoutInflater.from(parent.getContext()).inflate(footerLayout, parent, false);
break;
default:
view = onCreateDefViewHolder(parent,viewType);
}
//实例化VH对象
return createViewHolder(view);
}
//be override
abstract View onCreateDefViewHolder(ViewGroup parent, int viewType);
回顾的时候发现还是返回VH类型的Viewholder容易定制,这样自定义实现就不需要泛型实例化方法
abstract VH onCreateDefViewHolder(ViewGroup parent, int viewType);
暴露抽象方法onBindDefViewHolder(VH holder, int realDataPos)
public void onBindViewHolder(VH holder, int position) {
autoLoadMore(position);
if(position < (hasHeader ? 1 : 0))
{
// this is header view
return ;
}
int realDataPos = adjustPositionByType(position);
if(realDataPos < datas.size())
{
onBindDefViewHolder(holder,realDataPos);
}
else{
//this is footer View
}
}
//be override
abstract void onBindDefViewHolder(VH holder, int realDataPos);
我们的ItemType是由传入的数据类型决定的,可以定义一个接口,让传入的数据类型实现该接口并且实现getItemType
public interface ItemType {
int getItemType();
}
public class MultiAdapter<T extends ItemType,VH extends MyViewHolder> extends BackQuickAdapter<T,VH>{
public MultiAdapter(List<T> datas) {
super(datas);
}
@Override
View onCreateDefViewHolder(ViewGroup parent, int viewType) {
return LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list, parent, false);
}
@Override
void onBindDefViewHolder(VH holder, int realDataPos) {
View contentView = holder.itemView;
String args = "自定义数据类型:"+String.valueOf(datas.get(realDataPos).getItemType());
((TextView)contentView.findViewById(R.id.recycler_list_txt)).setText(args);
}
@Override
protected int getDefItemViewType(int realDataPos) {
return datas.get(realDataPos).getItemType();
}
}
这样就可以在外部自定义数据类型了
添加分组
首先我们看BRAVH怎么介绍
- 实体数据集类型要继承SectionEntity
Stop,我们到这边似乎不必要去看他怎么实现了,我猜原理应该和上面添加自定义类型是一样的,上面更加广泛,所以这里我们只有2种类型而已,一种分组头类型。一种分组内容类型。
我们开始编写代码试试:
- 写一个数据集类型实现ItemType接口
- 数据集返回2种类型,第一种是分组头类型假定(GroupHeader),第二种是一种分组内容类型(GroupContent)
- 编写我们的Adapter传入参数类型为1的类型
我们先写一个数据集类型实现ItemType接口
public class GroupEntity implements ItemType{
public final static int GROUPHEADER = 0X11;
public final static int GROUPCONTENT = 0X12;
private boolean isHeader = false;
private String values;
public GroupEntity(boolean isHeader, String values) {
this.isHeader = isHeader;
this.values = values;
}
@Override
public int getItemType() {
return isHeader ? GROUPHEADER : GROUPCONTENT;
}
}
这样添加分组就是2种自定义数据类型而已,我们可以写一个实体类,返回2种类型,使用MultiAdapter适配,实现方法:
- onCreateDefViewHolder:根据viewType来inflate不同的layout的子view
- onBindDefViewHolder:根据Viewholder的VH holder, int position来绑定itemview与数据
分组的伸缩栏
既然要实现分组,我的思路是这样的
- 是否需要提供不同的itemtype,然后根据不同itemtype提供不同级别的子View,这样视图倒是没有问题了。
- 下面需要思考的是数据,如果想要添加到列表的数据集中,之前我们定义的是T类型,那么不同级别也是要是T类型的才能加入到列表的数据集中
- 然后插入notifyitemInserted,删除notifyitemremoved这样就没问题了.
可是设计主实体类或许有些麻烦了,要求里面有个子数据集类型是实体类类型的:
我们先定义接口
public interface Expandable<E> extends ItemType{
List<E> subItems();
boolean isExpandable();
void setExpand(boolean expand);
boolean isExpand();
}
这个接口要求子数据级,是否可以扩展
然后定义数据类型
public class ExpandEntity implements Expandable<ExpandEntity>{
public final static int SUB1 = 0x111;
public final static int SUB2 = 0x112;
// public final static int SUB3 = 0x113;
private int itemType;
private List<ExpandEntity> subDatas;
private String values;
public ExpandEntity(int itemType, List<ExpandEntity> subDatas,String values) {
this.itemType = itemType;
this.subDatas = subDatas;
this.values = values;
}
public String getValues() {
return values;
}
@Override
public int getItemType() {
return itemType;
}
@Override
public List<ExpandEntity> subItems() {
return subDatas;
}
@Override
public boolean isExpandable() {
return subDatas != null && subDatas.size() > 0;
}
boolean isExpand = false;
@Override
public void setExpand(boolean expand) {
isExpand = expand;
}
@Override
public boolean isExpand() {
return isExpand;
}
}
这样就可以实现该接口,外部类自由继承,然后自由添加values属性
下面我们的接口实体类好了,需要制作adapter,可是adapter怎么写呢?我们需要传入的数据类型为Expandable类型,而且数据集合类型要是Expandable的实现类,是否可以这样写?
class ExpandAdapter<I extends Expandable<I>,VH extends MyViewHolder> extends BackQuickAdapter<I,VH>
这样保证数据集市I类型,I又是Expandable类型,这样可以遍历I的子数据集实现多级的展开与隐藏
Adapter具体实现该怎样呢?
我们可以在onBindDefViewHolder方法里面添加itemview的点击事件,然后为itemview添加tag,tag里面是绑定的数据,再实现点击事件的时候取出tag里面数据,判断单项是否可以展开,如果可以展开,得到子数据集放入主数据集中,然后notifyItemRangeInserted就能实现数据的多级展开
void onBindDefViewHolder(VH holder, int realDataPos) {
ExpandEntity entity = (ExpandEntity) datas.get(realDataPos);
((TextView)holder.itemView.findViewById(R.id.recycler_list_txt)).setText(entity.getValues());
holder.itemView.setTag(datas.get(realDataPos));
holder.itemView.setOnClickListener(new ClickDelegate(realDataPos));
}
private class ClickDelegate implements View.OnClickListener{
//当前位置之后插入+headercount
private int position = 0;
public ClickDelegate(int position) {
this.position = position;
}
@Override
public void onClick(View v) {
Expandable<I> raw = (Expandable<I>) v.getTag();
if(raw.isExpandable() && !raw.isExpand()){
//toggle this subItem
//expand or compose
datas.addAll(position+1,raw.subItems());
//from start +2 to datas.size count number
notifyItemRangeInserted(position + 1 + 1, raw.subItems().size());
raw.setExpand(true);
Log.i("itemclick","expand");
}else if(raw.isExpand()){
//这里需要折叠,去除datas中间的数据,多级遍历删除所有当前项的子项
Log.i("itemclick","compose");
}
}
}
注意这里移除操作,去除datas中间的数据,实现折叠效果,然后notifyItemRangeMoved
这部分的逻辑也是对主数据集datas操作,这里没有具体实现,有心的小伙伴可以看BRAVH的expand与collapse方法,人家的折叠可是多级折叠的,将所有子集都移除然后notifyItemRangeMoved。这里我就不写这部分逻辑代码了。
我是分割线
这里我们写Adapter需要涉及到一个泛型的实例化,因为需要将View加入Viewholder,并把Viewholder实例化,而Viewholder又是VH类型的,所以下面方法我直接贴上代码
/**
* 抽象类反射实例化
* @param view
* @return
*/
protected VH createViewHolder(View view) {
Class temp = getClass();
Class z = null;
while (z == null && null != temp) {
z = getInstancedGenericKClass(temp);
temp = temp.getSuperclass();
}
VH k;
// 泛型擦除会导致z为null
if (z == null) {
k = (VH) new MyViewHolder(view);
} else {
k = createGenericKInstance(z, view);
}
return k != null ? k : (VH) new MyViewHolder(view);
}
/**
* try to create Generic K instance
*
* @param z
* @param view
* @return
*/
@SuppressWarnings("unchecked")
private VH createGenericKInstance(Class z, View view) {
try {
Constructor constructor;
// inner and unstatic class
if (z.isMemberClass() && !Modifier.isStatic(z.getModifiers())) {
constructor = z.getDeclaredConstructor(getClass(), View.class);
constructor.setAccessible(true);
return (VH) constructor.newInstance(this, view);
} else {
constructor = z.getDeclaredConstructor(View.class);
constructor.setAccessible(true);
return (VH) constructor.newInstance(view);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
/**
* get generic parameter K
*
* @param z
* @return
*/
private static Class getInstancedGenericKClass(Class z) {
Type type = z.getGenericSuperclass();
if (type instanceof ParameterizedType) {
Type[] types = ((ParameterizedType) type).getActualTypeArguments();
for (Type temp : types) {
if (temp instanceof Class) {
Class tempClass = (Class) temp;
if (MyViewHolder.class.isAssignableFrom(tempClass)) {
return tempClass;
}
}
}
}
return null;
}
这样泛型的实例化就完成了~
又到总结时间啦。我们模拟BRAVH能够学到什么?
我们在写Adapter的时候。将数据集类型以泛型的形式传入。
在Adapter中抽象出onBindViewHolder,onCreateViewHolder,除了处理预置的类型,比如头布局,尾布局,空布局,loading布局。其他的都需要自定义ItemType数据类型来实现定制view。
扩展与折叠也是属于自定义数据类型中的一种,但是要求传入的数据集类型T中还有T类型的子集合,用来得到子集,这样可以保持与Adapter
的数据类型一直,用于展开删除其实就是对Adapter中的数据集datas插入与删除然后通知刷新而已
拖拽与滑动删除默认ItemTouhHelper,在onmoved与onswipe中交换数据集中的位置或者删除某个位置来通知刷新