【IM】网易lM聊天列表UI

对于一个初学者来说,如何优雅的写好一个聊天消息列表是非常麻烦的事情,刚开始使用网易云demo中的UI库,但是该库特别沉重,就其中一些群,聊天室来说。我们可能是不需要的,引入进来就会增加apk的大小。后来我引用github上的一些开源库来实现,后来因为一些需求要更改,也特别麻烦,就有了自己写的想法。
这里写图片描述

设计思想

简单说一下该demo的设计思想,其实和大部分列表显示不同类型的Item差不多,使用Apdater中的getItemViewType方法给父类返回这Item所需要的布局。我这里借鉴了网易云demo中的实现方法。将每个Item布局分别继承一个基类,这样继承的类就可以专注一种类型的实现。不必关心其他类。

  1. 创建一个抽象的用于被继承的基类MessageViewHelper和一个继承该类的BaseMessageViewHelper。
  2. 创建一个工厂类用来储存继承自MessageViewHelper的子类,以及根据消息类型不同返回不同的Helper类。
  3. 在Adapter的onBindViewHolder方法中,我们获取到当前的消息类型,根据不同的消息类型,通过反射获得继承自MessageViewHelper的子类。得到实例之后helper.convert(holder, mDatas[position], position)实现基类抽象方法,将数据传递到子类中。
  4. 准备工作都做完了,接下来就是实现Helper,首先实现BaseMessageViewHelper,该类用来实现头像显示,判断消息方向,做一些通用的方法判断之类

大概的思路就是这样,接下来展示一些核心代码,我会带你一步步实现。代码我使用的是kotlin,当然了,我不是为了装逼,这段时间刚好在学习这个,就想试着用用。不熟悉语法不要紧,思路是一样的。

第一步 MessageViewHelper

abstract class MessageViewHelper<out ADAPTER : RecyclerView.Adapter<MessageViewHolder>, in HOLDER : MessageViewHolder, in DATA>(adapter: ADAPTER){

    private val mAdapter:ADAPTER = adapter

     fun getAdapter() : ADAPTER{
        return mAdapter
    }

  //传递Adapter中的数据到Helper中
    abstract fun convert(holder: HOLDER, data: DATA, position: Int)
}

这个类特别简单,跟我上面介绍的一样,实现了Adapter,MessageViewHolder,Data(数据model)三个泛型和一个传递Adapter的构造函数。这几个泛型,是为了convert这个方法做服务。

第二步 ViewHelperFactory工厂类

class ViewHelperFactory {

    companion object {
        private val viewHelpers: HashMap<MessageType, Class<out BaseMessageViewHelper>> = HashMap()

        /**
         * 注册消息类型
         */
        fun register(messageType: MessageType, viewhelper: Class<out BaseMessageViewHelper>) {
            viewHelpers.put(messageType, viewhelper)
        }


        /**
         * 获取所有继承自BaseMessageViewHelper的子类
         */
        fun getAllViewHolders(): List<Class<out BaseMessageViewHelper>> {
            val list = ArrayList<Class<out BaseMessageViewHelper>>()
            list.add(TextViewHelper::class.java)
            list.add(UnknownViewHelper::class.java)
            when {
                viewHelpers.size > 0 -> list.addAll(viewHelpers.values)
            }
            return list
        }

        /**
         * 不同的消息类型返回不同的Helper
         */
        fun getViewHolderByType(message: IMessage): Class<out BaseMessageViewHelper> {
            when {
                message.getMsgType() == MessageType.text -> return TextViewHelper::class.java
                else -> {
                    var helper: Class<out BaseMessageViewHelper>? = null
                    while (helper == null && viewHelpers.size>0) {
                        helper = viewHelpers[message.getMsgType()]
                    }
                    return if (helper==null) UnknownViewHelper::class.java else  helper
                }
            }

        }
    }
}

getAllViewHolders()这个静态方法,返回所有你继承自BaseMessageViewHelper的子类集合。getViewHolderByType()根据消息类型的返回与之匹配的Helper,其中TextViewHelper是我实现的一个文本显示的helper。

第三步 BaseRecyclerAdapter 适配器

这个类太长,我分开来说,

     helperViewType = HashMap()
     val list: List<Class<out BaseMessageViewHelper>> = ViewHelperFactory.getAllViewHolders()
     var viewType = 0
     for (helper: Class<out BaseMessageViewHelper> in list) {
         viewType++
         addItemType(viewType, R.layout.im_base_layout, helper)
         helperViewType[helper] = viewType
     }

