You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android RecyclerView接入Lottie动画后scrollToPosition失效及条目异常问题

问题根源

  1. ViewHolder复用导致动画监听冲突
    在bind方法中每次绑定数据都新增一个AnimatorListener,但从未移除旧监听。当ViewHolder被复用到其他位置时,残留的旧监听触发时会携带首次绑定的旧数据,直接导致条目内容错乱,也就是第一条消息出现在最新位置的问题。
  2. 异步动画导致item高度测量错乱
    调用scrollToPosition时Lottie动画正在显示,RecyclerView按照当前Lottie的高度计算滚动位置。等动画结束隐藏Lottie、显示消息卡片后,该item的实际高度发生了变化,但RecyclerView不会重新触发滚动逻辑,就会停在之前计算的半中间位置,出现滚动不到底的问题。
  3. 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单独处理,不要混入普通消息列表:
    1. 新增itemType对应输入提示布局
    2. 对方输入时仅控制Footer显示/隐藏,不需要修改消息列表数据,从根源上避免普通消息条目被影响的问题。

内容的提问来源于stack exchange,提问作者AndroidDev123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.15 20:45:04