【IM】網易lM聊天列表UI
對於一個初學者來說,如何優雅的寫好一個聊天訊息列表是非常麻煩的事情,剛開始使用網易雲demo中的UI庫,但是該庫特別沉重,就其中一些群,聊天室來說。我們可能是不需要的,引入進來就會增加apk的大小。後來我引用github上的一些開源庫來實現,後來因為一些需求要更改,也特別麻煩,就有了自己寫的想法。
設計思想
簡單說一下該demo的設計思想,其實和大部分列表顯示不同型別的Item差不多,使用Apdater中的getItemViewType方法給父類返回這Item所需要的佈局。我這裡借鑑了網易雲demo中的實現方法。將每個Item佈局分別繼承一個基類,這樣繼承的類就可以專注一種型別的實現。不必關心其他類。
- 建立一個抽象的用於被繼承的基類MessageViewHelper和一個繼承該類的BaseMessageViewHelper。
- 建立一個工廠類用來儲存繼承自MessageViewHelper的子類,以及根據訊息型別不同返回不同的Helper類。
- 在Adapter的onBindViewHolder方法中,我們獲取到當前的訊息型別,根據不同的訊息型別,通過反射獲得繼承自MessageViewHelper的子類。得到例項之後
helper.convert(holder, mDatas[position], position)
實現基類抽象方法,將資料傳遞到子類中。 - 準備工作都做完了,接下來就是實現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,看我裡面如何實現,如果有什麼不明白的可以留言。我會盡量及時回覆。