helperViewType 是一个Map集合,使用工厂类中的getAllViewHolders()取得所有的Helper,在循环中将Helper根据viewType存储到Map中。


        /**
     * viewType->布局
     */
    private var layouts: SparseArray<Int>? = null

    /**
     * viewType->helper类
     */
    private var helperClasses: SparseArray<Class<out BaseMessageViewHelper>>? = null

    /**
     * viewType->实例化helper
     */
    private var typeViewHelper: MutableMap<Int, HashMap<String, BaseMessageViewHelper>>? = null

    ........

    private fun addItemType(type: Int, layout: Int, helper: Class<out BaseMessageViewHelper>) {
        if (layouts == null) {
            layouts = SparseArray()
        }
        layouts!!.put(type, layout)

        if (helperClasses == null) {
            helperClasses = SparseArray()
        }
        helperClasses!!.put(type, helper)


        if (typeViewHelper == null) typeViewHelper = HashMap()
        typeViewHelper!!.put(type, HashMap())
    }

addItemType中,layouts中存储基类的布局资源,helperClasses中存储Helper类型类,typeViewHelper存储实例化的Helper,这个集合是为了避免重复实例化Helper所设置的;接下来就是获取layouts 中存储的布局资源,设置到MessageViewHolder中。

   override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): MessageViewHolder {
        this.mLayoutInflater = LayoutInflater.from(mContext)
        return onCreateBaseViewHolder(parent!!, viewType)
    }

......


  /**
     * 这里获取layouts中存储的布局资源,生成View,放入MessageViewHolder中
     */
    private fun onCreateBaseViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {

        return MessageViewHolder(getItemView(layouts!![viewType], parent))
    }


    private fun getItemView(layoutResId: Int, parent: ViewGroup): View {
        return mLayoutInflater.inflate(layoutResId, parent, false)
    }

布局和返回的类型都设置好了,现在就是如何显示这些数据。这里也是第三步的核心。在onBindViewHolder方法中获取到当前的item类型,首先判断typeViewHelper中是否有了该类型的Helper,如果没有,就根据类型获取helperClasses其中的Helper,在通过反射获取到实例,将获取的到的实例存储到typeViewHelper中。最后把数据通过MessageViewHelper中的convert方法传递到它的子类中取。代码如下:

    override fun onBindViewHolder(holder: MessageViewHolder?, position: Int) {
        val itemType: Int = holder!!.itemViewType
        val itemKey: String = getItemKey(mDatas[position])
        var helper: BaseMessageViewHelper? = typeViewHelper?.get(itemType)?.get(itemKey)
        if (helper == null) {
            try {
                val cls: Class<out BaseMessageViewHelper> = helperClasses!!.get(itemType)
                val constructor = cls.declaredConstructors[0]
                constructor.isAccessible = true
                helper = constructor.newInstance(this) as BaseMessageViewHelper?
                typeViewHelper!![itemType]!!.put(itemKey, helper!!)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        if (helper != null) {
            helper.convert(holder, mDatas[position], position)
        }
    }

全部代码我就不贴了,核心的都在这里。

第四步 BaseMessageViewHelper 统一设置

这个类就非常容易理解了,他是继承自MessageViewHelper类的子类,所以他实现了父类的抽象方法convert,从而就拿到了Adapter中的消息数据和View,拿到这些就可以做一些操作了,头像,消息背景,点击事件等,例如下面这些:

 /**
     * 设置列表的点击事件
     */
    private fun setOnClick() {
        val helperListener: IMListEventListener = getAdapter().getHelperEvent() ?: return
        mLayoutContent.setOnClickListener {
            helperListener.onItemClick(mData)
        }
        mLayoutContent.setOnLongClickListener {
            helperListener.onItemLongClick(mData)
            false
        }
        mLeftAvatar.setOnClickListener {
            helperListener.onLeftAvatar(mData)
        }

        mRightAvatar.setOnClickListener {
            helperListener.onRightAvatar(mData)
        }
    }

    /**
     * 设置内容布局显示
     */
    @SuppressLint("RtlHardcoded")
    private fun setContentView() {
        val bodylayout: LinearLayout = findViewById(R.id.im_base_body)
        if (isMiddleItem()) {
            setGravity(bodylayout, Gravity.CENTER)
        } else {
            if (isMsgDirection()) {
                setGravity(bodylayout, Gravity.LEFT)
                mLayoutContent.setBackgroundResource(leftBackground())
            } else {
                setGravity(bodylayout, Gravity.RIGHT)
                mLayoutContent.setBackgroundResource(rightBackground())
            }
        }
    }

到这里就将整个列表的实现过程表述完了,可以直接下载demo,看我里面如何实现,如果有什么不明白的可以留言。我会尽量及时回复。

项目地址

猜你喜欢

转载自blog.csdn.net/sinat_32089827/article/details/79653667