对于一个初学者来说,如何优雅的写好一个聊天消息列表是非常麻烦的事情,刚开始使用网易云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,看我里面如何实现,如果有什么不明白的可以留言。我会尽量及时回复。