Android RecyclerView接入Lottie动画后scrollToPosition失效及条目异常问题
问题根源
- ViewHolder复用导致动画监听冲突
在bind方法中每次绑定数据都新增一个AnimatorListener,但从未移除旧监听。当ViewHolder被复用到其他位置时,残留的旧监听触发时会携带首次绑定的旧数据,直接导致条目内容错乱,也就是第一条消息出现在最新位置的问题。 - 异步动画导致item高度测量错乱
调用scrollToPosition时Lottie动画正在显示,RecyclerView按照当前Lottie的高度计算滚动位置。等动画结束隐藏Lottie、显示消息卡片后,该item的实际高度发生了变化,但RecyclerView不会重新触发滚动逻辑,就会停在之前计算的半中间位置,出现滚动不到底的问题。 - DiffUtil实现不规范
当前的areItemsTheSame直接对比对象相等,没有使用消息的唯一ID作为判断依据,会导致DiffUtil计算更新时出现判断错误,加重条目复用混乱的问题。
解决方案
- 修复动画监听重复添加问题
在bind方法添加新监听前,先移除旧的监听,同时控制Lottie的显示逻辑,仅当消息处于“正在输入”状态时才播放动画,否则直接显示消息内容:
// 在MessageAppViewHolder类中先定义 listener 成员变量,避免每次新建 private var animatorListener: Animator.AnimatorListener? = null fun bind(item: ReplaceAppModel?) = with(binding) { // 先移除旧监听,防止复用冲突 animatorListener?.let { typingAnimation.removeAnimatorListener(it) } // 重置视图状态 typingAnimation.visibility = View.GONE messageCv.visibility = View.GONE timestamp.visibility = View.GONE item ?: return@with // 自行给ReplaceAppModel添加isTyping字段控制输入状态 if (item.isTyping) { typingAnimation.visibility = View.VISIBLE animatorListener = object : Animator.AnimatorListener { override fun onAnimationEnd(animation: Animator?) { // 动画结束后刷新当前条目 item.isTyping = false bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { pos -> binding.root.post { (binding.root.parent as RecyclerView).adapter?.notifyItemChanged(pos) } } } override fun onAnimationRepeat(animation: Animator?) {} override fun onAnimationCancel(animation: Animator?) {} override fun onAnimationStart(animation: Animator?) {} } typingAnimation.addAnimatorListener(animatorListener!!) typingAnimation.playAnimation() } else { // 非输入状态直接显示消息 message.text = HtmlCompat.fromHtml(item.message, HtmlCompat.FROM_HTML_MODE_LEGACY) timestamp.text = dateUtil.getCurrentTimestampEDDMMMYYYYHHMM() messageCv.visibility = View.VISIBLE timestamp.visibility = View.VISIBLE } }
- 修正DiffUtil实现
将areItemsTheSame的判断逻辑改为对比消息唯一标识:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { // 替换成ReplaceAppModel中实际的消息唯一ID字段 return oldList[oldItemPosition].msgId == newList[newItemPosition].msgId }
- 优化滚动逻辑
调用完更新列表的方法后,等RecyclerView布局完成再触发滚动,确保高度计算准确:
adapter.addChat(newMessageList) recyclerView.postOnAnimation { linearLayoutManager.scrollToPosition(adapter.itemCount - 1) }
- (推荐)优化输入提示的实现方式
参考主流聊天应用的实现,将“对方正在输入”的提示作为RecyclerView的FooterView单独处理,不要混入普通消息列表:- 新增itemType对应输入提示布局
- 对方输入时仅控制Footer显示/隐藏,不需要修改消息列表数据,从根源上避免普通消息条目被影响的问题。
内容的提问来源于stack exchange,提问作者AndroidDev123
相关产品推荐
相关产品推荐

