区伯肺癌病重:一位逐渐被遗忘的广州公民

前外交部副部长傅莹:一旦中美闹翻,有没有国家会站在中国一边

Weixin Official Accounts Platform

去泰国看了一场“成人秀”,画面尴尬到让人窒息.....

多年来,中国有个省,几乎每一个村庄都在偷偷“虎门销烟”

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

RecyclerView 多样式 Item 优雅解决方案

徐公 2022-04-23
作者:首席网管
来源: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,切记!先简单说明下各个方法含义:

  1. 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说明如下:

  1. void checkItem(int position,@Nullable Object payload):选中item。position:当前位置;payload:用于局部刷新的参数,与 RecyclerView.Adapter notifyItemChanged方法的意义相同。

  2. void uncheckItem(int position,@Nullable Object payload):取消选中item。与checkItem相反。

  3. void checkAll(@Nullable Object payload):全选。payload:同上。

  4. void cancelAll(@Nullable Object payload):取消全选。

  5. 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库历经项目多个版本雕琢,已趋于稳定。众道友如有在使用过程中不幸踩坑,务必先冷静三分,反馈与我,虽忙必复!

推荐阅读

我的 5 年 Android 学习之路,那些年一起踩过的坑

耗时一周,我解决了微信 Matrix 增量编译的 Bug,已提 PR

Android QMUI实战:实现APP换肤功能,并自动适配手机深色模式

如果觉得对你有所帮助的话,可以关注我的微信公众号徐公,5 年中大厂工作经验。
  1. 公众号徐公回复黑马,获取 Android 学习视频
  2. 公众号徐公回复徐公666,获取简历模板,教你如何优化简历,走近大厂
  3. 公众号徐公回复面试,可以获得面试常见算法,剑指 ofer 题解
  4. 公众号徐公回复马士兵,可以获得马士兵学习视频一份





文章有问题?点此查看未经处理的缓存