Android Kotlin聊天应用RecyclerView项背景复用圆角异常问题
我用Kotlin开发Android聊天应用,用RecyclerView展示消息条目。消息条目有8种不同背景,取决于两个条件:
- 是否为本人发送
- 在时间分组中的位置(首项/中间项/末项/单独项)
最初在onBindViewHolder()里每次用ContextCompat.getDrawable()获取Drawable设置背景,显示完全正常;但为了优化性能,我把所有Drawable提前初始化作为Adapter的成员变量复用后,就出现了部分圆角消失、文字溢出背景的绘制异常。
初始正常代码:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val message = ... textTV.background = ContextCompat.getDrawable( context, when { msg.isLastInTimeGroup() && !msg.sentByMe -> R.drawable.chat_message_in_last_bg msg.isFirstInTimeGroup() && !msg.sentByMe -> R.drawable.chat_message_in_first_bg msg.isMiddleInTimeGroup() && !msg.sentByMe -> R.drawable.chat_message_in_middle_bg msg.isLastInTimeGroup() && msg.sentByMe -> R.drawable.chat_message_out_last_bg msg.isFirstInTimeGroup() && msg.sentByMe -> R.drawable.chat_message_out_first_bg msg.isMiddleInTimeGroup() && msg.sentByMe -> R.drawable.chat_message_out_middle_bg else -> if (msg.sentByMe) R.drawable.chat_message_out_bg else R.drawable.chat_message_in_bg } ) }
复用Drawable后的异常代码:
private val inFirstBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageInFirstBg) private val inMiddleBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageInMiddleBg) private val inLastBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageInLastBg) private val inAloneBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageInBg) private val outFirstBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageOutFirstBg) private val outMiddleBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageOutMiddleBg) private val outLastBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageOutLastBg) private val outAloneBg: Drawable? = ContextCompat.getDrawable(ctx, R.drawable.chatMessageOutBg) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val message = ... textTV.background = when { msg.isLastInTimeGroup() && !msg.sentByMe -> inLastBg msg.isFirstInTimeGroup() && !msg.sentByMe -> inFirstBg msg.isMiddleInTimeGroup() && !msg.sentByMe -> inMiddleBg msg.isLastInTimeGroup() && msg.sentByMe -> outLastBg msg.isFirstInTimeGroup() && msg.sentByMe -> outFirstBg msg.isMiddleInTimeGroup() && msg.sentByMe -> outMiddleBg else -> if (msg.sentByMe) outAloneBg else inAloneBg } }
背景资源示例(out_middle):
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@color/blue" /> <corners android:radius="24dp" android:topRightRadius="4dp" android:bottomRightRadius="4dp"/> </shape>
想请教下为什么复用Drawable会导致这些绘制异常?问题出在哪里?
嘿,这个坑我之前踩过!核心原因是同一个Drawable实例不能同时被多个View复用——Drawable是带状态的对象,当你把它设置给一个View后,View会修改它的内部状态(比如尺寸边界、圆角渲染参数),当另一个View再使用这个实例时,就会继承之前被修改过的状态,直接导致各种绘制异常。
具体拆解下问题:
- Drawable状态共享冲突:你初始化的那些全局Drawable实例是单例,RecyclerView的ViewHolder本身就是复用机制,第一个ViewHolder设置这个Drawable后,它的
bounds(边界)会被调整为当前View的大小;当这个Drawable被传给下一个ViewHolder的View时,它会带着之前的边界状态,新View的背景尺寸就会错乱,看起来就是文字溢出或者背景变形。 - 圆角参数被篡改:像你用的shape类型Drawable,它的圆角参数会随着View的尺寸变化、绘制流程被动态调整,复用同一个实例时,后面的View就会用上被改坏的圆角设置,自然出现圆角消失或异常的情况。
给你两个靠谱的解决思路:
方案1:每次使用时创建Drawable副本
不要直接复用原始Drawable,而是每次在onBindViewHolder里获取它的独立副本,确保每个View拿到的都是干净的实例:
// 只存资源ID,不存Drawable实例 private fun getBgResId(msg: Message): Int { return when { msg.isLastInTimeGroup() && !msg.sentByMe -> R.drawable.chat_message_in_last_bg msg.isFirstInTimeGroup() && !msg.sentByMe -> R.drawable.chat_message_in_first_bg msg.isMiddleInTimeGroup() && !msg.sentByMe -> R.drawable.chat_message_in_middle_bg msg.isLastInTimeGroup() && msg.sentByMe -> R.drawable.chat_message_out_last_bg msg.isFirstInTimeGroup() && msg.sentByMe -> R.drawable.chat_message_out_first_bg msg.isMiddleInTimeGroup() && msg.sentByMe -> R.drawable.chat_message_out_middle_bg else -> if (msg.sentByMe) R.drawable.chat_message_out_bg else R.drawable.chat_message_in_bg } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val message = ... // 用mutate()创建可变副本,避免和系统缓存的Drawable共享状态 val bgDrawable = ContextCompat.getDrawable(context, getBgResId(msg))?.mutate() textTV.background = bgDrawable }
这里的mutate()是关键,它会生成一个独立的Drawable实例,彻底避免状态共享问题。
方案2:用DataBinding绑定资源ID(更优雅)
如果项目用了DataBinding,可以直接在布局里绑定背景资源ID,让系统自动处理Drawable的复用和状态管理:
<!-- 消息条目布局 --> <TextView android:id="@+id/textTV" android:background="@{msg.getBgResId()}" ... />
然后在Message类或自定义BindingAdapter里实现getBgResId()方法,系统会自动为每个View创建合适的Drawable实例,既保证性能又不会有状态冲突。
补充一句:你最初的代码虽然每次获取Drawable,但系统内部有Drawable缓存,性能并不会差太多;反而直接复用实例的优化方向错了,才引入了bug,优化性能也要找对场景哦~
内容的提问来源于stack exchange,提问作者Majkeee

