仿MultiType打造ListView多类型列表

2,951 阅读6分钟

地址:github.com/stevenwsg/M…

1、介绍

第一次见到MultiType时,真的被惊艳到了,没想到多类型列表可以写的这么简洁,解耦。之前有幸曾维护过一个有15种类型构成的ListView, 实在难以恭维,单个Adapter超过三千多行,各类型ViewHolder 插入其中。虽然ListView多类型Adapter 很好理解,重点就getTypeCount(),getViewType,getView()这三个方法, 但是这样写总感觉怪怪的, 如果项目组来个年轻人没用过ListView ,新增一种类型而忘了给 getTypeCount()方法加1,就要出线上事故,增加维护成本。

image

最近感悟到,在日常的业务开发中,觉得不爽,不合理的地方想想有没有更好的解决方式,多思考, 而不是一直在搬砖堆业务。 就MultiType而言,原理大家基本上看一下度能够理解,并不是很深奥的东西。 但是提出这个想法的人却比较少,并且在RecyclerVeiw 出现之后提出, 从Android 1.0时代,列表都是用ListView实现的, 大家实现多类型列表都是那三个方法, 为什么没有更简洁的方案提出来那(可能有些公司有大佬提出来没有开源),值得思考。

从去年开始一直想写关于ListView实现多类型列表的实现方式,一直拖到现在,一个原因是菜,一个原因是懒。之前工作太忙了。 趁着前段时间不太忙,把这个想法完善了一下。

了解MultiType原理的同学,理解接下来要说的其实问题不大, 不了解的同学其实可以看看我这篇, RecyclerView多类型列表实现—— MultiType分析, 简单的来说就是维护一个映射表,实现ViewType类型,Bean数据,ViewHolder的映射。

回忆一下MultiType的实现: image

对应的映射表:

ViewTypeJava Bean ClassLinkerItemViewBinder
0C1L1binder_1
1C2L2binder_2_1
2C2L2binder_2_2
3C2L2binder_2_3
4C2L3binder_3_1

此次MultiTypeListViewAdapter的实现:

image 对应的映射表:

ViewType | Java Bean Class | ViewHolder ---|---|---|--- 0 | C0 | viewHolder0 1 | C1 | viewHolder1 2 | C2 | viewHolder2 3 | C3 | viewHolder3 4 | C4 | viewHolder4

可以看出此次实现的MultiTypeListViewAdapter相比MultiType只实现了一对一映射, 而没有实现一对多映射,如果有相关需求的话,可以考虑在数据层解决这个问题,即多个JavaBean绑定同一个ViewHolder。

如果你想对老项目中的多类型ListView进行整理或者小范围重构的话,可以试试这个方案, 对代码改动量和迁移量是比较小的。大范围重构可能就考虑RecyclerView了。

2、 使用

用法比较简洁, 和MultiType的用法其实是一样的

1、创建数据实体

data class TextItem(val text : String)

2、创建ViewHolder

class TextViewHolder : BaseViewHolder<TextItem>() {

    private var text: TextView? = null

    override fun getLayoutId(): Int {
        return R.layout.item_text //返回VH的布局文件
    }

    override fun onBindViewHolder(rootView: View) {
        text = rootView.findViewById(R.id.text) //绑定View
    }

    override fun render(item: TextItem, position: Int) {
        text?.text = item.text //渲染数据和设置点击事件等
    }
}

3、Adapter绑定ViewHolder,注入数据

class MainActivity : AppCompatActivity() {

    private var mList: MutableList<Any>? = null
    private var mListView: ListView? = null
    private var adapter: MultiTypeListViewAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initData();
        initAdapter();

        mListView = findViewById(R.id.listview)
        mListView?.adapter = adapter
    }

    private fun initData() {
        mList = ArrayList()
        for (i in 1..100) { //添加各种类型的数据
            mList?.add(ImageItem(R.mipmap.ic_launcher))
            mList?.add(RichItem("小艾大人赛高", R.drawable.ic_launcher_foreground))
            mList?.add(TextItem("没有什么能够阻挡,你对自由的向往~"))
        }
    }

    //注册ViewHolder
    private fun initAdapter() { //绑定各个类型的VH和数据,填充映射表
        adapter = MultiTypeListViewAdapter(this, mList)
        adapter?.register(RichItem::class.java, RichTextViewHolder())
        adapter?.register(ImageItem::class.java, ImageViewHolder())
        adapter?.register(TextItem::class.java, TextViewHolder())
    }
}

实现效果

image

3、原理

原理其实就是将getTypeCount(),getViewType,getView()这三个方法封装起来,帮助开发者避免这三个方法, 具体的实现方式就是维护这个映射表。

ViewType | Java Bean Class | ViewHolder ---|---|---|--- 0 | C0 | viewHolder0 1 | C1 | viewHolder1 2 | C2 | viewHolder2 3 | C3 | viewHolder3 4 | C4 | viewHolder4

怎么映射那,下面就开始上代码:
这个是项目结构,代码量很少, 大家如果感兴趣的话, 可以下载下来看看,不大只有四个类

image

