RecyclerView 多样式 Item 优雅解决方案
作者:首席网管
来源:https://www.jianshu.com/p/5bc618cb1c1d
一、介绍一下
MultiAdapter 是一个轻松优雅实现RecyclerView多样式的强大组件!它将item的行为及表现抽象为一个 ItemType
,不同类型的item都有着自己独立的点击事件处理及视图绑定行为,极大地降低了耦合度,采用 反射 、 ViewBinding 及 泛型
等技术极大地简化了item相关点击事件处理过程。其内部封装了若干实用的工具组件以满足RecyclerView日常需求,如列表的单选/多选。
正是因为有了上述功能支持,我们在给RecyclerView添加头布局、脚布局、嵌套RecyclerView布局的时候,就简单的太多了!
依赖详见GitHub:https://github.com/censhengde/MultiAdapter
二、用法(这里主推ViewBinding用法,原理在后面讲)
2.1 列表Item多样式实现
先看成品效果,如图:
图 2.0.1
使用步骤:
Step1 在app build.gradle文件开启ViewBinding:
1android {
2 //......
3 viewBinding {
4 enabled true
5 }
6}
Step 2 创建item的实体类 :
ItemBean.java:
1public class ItemBean {
2
3 //所有Item类型都在这里定义
4 public static final int TYPE_A = 0;
5 public static final int TYPE_B = 1;
6 public static final int TYPE_C = 2;
7
8 public int id;
9 //Item类型标识(很关键!)
10 public int viewType;
11
12
13 //item具体业务数据字段
14 public String text = "";
15
16
17 public ItemBean(int viewType, String text) {
18 this.viewType = viewType;
19 this.text = text;
20 }
21
22 public ItemBean(int id, int viewType, String text) {
23 this.viewType = viewType;
24 this.text = text;
25 this.id = id;
26 }
27
28
29
30}
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 3 声明各个item类型布局文件:
item_a.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="100dp"
6 android:background="#FF5722"
7 android:orientation="vertical">
8
9 <TextView
10 android:id="@+id/tv_a"
11 android:layout_width="wrap_content"
12 android:layout_height="match_parent"
13 android:layout_gravity="center"
14 android:gravity="center"
15 android:textSize="18sp"
16 tools:text="A 类 Item" />
17
18</LinearLayout>
item_b.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:orientation="horizontal"
4 android:layout_width="match_parent"
5 android:background="@color/colorAccent"
6 android:layout_height="120dp">
7
8 <Button
9 android:id="@+id/btn_b"
10 android:layout_width="wrap_content"
11 android:layout_height="wrap_content"
12 android:layout_gravity="center_vertical"
13 android:background="@android:color/transparent"
14 android:gravity="center"
15 android:text="Button"
16 android:textAllCaps="false"
17 android:textSize="18sp" />
18 <TextView
19 android:id="@+id/tv_b"
20 android:layout_width="wrap_content"
21 android:layout_height="wrap_content"
22 android:textSize="18sp"
23 android:gravity="center"
24 android:text="B 类 Item"/>
25
26</LinearLayout>
item_c.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:orientation="vertical"
4 android:layout_width="match_parent"
5 android:background="#9C27B0"
6 android:layout_height="150dp">
7 <TextView
8 android:id="@+id/tv_c"
9 android:layout_width="wrap_content"
10 android:layout_height="wrap_content"
11 android:textSize="18sp"
12 android:gravity="center"
13 android:layout_gravity="center|center_vertical"
14 android:text="C 类 Item"/>
15<ImageView
16 android:id="@+id/iv_c"
17 android:layout_width="72dp"
18 android:layout_height="72dp"
19 android:layout_gravity="center_horizontal"
20 android:layout_marginTop="5dp"
21 android:src="https:@mipmap/ic_launcher"/>
22</LinearLayout>
Step 4 创建各个item类型的ItemType实现类:
AItemType.java:
1public class AItemType extends MultiVBItemType<ItemBean,ItemABinding> {
2
3 /**
4 * @param data 当前position对应的实体对象
5 * @param position
6 * @return true 表示成功匹配到对应的ItemType
7 */
8 @Override
9 public boolean matchItemType(@Nullable ItemBean data, int position) {
10 return data == null || ItemBean.TYPE_A == data.viewType;//这句话的含义是:当前position 的ItemBean想要表现的item类型是哪一种,
11 //以本例为例,会依次遍历A、B、C三个Item类型,直到返回true为止。(详见MultiHelper getItemViewType方法实现)
12 }
13
14 /**
15 * @return 返回当前item类型的布局文件id
16 */
17 @Override
18 public int getItemLayoutRes() {
19 return R.layout.item_a;
20 }
21
22 /**
23 * 给当前item类型布局视图设置数据,意义基本与RecyclerView.Adapter onBindViewHolder 相同。
24 * @param vb
25 * @param position
26 */
27
28 @Override
29 protected void onBindViewHolder(@NonNull ItemABinding vb,
30 @NonNull ItemBean itemBean,
31 int position) {
32 //直接从ViewBinding 获取控件设置数据
33 vb.tvA.setText(itemBean.text);
34 }
35
36}
BItemType.java:
1public class BItemType extends MultiVBItemType<ItemBean,ItemBBinding > {
2
3 @Override
4 public boolean matchItemType(ItemBean data, int position) {
5 return ItemBean.TYPE_B == data.viewType;
6 }
7
8 @Override
9 public int getItemLayoutRes() {
10 return R.layout.item_b;
11 }
12
13
14 @Override
15 protected void onBindViewHolder(@NonNull ItemBBinding vb,
16 @NonNull ItemBean itemBean,
17 int position) {
18 vb.tvB.setText(itemBean.text);
19 }
20
21}
CItemType.java:
1public class CItemType extends MultiVBItemType<ItemBean,ItemCBinding> {
2
3
4 @Override
5 public boolean matchItemType(ItemBean data, int position) {
6 return ItemBean.TYPE_C == data.viewType;
7 }
8
9 @Override
10 public int getItemLayoutRes() {
11 return R.layout.item_c;
12 }
13
14
15 @Override
16 protected void onBindViewHolder(@NonNull ItemCBinding vb,
17 @NonNullItemBean bean,
18 int position) {
19 vb.tvC.setText(bean.text);
20 }
21}
ItemType 是本项目的核心概念之一,如前文所说,ItemType是一类item的抽象,其拥有独立的 视图绑定 和 点击事件处理
过程,并且它接管了RecyclerView.Adapter的生命周期业务; ItemType 是一个接口, MultiVBItemType
是其子类,实现了item
view点击事件回调等一系列核心功能,具体说明后面讲原理时候再详说。一种类型item对应一个ItemType,切记!先简单说明下各个方法含义:
public boolean matchItemType: 判断当前 position 的item样式是否对应当前的ItemType。如此例判断的依据就是实体对象的viewType字段取值。(这个方法是实现RecyclerView item多样式的核心,单样式item无需重写此方法,具体含义后面会再讲。)
2) public int getItemLayoutRes: 返回该类item的布局资源文件id。
3) protected void onBindViewHolder: 视图数据绑定。含义基本与RecyclerView Adapter 的
onBindViewHolder方法相同, 不同的是ItemType的这个方法仅进行当前item类型的视图数据绑定。
ItemABinding、ItemBBinding和ItemCBinding等类都是gradle根据item_a.xml、item_b.xml、item_c.xml布局文件自动生成的ViewBinding
实现类。
Step 5 在Activity里的初始化
创建Activity布局文件 activity_multi_item.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 tools:context="com.tencent.multiadapter.example.ui.MultiItemActivity">
8
9 <androidx.recyclerview.widget.RecyclerView
10 android:id="@+id/rv_list"
11 android:layout_width="match_parent"
12 android:layout_height="match_parent"
13 android:orientation="vertical"
14 app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
15
16</RelativeLayout>
在Activity onCreate方法中初始化RecyclerView:
1class MultiItemActivity : AppCompatActivity() {
2
3 lateinit var adapter: MultiAdapter<ItemBean, MultiViewHolder>
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContentView(R.layout.activity_multi_item)
8 //初始化ItemType
9 val aItemType = AItemType()
10 val bItemType = BItemType()
11 val cItemType = CItemType()
12
13 /*初始化Adapter;MultiAdapter 是内部封装好了的Adapter,简单使用无需新建其子类*/
14 adapter = MultiAdapter<ItemBean, MultiViewHolder>()
15 /*将所有ItemType添加到Adapter中*/
16 adapter.addItemType(aItemType)
17 .addItemType(bItemType)
18 .addItemType(cItemType)
19 /*设置数据*/
20 adapter.setData(getData())
21 rv_list.adapter = adapter
22
23 }
24
25 /**
26 * 模拟数据
27 */
28 private fun getData(): List<ItemBean> {
29 val beans = ArrayList<ItemBean>()
30 for (i in 0..5) {
31 beans.add(ItemBean(ItemBean.TYPE_A, "我是A类Item$i"))
32 beans.add(ItemBean(ItemBean.TYPE_B, "我是B类Item${i + 1}"))
33 beans.add(ItemBean(ItemBean.TYPE_C, "我是C类Item${i + 2}"))
34 }
35 return beans
36 }
37
38}
至此,点击运行就可以看到图2.0.1的效果了!
2.2 在Step 5 的基础上 给item view设置点击事件监听
与item相关的点击监听方案支持 反射 和 监听器 两种方式。
本文主要介绍 反射 方式:
反射方案是参考了系统的 xml android:onClick="目标方法名" 实现思路,但View并没有对外提供获取
android:onClick属性值的方法,故“目标方法名”需另辟蹊径传入,由于ItemType拥有独立的点击事件处理过程,所以在其实现类AbstractItemType中提供了registerItemViewClickListener和registerItemViewLongClickListener等工具方法用以注册指定item
view点击事件监听。方法原型及其参数说明如下:
1 protected final void registerItemViewClickListener(@NonNull VH holder,//当前item类型的ViewHolder。
2 @NonNull MultiHelper<T, VH> helper,//帮助类对象,通过它可以访问到item实体对象。
3 @Nullable String target,//目标方法名,可null,当采用监听器方式时,就传null。
4 @IdRes int... viewIds)//item 指定view 的id,可同时指定多个,因为存在多个view响应同一套逻辑的情况。
5 //不传id时默认是给item根布局设置监听。
例如:给 A类item 的item view设置监听:
这里先回顾一下我们使用原生RecyclerView
Adapter给item设置点击事件监听的过程,大致就是在onCreateViewHolder方法中xxxxxxxxx一顿操作……还有的朋友在onBindViewHolder方法中获取指定view再xxxxxx一顿操作(这里顺带提一嘴,在onBindViewHolder方法进行事件监听设置绝对是不专业的!)……,如前文所说,
ItemType 接管了Adapter生命周期,我们先看 ItemType 接口声明:
ItemType.java:
1public interface ItemType<T, VH extends RecyclerView.ViewHolder> {
2
3 /**
4 * 当前position 是否匹配当前的ItemType
5 *
6 * @param data 当前position对应的实体对象,当是依赖paging3 getItem()方法返回时,有可能为null。
7 * @param position adapter position
8 * @return true 表示匹配,false:不匹配。
9 */
10 boolean matchItemType(@Nullable T data, int position);
11
12
13 /**
14 * 创建当前ItemType的ViewHolder
15 *
16 * @param parent parent
17 * @return ViewHolder
18 */
19 @NonNull
20 VH onCreateViewHolder(@NonNull ViewGroup parent);
21
22 /**
23 * ViewHolder已经创建完成,在这里可以注册Item及其子View点击事件监听器,但不要做数据的绑定。
24 */
25 void onViewHolderCreated(@NonNull VH holder, @NonNull MultiHelper<T, VH> helper);
26
27 /**
28 * 意义与Adapter onBindViewHolder 基本相同,表示当前ItemType的数据绑定过程。
29 *
30 * @param holder
31 * @param position
32 */
33 void onBindViewHolder(@NonNull VH holder, @NonNull MultiHelper<T, VH> helper, int position,
34 @NonNull List<Object> payloads) throws Exception;
35
36
37 void onBindViewHolder(@NonNull VH holder, @NonNull MultiHelper<T, VH> helper, int position) throws Exception;
38
39}
ItemType 生命周期是以RecyclerView Adapter生命周期为前提的,如果有对RecyclerView
adapter相关方法调用流程还不熟的朋友,建议先去找资料研究一下。既然 ItemType
接管了adapter生命周期,那其所有方法必定都对应在adapter几个关键方法中被调用,其中在adapter
onCreateViewHolder方法中调用到的ItemType的方法有:onCreateViewHolder方法、onViewHolderCreated方法,这里可能有朋友有疑问:为什么要把adapter
onCreateViewHolder方法拆分为ItemType的两个阶段方法?答案就是出于 职责单一
编程原则考虑。创建ViewHolder就是创建ViewHolder,给ViewHolder
view设置监听或者进行其他操作又是另一回事了。所以最终我们是在ItemType onViewHolderCreated
方法给item相关view设置监听:
1.在AItemType中重写onViewHolderCreated方法:
1public class AItemType extends MultiItemType<ItemBean> {
2
3
4 /**
5 * @param data 当前position对应的实体对象
6 * @param position
7 * @return true 表示成功匹配到对应的ItemType
8 */
9 @Override
10 public boolean matchItemType(@Nullable ItemBean data, int position) {
11 return data == null || ItemBean.TYPE_A == data.viewType;//这句话的含义是:当前position 的ItemBean想要表现的item类型是哪一种,
12 //以本例为例,会依次遍历A、B、C三个Item类型,直到返回true为止。(详见MultiHelper getItemViewType方法实现)
13 }
14
15 /**
16 * @return 返回当前item类型的布局文件
17 */
18 @Override
19 public int getItemLayoutRes() {
20 return R.layout.item_a;
21 }
22
23 /**
24 * 表示ViewHolder已经创建完成。本方法最终是在RecyclerView.Adapter onCreateViewHolder方法中被调用,
25 * 所以所有的与item相关的点击事件监听器都应在这里注册。
26 *
27 * @param holder
28 * @param helper
29 */
30 @Override
31 public void onViewHolderCreated(@NonNull MultiViewHolder holder,
32 @NonNull MultiHelper<ItemBean, MultiViewHolder> helper) {
33 /*注册监听器,不传viewId则默认是给item根布局注册监听*/
34 registerItemViewClickListener(holder, helper, "onClickItem");
35 }
36 //其他代码不变,这里省略。
37}
2. 在Activity onCreate方法调用AItemType inject方法注入事件接收者:
1class MultiItemActivity : AppCompatActivity() {
2
3 lateinit var adapter: MultiAdapter<ItemBean, MultiViewHolder>
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContentView(R.layout.activity_multi_item)
8 //初始化ItemType
9 val aItemType = AItemType()
10 //注入事件接收对象
11 aItemType.inject(this)
12 //其他代码不变,略。
13
14 }
15 //......
16}
3.在Activity中声明目标方法(注意,方法名一定要与刚才传入的target值对应!参数列表顺序不能乱!方法访问修饰符任意。):
1 /**
2 *item点击事件
3 */
4 private fun onClickItem(view: View, itemBean: ItemBean, position: Int) {
5 Toast.makeText(this, "ItemBean:${itemBean.text},position:$position", Toast.LENGTH_SHORT).show()
6 }
附Activity完整代码:
1class MultiItemActivity : AppCompatActivity() {
2
3 lateinit var adapter: MultiAdapter<ItemBean, MultiViewHolder>
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContentView(R.layout.activity_multi_item)
8 //初始化ItemType
9 val aItemType = AItemType()
10 //注入事件接收对象
11 aItemType.inject(this)
12
13 val bItemType = BItemType()
14 val cItemType = CItemType()
15 bItemType.inject(this)
16 cItemType.inject(this)
17 /*初始化Adapter*/
18 adapter = MultiAdapter<ItemBean, MultiViewHolder>()
19 /*将所有ItemType添加到Adapter中*/
20 adapter.addItemType(aItemType)
21 .addItemType(bItemType)
22 .addItemType(cItemType)
23 /*设置数据*/
24 adapter.setData(getData())
25 rv_list.adapter = adapter
26 }
27
28 /**
29 * 模拟数据
30 */
31 private fun getData(): List<ItemBean> {
32 val beans = ArrayList<ItemBean>()
33 for (i in 0..5) {
34 beans.add(ItemBean(ItemBean.TYPE_A, "我是A类Item$i"))
35 beans.add(ItemBean(ItemBean.TYPE_B, "我是B类Item${i + 1}"))
36 beans.add(ItemBean(ItemBean.TYPE_C, "我是C类Item${i + 2}"))
37 }
38 return beans
39 }
40
41 /**
42 *item点击事件
43 */
44 private fun onClickItem(view: View, itemBean: ItemBean, position: Int) {
45 Toast.makeText(this, "ItemBean:${itemBean.text},position:$position", Toast.LENGTH_SHORT).show()
46 }
47
48}
完毕,点击运行之。效果如图:
图 2.0.2
其他item相关点击事件如item子view点击事件、长点击事件等监听实现同理,详见工程用例。
采用反射方式实现item点击监听务必注意代码混淆后无法找到目标方法的问题,这里附上两种避免方式:
例1.在module 的proguard-rules.pro文件配置:
1-keepclassmembers class com.tencent.multiadapter.example.ui.MultiItemActivity{ private void onClickItem(...); }
例2.在方法声明上标记@Keep注解(推荐):
1 /**
2 *item点击事件
3 */
4 @Keep
5 private fun onClickItem(view: View, itemBean: ItemBean, position: Int) {
6 Toast.makeText(this, "ItemBean:${itemBean.text},position:$position", Toast.LENGTH_SHORT).show()
7 }
2.3 列表单选/多选实现
列表单/多选实现主要依靠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 callback):设置完成选择后的回调接口。OnCheckingFinishedCallback 接口方法说明如下:
public interface OnCheckingFinishedCallback {
1/**
2 * @param checked 被选中的Item集合
3 */
4void onCheckingFinished(@NonNull List<T> checked);}
6)void finishChecking():完成选择。调用这个方法将触发OnCheckingFinishedCallback接口 回调。
一般的简单列表选择是当列表是单样式item的时候,实现比较简单,这里先不介绍。我们重点关注一下当列表是多样式item的时候(如当列表存在头布局脚布局的时候),我们应该如何排除掉无效item。如图:
图 2.3.1
某一条item的选中状态,它是否是符合我们预期的,需要定义一个符合我们预期的规则,比如当列表存在头布局脚布局的时候,我们点击全选,最后只有中间的item是可选中的,点击完成,最后只有被选中的item集合回调出来,这才符合我们的预期。
某一item是否是可选的,以及它符合什么样的规则才被认为是选中的,我们抽象出一个接口,名叫Checkable,意为可选的。CheckingHelper将会依据这个接口做统一判断处理item。Checkable声明如下:
1public interface Checkable {
2 /*设置是否被选中,注意,复杂的item是否被选中规则一定要注意此方法的实现,
3 *不要局限于单纯搞个boolean 变量做判断。
4 */
5 void setChecked(boolean checked);
6 /*判断是否被选中*/
7 boolean isChecked();
8}
现在我们开始实现复杂列表多选功能。
实现步骤:
step 1:新建item实体类CheckableItem.java,并实现Checkable接口,代码如下:
1public class CheckableItem implements Checkable {
2
3 public static final int VIEW_TYPE_HEADER=1;/*头布局标识位*/
4 public static final int VIEW_TYPE_CHECKABLE = 0;/*可选中的Item标识位*/
5 public static final int VIEW_TYPE_FOOTER=2;/*脚布局标识位*/
6 public int viewType = VIEW_TYPE_CHECKABLE;/*默认是可选中item*/
7
8 private boolean mIsChecked;/*判断当前item是否被选中*/
9
10 public String text="";
11 public CheckableItem(int viewType, String text) {
12 this.viewType = viewType;
13 this.text = text;
14 }
15
16 @Override
17 public void setChecked(boolean checked) {
18 /*头布局和脚布局是不可选的。注意,只有这里的被选中规则定义得准确,
19 *后面调用CheckingHelper finishedChecking才能准确甄选出被选中的item
20 */
21 mIsChecked = checked && viewType == VIEW_TYPE_CHECKABLE;
22 }
23
24 @Override
25 public boolean isChecked() {
26 return mIsChecked;
27 }
28}
注意看setChecked方法的实现,这里再次强调一个理念: RecyclerView 某position上的item所表现的样式是由item实体对象决定的。 这个理念的另一个解读含义是: 尽管RecyclerView item是多样式的,但外层实体类的类型是一致的!
当用户点击全选时,所有类型item实体类(Checkable类型)的setChecked方法被调用且checked参数都传入true,当用户点击完成时,所有类型item实体类(Checkable类型)的isChecked方法被调用,依据其返回值最终判断当前item最终选中状态。
step 2:编写各类item布局文件:
头布局:item_checking_header.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="100dp">
5 <TextView
6 android:layout_width="wrap_content"
7 android:layout_height="wrap_content"
8 android:textSize="18sp"
9 android:layout_gravity="center"
10 android:text="头布局"/>
11</FrameLayout>
可选的item布局:item_checking_checkable.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="120dp">
5
6 <TextView
7 android:id="@+id/tv"
8 android:layout_width="wrap_content"
9 android:layout_height="wrap_content"
10 android:textSize="18sp"
11 android:layout_centerVertical="true"
12 android:layout_marginStart="50dp"/>
13
14 <CheckBox
15 android:id="@+id/checkbox"
16 android:layout_width="wrap_content"
17 android:layout_height="wrap_content"
18 android:clickable="false"
19 android:layout_alignParentEnd="true"
20 android:layout_centerVertical="true"
21 android:layout_marginEnd="20dp" />
22</RelativeLayout>
脚布局:item_checking_footer.xml:
1<?xml version="1.0" encoding="utf-8"?>
2<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="100dp"
5 android:background="@android:color/darker_gray">
6 <TextView
7 android:layout_width="wrap_content"
8 android:layout_height="wrap_content"
9 android:textSize="18sp"
10 android:layout_gravity="center|top"
11 android:textColor="@color/colorAccent"
12 android:text="脚布局"/>
13</FrameLayout>
step 3:创建各个item类型的ItemType实现类:
头布局:HeaderItemType.kt:
1class HeaderItemType:MultiItemType<CheckableItem>() {
2
3 override fun getItemLayoutRes(): Int = R.layout.item_checking_header
4
5 override fun matchItemType(data: CheckableItem, position: Int): Boolean
6 =data.viewType== CheckableItem.VIEW_TYPE_HEADER
7
8 override fun onBindViewHolder(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>, position: Int) {
9
10 }
11}
可选item布局:CheckableItemType.kt:
1class CheckableItemType : MultiItemType<CheckableItem>() {
2
3 override fun getItemLayoutRes(): Int = R.layout.item_checking_checkable
4
5 override fun matchItemType(data: CheckableItem, position: Int): Boolean = data.viewType == CheckableItem.VIEW_TYPE_CHECKABLE
6
7 override fun onViewHolderCreated(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>) {
8 registerItemViewClickListener(holder,helper,"onClickItem")
9 }
10
11
12
13 /**
14 * 只有局部刷新才会回调到这里,RecyclerView上下滑动则不会,有区别于RecyclerView.Adapter中的实现.
15 */
16 override fun onBindViewHolder(holder: MultiViewHolder,
17 helper: MultiHelper<CheckableItem,MultiViewHolder>,
18 position: Int,
19 payloads: MutableList<Any>) {
20 payloads.forEach {
21 if (it is Int)
22 if (it == R.id.checkbox) {
23 val item = helper.getItem(position) ?: return
24 val checkbox = holder.getView<CheckBox>(R.id.checkbox)
25 checkbox.isChecked = item.isChecked
26 }
27 }
28 }
29
30 override fun onBindViewHolder(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>, position: Int) {
31 val item = helper.getItem(position) ?: return
32
33 val tv = holder.getView<TextView>(R.id.tv)
34 tv.text = item.text
35 //CheckBox
36 val checkbox = holder.getView<CheckBox>(R.id.checkbox)
37 checkbox.isChecked = item.isChecked
38
39 }
40}
注意图中两个onBindViewHolder重载方法的实现,清楚RecyclerView
item局部刷新的朋友应该了解这两个方法的用法及区别,本文不做赘述。
脚布局:FooterItemType.kt:
1class FooterItemType:MultiItemType<CheckableItem>() {
2
3 override fun getItemLayoutRes(): Int = R.layout.item_checking_footer
4
5 override fun matchItemType(data: CheckableItem, position: Int): Boolean =data.viewType == CheckableItem.VIEW_TYPE_FOOTER
6
7 override fun onBindViewHolder(holder: MultiViewHolder, helper: MultiHelper<CheckableItem,MultiViewHolder>, position: Int) {
8
9 }
10}
step 4:在activity初始化:
1class CheckItemActivity : AppCompatActivity(), OnCheckingFinishedCallback<CheckableItem> {
2 val adapter = MultiAdapter<CheckableItem,MultiViewHolder>()
3 val dataSize = 30
4 override fun onCreate(savedInstanceState: Bundle?) {
5 super.onCreate(savedInstanceState)
6 setContentView(R.layout.activity_check_item)
7 val checkableItemType = CheckableItemType()
8 /*注册item点击监听*/
9 checkableItemType.inject(this)
10
11 /*添加ItemType*/
12 adapter.addItemType(HeaderItemType())
13 .addItemType(checkableItemType)
14 .addItemType(FooterItemType())
15
16 //设置完成选择的回调
17 adapter.checkingHelper.setOnCheckingFinishedCallback(this)
18
19 adapter.setData(getData())
20 rv_list.adapter = adapter
21 }
22
23 /*模拟数据(页面状态的改变可能会导致列表选择状态丢失,建议在ViewModel或其他序列化手段保存数据以便恢复列表选择状态)
24 * */
25 private fun getData(): MutableList<CheckableItem> {
26 val data = ArrayList<CheckableItem>(dataSize + 2)
27 /*头布局item 实体对象*/
28 data.add(CheckableItem(CheckableItem.VIEW_TYPE_HEADER, ""))
29 /*中间可选的item实体对象*/
30 for (i in 0 until dataSize) {
31 data.add(CheckableItem(CheckableItem.VIEW_TYPE_CHECKABLE, "可选的Item position=${i}"))
32 }
33 /*脚布局item实体对象*/
34 data.add(CheckableItem(CheckableItem.VIEW_TYPE_FOOTER, ""))
35 return data
36 }
37
38 /*点击完成*/
39 fun onClickFinished(view: View) {
40 adapter.checkingHelper.finishChecking()
41 }
42
43 /*点击全选、取消*/
44 fun onClickCheckAll(view: View) {
45 val btn = (view as Button)
46 when (btn.text) {
47 "全选" -> {
48 btn.text = "取消"
49 adapter.checkingHelper.checkAll(R.id.checkbox)
50
51 }
52 "取消" -> {
53 btn.text = "全选"
54 adapter.checkingHelper.cancelAll(R.id.checkbox)
55 }
56 }
57 }
58
59 /*点击可选的item*/
60 private fun onClickItem(view: View, item: CheckableItem, position: Int) {
61 if (item.isChecked) {
62 adapter.checkingHelper.uncheckItem(position, R.id.checkbox)
63 } else {
64 adapter.checkingHelper.checkItem(position, R.id.checkbox)
65 }
66 /*当你想实现列表单选时,请调用adapter.checkingHelper.singleCheckItem(position, R.id.checkbox)*/
67 }
68
69 /*点击完成时的数据回调*/
70 override fun onCheckingFinished(checked: List<CheckableItem>) {
71 checked.forEach {
72 Log.e("被选中的item:", it.text)
73 }
74 }
75}
这里注意payload参数要与CheckableItemType onBindViewHolder 3参数方法中的对应。
完毕,点击运行之,效果如图2.3.1
三、扩展
MultiAdapter库由于其内部组件的高度解耦性,可将其复用于其它RecyclerView.Adapter。当我们想要实现RecyclerView分页功能的时候、也许我们会选择Google
的paging1、2、3的解决方案,这时候MultiAdapter库提供的MultiAdapter将不再适用,但我们可以模仿MultiAdapter的构建过程,利用MultiHelper、CheckingHelper组件轻松地完成其他任意RecyclerView.Adapter改造。
以改造paging3 的PagingDataAdapter为例,代码如下:
1open class MultiPagedAdapter<T : Any,VH :RecyclerView.ViewHolder>(diffCallback: DiffUtil.ItemCallback<T>)
2 : PagingDataAdapter<T, VH>(diffCallback) {
3
4 val multiHelper = object : MultiHelper<T,VH>(this) {
5
6 override fun getItem(p0: Int): T? {
7 return this@MultiPagedAdapter.getItem(p0)
8 }
9
10 }
11 val checkingHelper = object : CheckingHelper<T>(this) {
12 override fun getItem(position: Int): T? = this@MultiPagedAdapter.getItem(position)
13 override fun getDataSize(): Int = this@MultiPagedAdapter.itemCount
14
15 }
16
17 override fun getItemViewType(position: Int): Int {
18 return multiHelper.getItemViewType(position)
19 }
20
21 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
22 return multiHelper.onCreateViewHolder(parent, viewType)
23 }
24
25 override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any?>) {
26 multiHelper.onBindViewHolder(holder, position, payloads)
27 }
28
29 override fun onBindViewHolder(holder: VH, position: Int) {
30
31 }
32
33}
可以看到,MultiHelper的本质就是代理的Adapter的生命周期。
这里注意onBindViewHolder方法,我们仅需要代理上面3参数的即可,因为内部做了处理,让执行流程直接调用到ItemType的两个重载onBindViewHolder方法。
四、原理篇
多样式item实现的核心逻辑封装在了MultiHelper类中,MultiHelper本质是接管了RecyclerView.Adapter生命周期,代理了RecyclerView.Adapter的getItemViewType、onCreateViewHolder与onBindViewHolder三个核心方法,并将其生命周期事件分发给了position对应的ItemType,最终转换成了ItemType的生命周期。(有对RecyclerView.Adapter生命周期流程不熟的同学建议先去了解一下,网上博客资料都有的)
原理篇主要依照RecyclerView.Adapter生命周期这条主线来讲解,其他细节由于篇幅有限,建议读者去阅读工程源码,注释解释思想一应俱全!
这里先瞜一眼MultiHelper整体源码:
1public abstract class MultiHelper<T, VH extends RecyclerView.ViewHolder> {
2
3
4 /**
5 * ItemType集合.
6 */
7 private final SparseArray<ItemType<T, VH>> mItemTypePool = new SparseArray<>();
8
9 public final int getItemViewType(int position) {
10 if (position == RecyclerView.NO_POSITION) {
11 return RecyclerView.INVALID_TYPE;
12 }
13 final T data = getItem(position);
14 final ItemType<T, VH> currentType = findCurrentItemType(data, position);
15 return currentType == null ? RecyclerView.INVALID_TYPE : currentType.getClass().hashCode();
16 }
17
18
19 /**
20 * 遍历查找当前position对应的ItemType。
21 *
22 * @param data
23 * @param position
24 * @return
25 */
26 @Nullable
27 private ItemType<T, VH> findCurrentItemType(T data, int position) {
28 //为当前position 匹配它的ItemType
29 for (int i = 0; i < mItemTypePool.size(); i++) {
30 final ItemType<T, VH> type = mItemTypePool.valueAt(i);
31 if (type.matchItemType(data, position)) {
32 return type;
33 }
34 }
35 return null;
36 }
37
38 @NotNull
39 public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
40 final ItemType<T, VH> type = mItemTypePool.get(viewType);
41 if (viewType == RecyclerView.INVALID_TYPE || type == null) {//表示无效
42 throw new IllegalStateException("ItemType 不合法:viewType==" + viewType + " ItemType==" + type);
43 }
44 final VH holder = type.onCreateViewHolder(parent);
45 type.onViewHolderCreated(holder, this);
46 return holder;
47 }
48
49 public final void onBindViewHolder(@NonNull VH holder,
50 int position,
51 @NonNull List<Object> payloads) {
52 if (position == RecyclerView.NO_POSITION) {
53 return;
54 }
55 /*统一捕获由position引发的可能异常*/
56 try {
57 final ItemType<T, VH> currentType = mItemTypePool.get(holder.getItemViewType());
58 final T bean = getItem(position);
59 if (bean == null || currentType == null) {
60 return;
61 }
62 if (payloads.isEmpty()) {
63 currentType.onBindViewHolder(holder, bean, position);
64 }
65 /*item局部刷新*/
66 else {
67 currentType.onBindViewHolder(holder, bean, position, payloads);
68 }
69 } catch (Exception e) {
70 e.printStackTrace();
71 }
72
73 }
74
75 @Nullable
76 public abstract T getItem(int position);
77
78
79 /**
80 * 注册ItemType
81 *
82 * @param type
83 * @return
84 */
85 public final void addItemType(ItemType<T, VH> type) {
86 if (type == null) {
87 return;
88 }
89 //getClass().hashCode():确保一种item类型只有一个对应的ItemType实例。
90 mItemTypePool.put(type.getClass().hashCode(), type);
91 }
92
93
94}
如前文所说,MultiHelper类代理了RecyclerView
Adapter,为什么采用这种设计模式呢???答案就是为了之后改造任意RecyclerView Adapter达到代码复用的目的,如前文改造goole
paging3 PagingDataAdapter
就是个例子。RecyclerView有很多框架组件,不同框架组件可能有不同的Adapter实现,我们需要考虑兼容人家的东西。
可以看到,这里又一大堆与RecyclerView Adapter
相似的方法,注意,这里既然是代理Adapter,那这些方法及其参数含义就肯定与Adapter的一致了!
先解释几个核心点:
1 、mItemTypePool:SparseArray类型, 以ItemType Class对象的hashCode()方法返回值为key,
ItemType 实例为value进行存储。在addItemType方法进行赋值。 (它的key含义具有重大意义!这里先留个神!)
有多少种item类型就有多少个ItemType实例被存储。同一种item类型只有一个ItemType实例,理解了有没有?
2、findCurrentType(data, position)方法源码:
1 @Nullable
2 private ItemType<T, VH> findCurrentType(T data, int position) {
3 //为当前position 匹配它的ItemType
4 for (int i = 0; i < mItemTypes.size(); i++) {
5 final ItemType<T, VH> type = mItemTypes.valueAt(i);
6 if (type.matchItemType(data, position)) {
7 return type;
8 }
9 }
10 return null;
11 }
可以看到,其实就是遍历mItemTypePool集合,逐个调用ItemType的matcItemType()方法进行判断,如果返回true
则表示匹配成功,返回ItemType。
用户不必担心item的增删改会导致item样式表现错乱问题。这里又再次强调了那个理念: RecyclerView 某position 对应的item 所表达的类型由其实体对象决定! 实体对象怎么决定的?请回顾前文ItemBean 的 int 类型的 viewType 字段、ItemType的
matchItemType() 方法的实现!!!你品!你细细品!!!
经此getItemViewType方法调用,进而得到position对应的viewType值,再返回,接着RecyclerView
Adapter生命周期就走到了 onCreateViewHolder 方法:
1 @NotNull
2 public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
3 final ItemType<T, VH> type = mItemTypePool.get(viewType);
4 if (viewType == INVALID_VIEW_TYPE || type == null) {//表示无效
5 throw new IllegalStateException("ItemType 不合法:viewType==" + viewType + " ItemType==" + type);
6 }
7 final VH holder = type.onCreateViewHolder(parent);
8 type.onViewHolderCreated(holder, this);
9 return holder;
10 }
注意看mItmTypePool集合,晕了就再回顾前文介绍。这里通过viewType值直接拿到了对应的ItemType,接着回调其onCreateViewHolder、onViewHolderCreated方法,返回ViewHolder,end!
最后最后,RecyclerView adapter生命周期走到了 onBindViewHolder方法(注意是3参数的),看代码:
1 public final void onBindViewHolder(@NonNull VH holder,
2 int position,
3 @NonNull List<Object> payloads) {
4 if (position == RecyclerView.NO_POSITION) {
5 return;
6 }
7 /*统一捕获由position引发的可能异常*/
8 try {
9 //这里通过 ViewHolder getItemType()方法又直接拿到对应的ItemType。
10 final ItemType<T, VH> currentType = mItemTypePool.get(holder.getItemViewType());
11 final T bean = getItem(position);
12 if (bean == null || currentType == null) {
13 return;
14 }
15 if (payloads.isEmpty()) {
16 currentType.onBindViewHolder(holder, bean, position);
17 }
18 /*item局部刷新*/
19 else {
20 currentType.onBindViewHolder(holder, bean, position, payloads);
21 }
22 } catch (Exception e) {
23 e.printStackTrace();
24 }
25 }
这里可以看到,mItemTypePool集合从MultiHelper. addItemType方法开始,到MultiHelper.
getItemViewType方法、再到MultiHelper. onCreateViewHolder方法,最后到MultiHelper.
onBindViewHolder方法,贯穿一路!全篇始终围绕着它的key含义进行构造,可以说这个集合是全篇的灵魂!……至此RecyclerView.Adapter生命周期流程就梳理完了。其他细枝末节诸如ItemType几个关键子类MultiItemType、MultiVBItemType、AbstrcactItemType的实现详见工程源码,注释解释思想一应俱全!
行文至此,已倾尽所有。MultiAdapter库历经项目多个版本雕琢,已趋于稳定。众道友如有在使用过程中不幸踩坑,务必先冷静三分,反馈与我,虽忙必复!
推荐阅读
耗时一周,我解决了微信 Matrix 增量编译的 Bug,已提 PR
Android QMUI实战:实现APP换肤功能,并自动适配手机深色模式
公众号徐公回复黑马,获取 Android 学习视频 公众号徐公回复徐公666,获取简历模板,教你如何优化简历,走近大厂 公众号徐公回复面试,可以获得面试常见算法,剑指 ofer 题解 公众号徐公回复马士兵,可以获得马士兵学习视频一份