1. 程式人生 > >【IM】網易lM聊天列表UI

【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,看我裡面如何實現,如果有什麼不明白的可以留言。我會盡量及時回覆。

專案地址