/ 今日科技快讯 /
近日,百度世界大会2021上,大会首发环节,百度创始人、董事长兼首席执行官李彦宏首次提出了“汽车机器人”概念,并发布了Apollo“汽车机器人”。配置方面,百度汽车机器人能够通过三重能力服务于人,具备L5级自动驾驶能力,其次具备语音、人脸识别等多模交互能力,可分析用户潜在需求,主动提供服务,此外,汽车机器人还具备自我学习和不断升级能力,是服务各种场景的智慧体。
/ 作者简介 /
本篇文章来自首席网管的投稿,文章主要分享了他在研究RecyclerView中多样式Item解决方案的详细过程,相信会对大家有所帮助!
首席网管的博客地址:
https://www.jianshu.com/u/e81db6c18dd0
/ 介绍一下 /
MultiAdapter是一个轻松优雅实现RecyclerView多样式的强大组件!它将item的行为及表现抽象为一个ItemType,不同类型的item都有着自己独立的点击事件处理及视图绑定行为,极大地降低了耦合度,采用反射及泛型技术极大地简化了item相关点击事件处理过程。
其内部封装了若干实用的工具组件以满足RecyclerView日常需求,如列表的单选/多选。正是因为有了上述功能支持,我们在给RecyclerView添加头布局、脚布局、嵌套RecyclerView布局的时候,就简单的太多了!
依赖详见GitHub地址,如下所示:
https://github.com/censhengde/MultiAdapter
/ 用法(原理在后面讲) /
列表Item多样式实现
先看成品效果,如图:
使用步骤如下所示。
Step 1 创建item的实体类
ItemBean.java
public class ItemBean {
//所有Item类型都在这里定义
public static final int TYPE_A = 0;
public static final int TYPE_B = 1;
public static final int TYPE_C = 2;
public int id;
//Item类型标识(很关键!)
public int viewType;
//item具体业务数据字段
public String text = "";
public ItemBean(int viewType, String text) {
this.viewType = viewType;
this.text = text;
}
public ItemBean(int id, int viewType, String text) {
this.viewType = viewType;
this.text = text;
this.id = id;
}
}
ItemBean的关键点就是对viewType字段与TYPE_A、TYPE_B、TYPE_C标识位的理解,viewType字段表示当前item实体对象所要表现的item样式,比如当viewType=TYPE_A时,表示该ItemBean实例想表现A类型Item 样式布局,其他同理。
总而言之,这里秉持一个理念,那就是RecyclerView某position上所表现的item样式由item实体对象决定,切记!后面讲原理时候会再次提到这个理念。(注意:给item实体类添加viewType字段用于指示其表现的item类型是典型用法之一,但并不唯一!)
Step 2 声明各个item类型布局文件
item_a.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#FF5722"
android:orientation="vertical">
<TextView
android:id="@+id/tv_a"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:textSize="18sp"
tools:text="A 类 Item" />
</LinearLayout>
item_b.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:background="@color/colorAccent"
android:layout_height="120dp">
<Button
android:id="@+id/btn_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@android:color/transparent"
android:gravity="center"
android:text="Button"
android:textAllCaps="false"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:gravity="center"
android:text="B 类 Item"/>
</LinearLayout>
item_c.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:background="#9C27B0"
android:layout_height="150dp">
<TextView
android:id="@+id/tv_c"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:gravity="center"
android:layout_gravity="center|center_vertical"
android:text="C 类 Item"/>
<ImageView
android:id="@+id/iv_c"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="5dp"
android:src="@mipmap/ic_launcher"/>
</LinearLayout>
Step 3 创建各个item类型的ItemType实现类
AItemType.java
public class AItemType extends MultiItemType<ItemBean> {
/**
* @param data 当前position对应的实体对象
* @param position
* @return true 表示成功匹配到对应的ItemType
*/
@Override
public boolean matchItemType(@Nullable ItemBean data, int position) {
return data == null || ItemBean.TYPE_A == data.viewType;//这句话的含义是:当前position 的ItemBean想要表现的item类型是哪一种,
//以本例为例,会依次遍历A、B、C三个Item类型,直到返回true为止。(详见MultiHelper getItemViewType方法实现)
}
/**
* @return 返回当前item类型的布局文件
*/
@Override
public int getItemLayoutRes() {
return R.layout.item_a;
}
/**
* 给当前item类型布局视图设置数据,意义基本与RecyclerView.Adapter onBindViewHolder 相同。
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull MultiViewHolder holder,
@NonNull MultiHelper<ItemBean, MultiViewHolder> helper,
int position) {
ItemBean itemBean = helper.getItem(position);
if (itemBean != null) {
TextView tv = holder.getView(R.id.tv_a);
tv.setText(itemBean.text);
}
}
}
BItemType.java
public class BItemType extends MultiItemType<ItemBean> {
@Override
public boolean matchItemType(ItemBean data, int position) {
return ItemBean.TYPE_B == data.viewType;
}
@Override
public int getItemLayoutRes() {
return R.layout.item_b;
}
@Override
public void onBindViewHolder(@NonNull MultiViewHolder holder,
@NonNull MultiHelper<ItemBean,MultiViewHolder> helper,
int position) {
ItemBean data = helper.getItem(position);
if (data != null) {
TextView tv = holder.getView(R.id.tv_b);
tv.setText(data.text);
}
}
}
CItemType.java
public class CItemType extends MultiItemType<ItemBean> {
@Override
public boolean matchItemType(ItemBean data, int position) {
return ItemBean.TYPE_C == data.viewType;
}
@Override
public int getItemLayoutRes() {
return R.layout.item_c;
}
@Override
public void onBindViewHolder(@NonNull MultiViewHolder holder,
@NonNull MultiHelper<ItemBean, MultiViewHolder> helper,
int position) {
ItemBean bean = helper.getItem(position);
if (bean == null) {
return;
}
TextView tvC = holder.getView(R.id.tv_c);
tvC.setText(bean.text);
}
}
ItemType是本项目的核心概念之一,如前文所说,ItemType是一类item的抽象,其拥有独立的视图绑定和点击事件处理过程,并且它接管了RecyclerView.Adapter的生命周期业务;ItemType是一个接口,MultiItemType是其子类,实现了item view点击事件回调等一系列核心功能,具体说明后面讲原理时候再详说。一种类型item对应一个ItemType,切记!先简单说明下各个方法含义。
public boolean matchItemType: 判断当前 position 的item样式是否对应当前的ItemType。如此例判断的依据就是实体对象的viewType字段取值。(这个方法是实现RecyclerView item多样式的核心,单样式item无需重写此方法,具体含义后面会再讲。)
public int getItemLayoutRes:返回该类item的布局资源文件id。
public void onBindViewHolder:视图数据绑定。含义基本与RecyclerView Adapter 的 onBindViewHolder方法相同,不同的是ItemType的这个方法仅进行当前item类型的视图数据绑定。MultiViewHolder是内部封装的通用的ViewHolder。
Step 4 在Activity里的初始化
创建Activity布局文件activity_multi_item.xml。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tencent.multiadapter.example.ui.MultiItemActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</RelativeLayout>
在Activity onCreate方法中初始化RecyclerView。
class MultiItemActivity : AppCompatActivity() {
lateinit var adapter: MultiAdapter<ItemBean, MultiViewHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_item)
//初始化ItemType
val aItemType = AItemType()
val bItemType = BItemType()
val cItemType = CItemType()
/*初始化Adapter*/
adapter = MultiAdapter<ItemBean, MultiViewHolder>()
/*将所有ItemType添加到Adapter中*/
adapter.addItemType(aItemType)
.addItemType(bItemType)
.addItemType(cItemType)
/*设置数据*/
adapter.setData(getData())
rv_list.adapter = adapter
}
/**
* 模拟数据
*/
private fun getData(): List<ItemBean> {
val beans = ArrayList<ItemBean>()
for (i in 0..5) {
beans.add(ItemBean(ItemBean.TYPE_A, "我是A类Item$i"))
beans.add(ItemBean(ItemBean.TYPE_B, "我是B类Item${i + 1}"))
beans.add(ItemBean(ItemBean.TYPE_C, "我是C类Item${i + 2}"))
}
return beans
}
}
至此,点击运行就可以看到图2.0.1的效果了!
在Step 4 的基础上 给item view设置点击事件监听
与item相关的点击监听方案支持反射和监听器两种方式。本文主要介绍反射方式。
反射方案是参考了系统的 xml android:onClick="目标方法名" 实现思路,但View并没有对外提供获取 android:onClick属性值的方法,故“目标方法名”需另辟蹊径传入,由于ItemType拥有独立的点击事件处理过程,所以在其实现类AbstractItemType中提供了registerItemViewClickListener和registerItemViewLongClickListener等工具方法用以注册指定item view点击事件监听。方法原型及其参数说明如下:
protected final void registerItemViewClickListener(@NonNull VH holder,//当前item类型的ViewHolder。
@NonNull MultiHelper<T, VH> helper,//帮助类对象,通过它可以访问到item实体对象。
@Nullable String target,//目标方法名,可null,当采用监听器方式时,就传null。
@IdRes int... viewIds)//item 指定view 的id,可同时指定多个,因为存在多个view响应同一套逻辑的情况。
//不传id时默认是给item根布局设置监听。
例如:给 A类item 的item view设置监听。
这里先回顾一下我们使用原生RecyclerView Adapter给item设置点击事件监听的过程,大致就是在onCreateViewHolder方法中xxxxxxxxx一顿操作......还有的朋友在onBindViewHolder方法中获取指定view再xxxxxx一顿操作(这里顺带提一嘴,在onBindViewHolder方法进行事件监听设置绝对是不专业的!)......如前文所说,ItemType接管了Adapter生命周期,我们先看ItemType接口声明。
ItemType.java
public interface ItemType<T, VH extends RecyclerView.ViewHolder> {
/**
* 当前position 是否匹配当前的ItemType
*
* @param data 当前position对应的实体对象,当是依赖paging3 getItem()方法返回时,有可能为null。
* @param position adapter position
* @return true 表示匹配,false:不匹配。
*/
boolean matchItemType(@Nullable T data, int position);
/**
* 创建当前ItemType的ViewHolder
*
* @param parent parent
* @return ViewHolder
*/
@NonNull
VH onCreateViewHolder(@NonNull ViewGroup parent);
/**
* ViewHolder已经创建完成,在这里可以注册Item及其子View点击事件监听器,但不要做数据的绑定。
*/
void onViewHolderCreated(@NonNull VH holder, @NonNull MultiHelper<T, VH> helper);
/**
* 意义与Adapter onBindViewHolder 基本相同,表示当前ItemType的数据绑定过程。
*
* @param holder
* @param position
*/
void onBindViewHolder(@NonNull VH holder, @NonNull MultiHelper<T, VH> helper, int position,
@NonNull List<Object> payloads) throws Exception;
void onBindViewHolder(@NonNull VH holder, @NonNull MultiHelper<T, VH> helper, int position) throws Exception;
}
ItemType生命周期是以RecyclerView Adapter生命周期为前提的,如果有对RecyclerView adapter相关方法调用流程还不熟的朋友,建议先去找资料研究一下。
既然ItemType接管了adapter生命周期,那其所有方法必定都对应在adapter几个关键方法中被调用,其中在adapter onCreateViewHolder方法中调用到的ItemType的方法有:onCreateViewHolder方法、onViewHolderCreated方法,这里可能有朋友有疑问:为什么要把adapter onCreateViewHolder方法拆分为ItemType的两个阶段方法?
答案就是出于职责单一编程原则考虑。创建ViewHolder就是创建ViewHolder,给ViewHolder view设置监听或者进行其他操作又是另一回事了。所以最终我们是在ItemType onViewHolderCreated 方法给item相关view设置监听。
在AItemType中重写onViewHolderCreated方法。
public class AItemType extends MultiItemType<ItemBean> {
/**
* @param data 当前position对应的实体对象
* @param position
* @return true 表示成功匹配到对应的ItemType
*/
@Override
public boolean matchItemType(@Nullable ItemBean data, int position) {
return data == null || ItemBean.TYPE_A == data.viewType;//这句话的含义是:当前position 的ItemBean想要表现的item类型是哪一种,
//以本例为例,会依次遍历A、B、C三个Item类型,直到返回true为止。(详见MultiHelper getItemViewType方法实现)
}
/**
* @return 返回当前item类型的布局文件
*/
@Override
public int getItemLayoutRes() {
return R.layout.item_a;
}
/**
* 表示ViewHolder已经创建完成。本方法最终是在RecyclerView.Adapter onCreateViewHolder方法中被调用,
* 所以所有的与item相关的点击事件监听器都应在这里注册。
*
* @param holder
* @param helper
*/
@Override
public void onViewHolderCreated(@NonNull MultiViewHolder holder,
@NonNull MultiHelper<ItemBean, MultiViewHolder> helper) {
/*注册监听器,不传viewId则默认是给item根布局注册监听*/
registerItemViewClickListener(holder, helper, "onClickItem");
}
/**
* 给当前item类型布局视图设置数据,意义基本与RecyclerView.Adapter onBindViewHolder 相同。
*
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull MultiViewHolder holder,
@NonNull MultiHelper<ItemBean, MultiViewHolder> helper,
int position) {
ItemBean itemBean = helper.getItem(position);
if (itemBean != null) {
TextView tv = holder.getView(R.id.tv_a);
tv.setText(itemBean.text);
}
}
}
在Activity onCreate方法调用AItemType inject方法注入事件接收者。
class MultiItemActivity : AppCompatActivity() {
lateinit var adapter: MultiAdapter<ItemBean, MultiViewHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_item)
//初始化ItemType
val aItemType = AItemType()
//注入事件接收对象
aItemType.inject(this)
val bItemType = BItemType()
val cItemType = CItemType()
bItemType.inject(this)
cItemType.inject(this)
/*初始化Adapter*/
adapter = MultiAdapter<ItemBean, MultiViewHolder>()
/*将所有ItemType添加到Adapter中*/
adapter.addItemType(aItemType)
.addItemType(bItemType)
.addItemType(cItemType)
/*设置数据*/
adapter.setData(getData())
rv_list.adapter = adapter
}
//......
}
在Activity中声明目标方法(注意,方法名一定要与刚才传入的target值对应!参数列表顺序不能乱!方法访问修饰符任意。)
/**
*item点击事件
*/
private fun onClickItem(view: View, itemBean: ItemBean, position: Int) {
Toast.makeText(this, "ItemBean:${itemBean.text},position:$position", Toast.LENGTH_SHORT).show()
}
附Activity完整代码:
class MultiItemActivity : AppCompatActivity() {
lateinit var adapter: MultiAdapter<ItemBean, MultiViewHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_item)
//初始化ItemType
val aItemType = AItemType()
//注入事件接收对象
aItemType.inject(this)
val bItemType = BItemType()
val cItemType = CItemType()
bItemType.inject(this)
cItemType.inject(this)
/*初始化Adapter*/
adapter = MultiAdapter<ItemBean, MultiViewHolder>()
/*将所有ItemType添加到Adapter中*/
adapter.addItemType(aItemType)
.addItemType(bItemType)
.addItemType(cItemType)
/*设置数据*/
adapter.setData(getData())
rv_list.adapter = adapter
}
/**
* 模拟数据
*/
private fun getData(): List<ItemBean> {
val beans = ArrayList<ItemBean>()
for (i in 0..5) {
beans.add(ItemBean(ItemBean.TYPE_A, "我是A类Item$i"))
beans.add(ItemBean(ItemBean.TYPE_B, "我是B类Item${i + 1}"))
beans.add(ItemBean(ItemBean.TYPE_C, "我是C类Item${i + 2}"))
}
return beans
}
/**
*item点击事件
*/
private fun onClickItem(view: View, itemBean: ItemBean, position: Int) {
Toast.makeText(this, "ItemBean:${itemBean.text},position:$position", Toast.LENGTH_SHORT).show()
}
}
完毕,点击运行之。效果如图:
其他item相关点击事件如item子view点击事件、长点击事件等监听实现同理,详见工程用例。采用反射方式实现item点击监听务必注意代码混淆后无法找到目标方法的问题,这里附上两种避免方式。
在module 的proguard-rules.pro文件配置
-keepclassmembers class com.tencent.multiadapter.example.ui.MultiItemActivity{ private void onClickItem(...); }
在方法声明上标记@Keep注解(推荐)
/**
*item点击事件
*/
@Keep
private fun onClickItem(view: View, itemBean: ItemBean, position: Int) {
Toast.makeText(this, "ItemBean:${itemBean.text},position:$position", Toast.LENGTH_SHORT).show()
}
列表单选/多选实现
列表单/多选实现主要依靠CheckingHelper核心类来实现。MultiAdapter集成了CheckingHelper。其核心api说明如下:
void checkItem(int position,@Nullable Object payload):选中item。position:当前位置;payload:用于局部刷新的参数,与 RecyclerView.Adapter notifyItemChanged方法的意义相同。
void uncheckItem(int position,@Nullable Object payload):取消选中item。与checkItem相反。
void checkAll(@Nullable Object payload):全选。payload:同上。
void cancelAll(@Nullable Object payload):取消全选。
void setOnCheckingFinishedCallback(OnCheckingFinishedCallback<T> callback):设置完成选择后的回调接口。OnCheckingFinishedCallback<T> 接口方法说明如下:
public interface OnCheckingFinishedCallback<T> {
/**
* @param checked 被选中的Item集合
*/
void onCheckingFinished(@NonNull List<T> checked);
}
6.void finishChecking():完成选择。调用这个方法将触发OnCheckingFinishedCallback接口 回调。
一般的简单列表选择是当列表是单样式item的时候,实现比较简单,这里先不介绍。我们重点关注一下当列表是多样式item的时候(如当列表存在头布局脚布局的时候),我们应该如何排除掉无效item。如图:
某一条item的选中状态,它是否是符合我们预期的,需要定义一个符合我们预期的规则,比如当列表存在头布局脚布局的时候,我们点击全选,最后只有中间的item是可选中的,点击完成,最后只有被选中的item集合回调出来,这才符合我们的预期。
某一item是否是可选的,以及它符合什么样的规则才被认为是选中的,我们抽象出一个接口,名叫Checkable,意为可选的。CheckingHelper将会依据这个接口做统一判断处理item。Checkable声明如下:
public interface Checkable {
/*设置是否被选中,注意,复杂的item是否被选中规则一定要注意此方法的实现,
*不要局限于单纯搞个boolean 变量做判断。
*/
void setChecked(boolean checked);
/*判断是否被选中*/
boolean isChecked();
}
现在我们开始实现复杂列表多选功能。
实现步骤如下所示。
step 1:新建item实体类CheckableItem.java,并实现Checkable接口
代码如下:
public class CheckableItem implements Checkable {
public static final int VIEW_TYPE_HEADER=1;/*头布局标识位*/
public static final int VIEW_TYPE_CHECKABLE = 0;/*可选中的Item标识位*/
public static final int VIEW_TYPE_FOOTER=2;/*脚布局标识位*/
public int viewType = VIEW_TYPE_CHECKABLE;/*默认是可选中item*/
private boolean mIsChecked;/*判断当前item是否被选中*/
public String text="";
public CheckableItem(int viewType, String text) {
this.viewType = viewType;
this.text = text;
}
@Override
public void setChecked(boolean checked) {
/*头布局和脚布局是不可选的。注意,只有这里的被选中规则定义得准确,
*后面调用CheckingHelper finishedChecking才能准确甄选出被选中的item
*/
mIsChecked = checked && viewType == VIEW_TYPE_CHECKABLE;
}
@Override
public boolean isChecked() {
return mIsChecked;
}
}
注意看setChecked方法的实现,这里再次强调一个理念:RecyclerView 某position上的item所表现的样式是由item实体对象决定的。这个理念的另一个解读含义是:尽管RecyclerView item是多样式的,但外层实体类的类型是一致的!
当用户点击全选时,所有类型item实体类(Checkable类型)的setChecked方法被调用且checked参数都传入true,当用户点击完成时,所有类型item实体类(Checkable类型)的isChecked方法被调用,依据其返回值最终判断当前item最终选中状态。
step 2:编写各类item布局文件
头布局:item_checking_header.xml。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_gravity="center"
android:text="头布局"/>
</FrameLayout>
可选的item布局:item_checking_checkable.xml。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="120dp">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_centerVertical="true"
android:layout_marginStart="50dp"/>
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="20dp" />
</RelativeLayout>
脚布局:item_checking_footer.xml。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/darker_gray">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_gravity="center|top"
android:textColor="@color/colorAccent"
android:text="脚布局"/>
</FrameLayout>
step 3:创建各个item类型的ItemType实现类
头布局:HeaderItemType.kt。
class HeaderItemType:MultiItemType<CheckableItem>() {
override fun getItemLayoutRes(): Int = R.layout.item_checking_header
override fun matchItemType(data: CheckableItem, position: Int): Boolean
=data.viewType== CheckableItem.VIEW_TYPE_HEADER
override fun onBindViewHolder(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>, position: Int) {
}
}
可选item布局:CheckableItemType.kt。
class CheckableItemType : MultiItemType<CheckableItem>() {
override fun getItemLayoutRes(): Int = R.layout.item_checking_checkable
override fun matchItemType(data: CheckableItem, position: Int): Boolean = data.viewType == CheckableItem.VIEW_TYPE_CHECKABLE
override fun onViewHolderCreated(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>) {
registerItemViewClickListener(holder,helper,"onClickItem")
}
/**
* 只有局部刷新才会回调到这里,RecyclerView上下滑动则不会,有区别于RecyclerView.Adapter中的实现.
*/
override fun onBindViewHolder(holder: MultiViewHolder,
helper: MultiHelper<CheckableItem,MultiViewHolder>,
position: Int,
payloads: MutableList<Any>) {
payloads.forEach {
if (it is Int)
if (it == R.id.checkbox) {
val item = helper.getItem(position) ?: return
val checkbox = holder.getView<CheckBox>(R.id.checkbox)
checkbox.isChecked = item.isChecked
}
}
}
override fun onBindViewHolder(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>, position: Int) {
val item = helper.getItem(position) ?: return
val tv = holder.getView<TextView>(R.id.tv)
tv.text = item.text
//CheckBox
val checkbox = holder.getView<CheckBox>(R.id.checkbox)
checkbox.isChecked = item.isChecked
}
}
注意图中两个onBindViewHolder重载方法的实现,清楚RecyclerView item局部刷新的朋友应该了解这两个方法的用法及区别,本文不做赘述。
脚布局:FooterItemType.kt。
class FooterItemType:MultiItemType<CheckableItem>() {
override fun getItemLayoutRes(): Int = R.layout.item_checking_footer
override fun matchItemType(data: CheckableItem, position: Int): Boolean =data.viewType == CheckableItem.VIEW_TYPE_FOOTER
override fun onBindViewHolder(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>, position: Int) {
}
}
step 4:在activity初始化
class CheckItemActivity : AppCompatActivity(), OnCheckingFinishedCallback<CheckableItem> {
val adapter = MultiAdapter<CheckableItem,MultiViewHolder>()
val dataSize = 30
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_check_item)
val checkableItemType = CheckableItemType()
/*注册item点击监听*/
checkableItemType.inject(this)
/*添加ItemType*/
adapter.addItemType(HeaderItemType())
.addItemType(checkableItemType)
.addItemType(FooterItemType())
//设置完成选择的回调
adapter.checkingHelper.setOnCheckingFinishedCallback(this)
adapter.setData(getData())
rv_list.adapter = adapter
}
/*模拟数据(页面状态的改变可能会导致列表选择状态丢失,建议在ViewModel或其他序列化手段保存数据以便恢复列表选择状态)
* */
private fun getData(): MutableList<CheckableItem> {
val data = ArrayList<CheckableItem>(dataSize + 2)
/*头布局item 实体对象*/
data.add(CheckableItem(CheckableItem.VIEW_TYPE_HEADER, ""))
/*中间可选的item实体对象*/
for (i in 0 until dataSize) {
data.add(CheckableItem(CheckableItem.VIEW_TYPE_CHECKABLE, "可选的Item position=${i}"))
}
/*脚布局item实体对象*/
data.add(CheckableItem(CheckableItem.VIEW_TYPE_FOOTER, ""))
return data
}
/*点击完成*/
fun onClickFinished(view: View) {
adapter.checkingHelper.finishChecking()
}
/*点击全选、取消*/
fun onClickCheckAll(view: View) {
val btn = (view as Button)
when (btn.text) {
"全选" -> {
btn.text = "取消"
adapter.checkingHelper.checkAll(R.id.checkbox)
}
"取消" -> {
btn.text = "全选"
adapter.checkingHelper.cancelAll(R.id.checkbox)
}
}
}
/*点击可选的item*/
private fun onClickItem(view: View, item: CheckableItem, position: Int) {
if (item.isChecked) {
adapter.checkingHelper.uncheckItem(position, R.id.checkbox)
} else {
adapter.checkingHelper.checkItem(position, R.id.checkbox)
}
/*当你想实现列表单选时,请调用adapter.checkingHelper.singleCheckItem(position, R.id.checkbox)*/
}
/*点击完成时的数据回调*/
override fun onCheckingFinished(checked: List<CheckableItem>) {
checked.forEach {
Log.e("被选中的item:", it.text)
}
}
}
这里注意payload参数要与CheckableItemType onBindViewHolder 3参数方法中的对应。完毕,点击运行之。
/ 扩展 /
MultiAdapter库由于其内部组件的高度解耦性,可将其复用于其它RecyclerView.Adapter。当我们想要实现RecyclerView分页功能的时候、也许我们会选择Google 的paging1、2、3的解决方案,这时候MultiAdapter库提供的MultiAdapter将不再适用,但我们可以模仿MultiAdapter的构建过程,利用MultiHelper、CheckingHelper组件轻松地完成其他任意RecyclerView.Adapter改造。
以改造paging3的PagingDataAdapter为例,代码如下:
open class MultiPagedAdapter<T : Any,VH :RecyclerView.ViewHolder>(diffCallback: DiffUtil.ItemCallback<T>)
: PagingDataAdapter<T, VH>(diffCallback) {
val multiHelper = object : MultiHelper<T,VH>(this) {
override fun getItem(p0: Int): T? {
return this@MultiPagedAdapter.getItem(p0)
}
}
val checkingHelper = object : CheckingHelper<T>(this) {
override fun getItem(position: Int): T? = this@MultiPagedAdapter.getItem(position)
override fun getDataSize(): Int = this@MultiPagedAdapter.itemCount
}
override fun getItemViewType(position: Int): Int {
return multiHelper.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return multiHelper.onCreateViewHolder(parent, viewType)
}
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any?>) {
multiHelper.onBindViewHolder(holder, position, payloads)
}
override fun onBindViewHolder(holder: VH, position: Int) {
}
}
可以看到,MultiHelper的本质就是代理的Adapter的生命周期。这里注意onBindViewHolder方法,我们仅需要代理上面3参数的即可,因为内部做了处理,让执行流程直接调用到ItemType的两个重载onBindViewHolder方法。
/ 原理篇 /
多样式item实现的核心逻辑封装在了MultiHelper类中,MultiHelper本质是接管了RecyclerView.Adapter生命周期,代理了RecyclerView.Adapter的getItemViewType、onCreateViewHolder与onBindViewHolder三个核心方法,并将其生命周期事件分发给了position对应的ItemType,最终转换成了ItemType的生命周期。
(有对RecyclerView.Adapter生命周期流程不熟的同学建议先去了解一下,网上博客资料都有的)
紧接着就到了MultiHelper这个关键主角登场了!先来粗略瞄一眼它的源码:
public abstract class MultiHelper<T, VH extends RecyclerView.ViewHolder> {
public static final int INVALID_VIEW_TYPE = -1;
private final RecyclerView.Adapter realAdapter;
/**
* ItemType集合,以其id为key,ItemType为value。
*/
private final SparseArray<ItemType<T, VH>> mItemTypes = new SparseArray<>();
/**
* ItemType在Adapter position上的对应记录,其索引与Adapter position一一对应,
* 表示某position想要表现的ItemType,
* 注意,并非一定与数据集的index对应。
*/
private List<ItemType<T, VH>> mItemTypeRecord;
public MultiHelper(Adapter realAdapter) {
this.realAdapter = realAdapter;
}
public int getItemCount() {
return realAdapter == null ? 0 : realAdapter.getItemCount();
}
public final int getItemViewType(int position) {
if (position == RecyclerView.NO_POSITION) {
return INVALID_VIEW_TYPE;
}
ItemType<T, VH> currentType = null;
final int typeSize = mItemTypes.size();
//单样式
if (typeSize == 1) {
currentType = mItemTypes.valueAt(0);
}
//多样式
else if (typeSize > 1) {
final T data = getItem(position);
//mItemTypeRecord 初始化在这里进行
if (mItemTypeRecord == null) {
mItemTypeRecord = new ArrayList<>();
}
if (position >= 0 && position < mItemTypeRecord.size()) {
currentType = mItemTypeRecord.get(position);
//如果当前 position 对应的ItemType不再与旧的ItemType匹配,
// 说明当前 position 对应的ItemType已经被更改,在RecyclerView列表进行增、删、改操作
// 时候可能会出现这种情况,这时候就需重新匹配当前position所对应的ItemType
if (!currentType.matchItemType(data, position)) {
final ItemType<T, VH> newType = findCurrentType(data, position);
if (newType != null) {
currentType = newType;
mItemTypeRecord.set(position, newType);
}
}
}
//首次进来 mItemTypeRecord.isEmpty() 为true,会走这里。
else {
currentType = findCurrentType(data, position);
if (currentType != null) {
mItemTypeRecord.add(currentType);
}
}
}
return currentType == null ? INVALID_VIEW_TYPE : currentType.getClass().hashCode();
}
/**
* 遍历查找当前position对应的ItemType。
*
* @param data
* @param position
* @return
*/
@Nullable
private ItemType<T, VH> findCurrentType(T data, int position) {
//为当前position 匹配它的ItemType
for (int i = 0; i < mItemTypes.size(); i++) {
final ItemType<T, VH> type = mItemTypes.valueAt(i);
if (type.matchItemType(data, position)) {
return type;
}
}
return null;
}
@NotNull
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final ItemType<T, VH> type = mItemTypes.get(viewType);
if (viewType == INVALID_VIEW_TYPE || type == null) {//表示无效
throw new IllegalStateException("ItemType 不合法:viewType==" + viewType + " ItemType==" + type);
}
final VH holder = type.onCreateViewHolder(parent);
type.onViewHolderCreated(holder, this);
return holder;
}
public final void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) {
/*统一捕获由position引发的可能异常*/
try {
final int size = mItemTypes.size();
ItemType<T, VH> type;
if (size == 1) {/*单样式*/
type = mItemTypes.valueAt(0);
} else if (size > 1) {/*多样式*/
/*可能有越界风险*/
type = mItemTypeRecord.get(position);
} else {
return;
}
if (payloads.isEmpty()) {
type.onBindViewHolder(holder, this, position);
}
/*局部刷新*/
else {
type.onBindViewHolder(holder, this, position, payloads);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Nullable
public abstract T getItem(int position);
/**
* 绝大多数情况下,用户不需要主动维护ItemTypeRecord集合,
* 即便当对item进行增、删、改(伴随数据集元素的增删改)。但用户应当拥有主动维护这个集合的权力。
* @return ItemType记录。当RecyclerView是单样式item的时候返回null。
*/
@Nullable
public final List<ItemType<T, VH>> getItemTypeRecord() {
return mItemTypeRecord;
}
/**
* 注册ItemType
* @param type
* @return
*/
public final MultiHelper<T, VH> addItemType(ItemType<T, VH> type) {
if (type == null) {
return this;
}
mItemTypes.put(type.getClass().hashCode(), type);
return this;
}
}
如前文所说,MultiHelper类代理了RecyclerView Adapter,为什么采用这种设计模式呢???答案就是为了之后改造任意RecyclerView Adapter达到代码复用的目的,如前文改造goole paging3 PagingDataAdapter 就是个例子。RecyclerView有很多框架组件,不同框架组件可能有不同的Adapter实现,我们需要考虑兼容人家的东西。
可以看到,这里又一大堆与RecyclerView Adapter相似的方法,注意,这里既然是代理Adapter,那这些方法及其参数含义就肯定与Adapter的一致了!
先解释几个核心字段:
mItemTypes:SparseArray类型,以ItemType getViewType方法返回值为key,ItemType 实例为value进行存储。
换句话说就是:有多少种item类型就有多少个ItemType实例被存储。单样式RecyclerView就只有一个ItemType,理解了有没有?
mItemTypeRecord:List类型,元素是ItemType,什么作用?记录Adapter position 对应的ItemType,其index与position是对应的,换句话说就是可以通过position访问到对应的ItemType,理解这个字段非常关键,它是实现整个功能的灵魂!它所表达的position、index和ItemType对应关系用一张图来表示:
用文字来解释图中的关系就是:RecyclerView adapter position 为0时的item类型是A类型,以此类推,当position为3时又是A类型item......
看到这个关系对应图可能有朋友提出疑惑,要是RecyclerView频繁进行item增删(伴随数据集元素的增删),图中的关系能否依旧正确表达?问题不大,注意看MultiHelper getItemViewType方法关键代码段:
final T data = getItem(position);
//......
if (position >= 0 && position < mItemTypeRecord.size()) {
currentType = mItemTypeRecord.get(position);
//如果当前 position 对应的ItemType不再与旧的ItemType匹配,
// 说明当前 position 对应的ItemType已经被更改,在RecyclerView列表进行增、删、改操作
// 时候可能会出现这种情况,这时候就需重新匹配当前position所对应的ItemType
if (!currentType.matchItemType(data, position)) {
//重新匹配当前position对应的ItemType。
final ItemType<T, VH> newType = findCurrentType(data, position);
if (newType != null) {
currentType = newType;
mItemTypeRecord.set(position, newType);
}
}
}
这里用示意图来表达ItemTypeRecord自动维护过程:
如图1是RecyclerView列表最初的对应关系,现移除position 为1对应的item(伴随数据集对应元素的移除),得到图2的对应关系。
能命中if(position >= 0 && position < mItemTypeRecord.size())这个条件说明RecyclerView 列表已经初始化过了,mItemTypeRecord也已经有了最初的记录(如图1的记录),现在 position为1对应item已经被移除(伴随数据集对应元素移除),那么调用getItem方法获取到的实体对象是bean 2,但此时调用 mItemTypeRecord.get(position)方法获取到的ItemType依旧 原来的B类型,如图2,此时 if (!currentType.matchItemType(data, position)) 条件将会成立,接着调用findCurrentType(data, position)方法重新为bean 2匹配ItemType,看一下findCurrentType(data, position)方法源码:
@Nullable
private ItemType<T, VH> findCurrentType(T data, int position) {
//为当前position 匹配它的ItemType
for (int i = 0; i < mItemTypes.size(); i++) {
final ItemType<T, VH> type = mItemTypes.valueAt(i);
if (type.matchItemType(data, position)) {
return type;
}
}
return null;
}
可以看到,其实就是遍历mItemTypes集合,逐个调用matcItemType()方法进行判断。此时一定会再次匹配到C类型ItemType并返回,接着调用mItemTypeRecord.set(position, newType)更新当前position对应的ItemType,这里可以看到mItemTypeRecord记录是有自动更新能力的,用户不必担心item的增删改会导致item样式表现错乱问题。
这里又再次强调了那个理念:RecyclerView 某position 对应的item 所表达的类型由其实体对象决定!实体对象怎么决定的?请回顾前文ItemBean 的 int 类型的viewType字段、ItemType的 matchItemType() 方法的作用!!!你品!你细细品!!!
经此getItemViewType方法调用,ItemTypeRecord关系已经确定,进而得到position对应的viewType值,再返回,接着RecyclerView Adapter生命周期就走到了 onCreateViewHolder 方法:
@NotNull
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final ItemType<T, VH> type = mItemTypes.get(viewType);
if (viewType == INVALID_VIEW_TYPE || type == null) {//表示无效
throw new IllegalStateException("ItemType 不合法:viewType==" + viewType + " ItemType==" + type);
}
final VH holder = type.onCreateViewHolder(parent);
type.onViewHolderCreated(holder, this);
return holder;
}
注意看mItmTypes集合,晕了就再回顾前文介绍。这里通过viewType值得到了对应的ItemType,接着回调其onCreateViewHolder、onViewHolderCreated方法,返回ViewHolder,end!最后最后,RecyclerView adapter生命周期走到了onBindViewHolder方法(注意是3参数的),看代码:
public final void onBindViewHolder(@NonNull VH holder,
int position,
@NonNull List<Object> payloads) {
/*统一捕获由position引发的可能异常*/
try {
final int size = mItemTypes.size();
ItemType<T, VH> type;
if (size == 1) {/*单样式*/
type = mItemTypes.valueAt(0);
} else if (size > 1) {/*多样式*/
/*可能有越界风险*/
type = mItemTypeRecord.get(position);
} else {
return;
}
if (payloads.isEmpty()) {
type.onBindViewHolder(holder, this, position);
}
/*局部刷新*/
else {
type.onBindViewHolder(holder, this, position, payloads);
}
} catch (Exception e) {
e.printStackTrace();
}
}
这里我们重点看多样式分支:由于ItemTypeRecord关系已经确定,所以能直接拿到position对应的ItemType,再回调其onBindViewHolder方法........完美结束!!!
行文至此,已倾尽所有。MultiAdapter库历经项目多个版本雕琢,已趋于稳定。众道友如有在使用过程中不幸踩坑,务必先冷静三分,反馈与我,虽忙必复!
推荐阅读:
再见JCenter,将你的开源库发布到MavenCentral上吧
PermissionX 1.5发布,支持申请Android特殊权限啦
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注