1、TypePool

interface TypePool {
    fun <T> register(clazz: Class<out T>, holder: BaseViewHolder<T>)
    fun unregister(clazz: Class<*>)
    fun size(): Int //类型数目
    fun indexOf(clazz: Class<*>): Int //Class对应的positon
    fun getBaseViewHolder(index: Int): BaseViewHolder<*>
}

2、MultiTypePool

public class MultiTypePool : TypePool {
    private val classes: MutableList<Class<*>>
    private val holders: MutableList<BaseViewHolder<*>>

    constructor() { //维护上面所说的映射表
        classes = ArrayList()
        holders = ArrayList()
    }

    constructor(initialCapacity: Int) {
        classes = ArrayList(initialCapacity)
        holders = ArrayList(initialCapacity)
    }

    //注册和解绑其实都是对上述映射表的操作
    override fun <T> register(
        clazz: Class<out T>,
        holder: BaseViewHolder<T>
    ) {
        classes.add(clazz)
        holders.add(holder)
    }

    override fun unregister(clazz: Class<*>) {
        val postion = indexOf(clazz)
        classes.remove(clazz)
        holders.removeAt(postion)
    }

    override fun size(): Int { // 一共多少个类型的VH
        return classes.size
    }

    override fun indexOf(clazz: Class<*>): Int { //对应的postion
        return classes.indexOf(clazz)
    }

    override fun getBaseViewHolder(index: Int): BaseViewHolder<*> {
        return holders[index]
    }
}

3、BaseViewHolder

就是普通的ViewHolder, 约定俗成的实现以下方法, 可以看出并没有像MultiType在做一层代理,减少理解成本, 和平时的使用其实是一样的

public abstract class BaseViewHolder<T> {

    protected var adapter: MultiTypeListViewAdapter? = null

    // 获取布局id
    abstract fun getLayoutId(): Int
    abstract fun onBindViewHolder(rootView: View) // 绑定View
    abstract fun render(item: T, position: Int) // 渲染数据
}

4、MultiTypeListViewAdapter

public class MultiTypeListViewAdapter extends BaseAdapter {

    private static final String TAG = "MultiTypeListViewAdapter";
    private List<?> items;
    private TypePool typePool;
    private LayoutInflater inflater;

    // 下面这些方法,和ListView普通Adapter写法一样
    public MultiTypeListViewAdapter(Context context) {
        this(context, Collections.emptyList());
    }

    public MultiTypeListViewAdapter(Context context, List<?> items) {
        this(context, items, new MultiTypePool());
    }

    public MultiTypeListViewAdapter(Context context, List<?> items, int initialCapacity) {
        this(context, items, new MultiTypePool(initialCapacity));
    }

    public MultiTypeListViewAdapter(Context context, List<?> items, TypePool pool) {
        this.items = items;
        this.typePool = pool;
        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void setItems(List<?> items) {
        this.items = items;
    }

    public List<?> getItems() {
        return items;
    }

    public <T> void register(Class<? extends T> clazz, BaseViewHolder<T> holder) {
        typePool.register(clazz, holder);
        holder.setAdapter(this); // 将Adapter注入VH中, 方便后面VH调用Adapter或者Activity的方法, 同时需要注意回收
    }


    @Override
    public int getCount() {
        return items.size();
    }

    @Override
    public Object getItem(int position) {
        return items.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        BaseViewHolder viewHolder;
        int viewType = getItemViewType(position);
//        if (convertView == null) {
//        viewHolder = typePool.getBaseViewHolder(viewType);
//       convertView = inflater.inflate(viewHolder.getLayoutId(), parent, false);
//        viewHolder.onBindViewHolder(convertView);
//
//            convertView.setTag(viewHolder);
//        } else {
//            viewHolder = typePool.getBaseViewHolder(viewType);
//        }

// 这块的VH 复用还是有点问题, 写出来还是想找大家看看有没有好的方式解决这个

        // 下面这块是VH不复用的方式
        viewHolder = typePool.getBaseViewHolder(viewType); //拿到相应的ViewHolder
        convertView = inflater.inflate(viewHolder.getLayoutId(), parent, false); //初始化布局
        viewHolder.onBindViewHolder(convertView); //ViewHolder绑定布局
    
        viewHolder.render(getItem(position), position); //ViewHolder渲染数据
        return convertView;
    }

    @Override
    public int getViewTypeCount() { // 就是返回映射表的行数量
        return typePool.size();
    }

    @Override
    public int getItemViewType(int position) { // 返回该用第几行的类型
        return typePool.indexOf(items.get(position).getClass());
    }
}

目前这个getView()方法中的VH是没有复用的, 不推荐这么做,但是复用版写的代码复用的有点问题,想让大家看看改怎么修改,这也是开源嘛, 逃(为自己的菜找原因)。

4、总结

大家日常写列表可能已经不考虑ListView了,都使用RecyclerView了。虽然这篇文章讲的是ListView实现的多类型列表,也是近期来的一些思考,分享出来。感兴趣的同学可以下载源码看看。

GitHub地址:github.com/stevenwsg/M…