RecyclerView多样式Item优雅方案之旅


/   今日科技快讯   /

近日,百度世界大会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,切记!先简单说明下各个方法含义。

  1. public boolean matchItemType: 判断当前 position 的item样式是否对应当前的ItemType。如此例判断的依据就是实体对象的viewType字段取值。(这个方法是实现RecyclerView item多样式的核心,单样式item无需重写此方法,具体含义后面会再讲。)

  2. public int getItemLayoutRes:返回该类item的布局资源文件id。

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

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

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

再见JCenter,将你的开源库发布到MavenCentral上吧

PermissionX 1.5发布,支持申请Android特殊权限啦

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值