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

RecyclerView动态增删时Choreography推入动画实现咨询

嘿,你想要实现的这种“新元素插入时把其他元素平滑推开”的动画,正是Material Design里推荐的列表项移动动效,用Android的Choreographer配合RecyclerView完全可以实现,我给你拆解下具体的实现思路:

核心原理梳理

本质上我们需要控制两个同步的动画过程:

  • 新插入的列表项从0高度(或收缩状态)逐渐展开到目标高度
  • 列表中其他受影响的项,根据新项的位置向上/向下平滑移动,让出空间

RecyclerView自带的ItemAnimator是实现这类动画的基础,但原生的DefaultItemAnimator效果不够精准,所以我们需要自定义ItemAnimator,再结合Choreographer来逐帧控制动画进度,保证动效的流畅性。

具体实现步骤

1. 自定义RecyclerView.ItemAnimator

这是实现动画的核心类,我们需要重写animateAdd(处理插入动画)、animateMove(处理其他项的移动动画)等关键方法:

  • 初始状态设置:插入新项时,先把它的高度设为0,同时记录其他需要移动的项的原始位置和目标位置
  • 测量目标尺寸:提前测量新项的最终高度(用View.measure()方法),明确动画要展开到的尺寸
  • 动画帧控制:借助Choreographer监听屏幕的每一次刷新,逐帧计算动画进度,更新视图的尺寸和位置

2. 用Choreographer实现逐帧动画

Choreographer是Android协调UI渲染的核心类,用它能让动画和屏幕刷新同步,避免卡顿:

  • 获取实例:Choreographer.getInstance()
  • 用postFrameCallback注册帧回调,在回调里用插值器(推荐AccelerateDecelerateInterpolator)计算自然的动画进度
  • 每次回调中,更新新项的高度和其他项的位置,然后调用requestLayout()刷新视图
  • 动画结束后及时移除回调,避免资源浪费

3. 处理其他列表项的移动逻辑

当新项插入到列表中间时:

  • 新项上方的所有列表项需要向上移动,移动距离等于新项的目标高度
  • 新项下方的所有列表项需要向下移动,移动距离同样等于新项的目标高度
  • 动画开始前,要先记录这些项的原始Y坐标,动画过程中根据进度计算当前的Y坐标,更新布局参数

4. 关键代码示例(Kotlin)

这里给你一个简化版的自定义ItemAnimator核心代码:

class PushInsertAnimator : RecyclerView.ItemAnimator() {
    private val choreographer = Choreographer.getInstance()
    private val interpolator = AccelerateDecelerateInterpolator()
    private val animatingAddItems = mutableListOf<AddAnimationData>()
    private val animatingMoveItems = mutableListOf<MoveAnimationData>()
    private lateinit var recyclerView: RecyclerView

    // 存储插入项的动画数据
    data class AddAnimationData(
        val holder: RecyclerView.ViewHolder,
        val startHeight: Int,
        val targetHeight: Int,
        val startTime: Long
    )

    // 存储移动项的动画数据
    data class MoveAnimationData(
        val holder: RecyclerView.ViewHolder,
        val startY: Int,
        val targetY: Int,
        val startTime: Long
    )

    override fun setRecyclerView(recyclerView: RecyclerView) {
        this.recyclerView = recyclerView
        super.setRecyclerView(recyclerView)
    }

    override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
        val itemView = holder.itemView
        // 初始状态:高度为0
        itemView.layoutParams.height = 0
        itemView.requestLayout()

        // 测量目标高度
        itemView.measure(
            View.MeasureSpec.makeMeasureSpec(itemView.parent.width, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        )
        val targetHeight = itemView.measuredHeight

        animatingAddItems.add(AddAnimationData(holder, 0, targetHeight, System.currentTimeMillis()))
        
        // 记录需要移动的其他项的位置
        recordMoveAnimations(holder.adapterPosition)
        
        choreographer.postFrameCallback(frameCallback)
        return true
    }

    private fun recordMoveAnimations(insertPosition: Int) {
        // 遍历RecyclerView的所有holder,筛选出需要移动的项
        for (i in 0 until recyclerView.childCount) {
            val child = recyclerView.getChildAt(i)
            val holder = recyclerView.getChildViewHolder(child)
            val adapterPos = holder.adapterPosition
            if (adapterPos != RecyclerView.NO_POSITION && adapterPos != insertPosition) {
                val targetY = if (adapterPos < insertPosition) {
                    child.top - animatingAddItems[0].targetHeight
                } else {
                    child.top + animatingAddItems[0].targetHeight
                }
                animatingMoveItems.add(MoveAnimationData(holder, child.top, targetY, System.currentTimeMillis()))
            }
        }
    }

    private val frameCallback = Choreographer.FrameCallback { frameTime ->
        var isAnimating = false

        // 处理插入项的展开动画
        val addIterator = animatingAddItems.iterator()
        while (addIterator.hasNext()) {
            val data = addIterator.next()
            val elapsed = frameTime - data.startTime
            val duration = 300L
            val progress = if (elapsed >= duration) 1f else interpolator.getInterpolation(elapsed / duration.toFloat())

            // 更新高度
            val currentHeight = (data.startHeight + (data.targetHeight - data.startHeight) * progress).toInt()
            data.holder.itemView.layoutParams.height = currentHeight
            data.holder.itemView.requestLayout()

            if (progress >= 1f) {
                addIterator.remove()
                dispatchAddFinished(data.holder)
            } else {
                isAnimating = true
            }
        }

        // 处理其他项的移动动画
        val moveIterator = animatingMoveItems.iterator()
        while (moveIterator.hasNext()) {
            val data = moveIterator.next()
            val elapsed = frameTime - data.startTime
            val duration = 300L
            val progress = if (elapsed >= duration) 1f else interpolator.getInterpolation(elapsed / duration.toFloat())

            // 更新Y位置
            val currentY = (data.startY + (data.targetY - data.startY) * progress).toInt()
            data.holder.itemView.translationY = (currentY - data.holder.itemView.top).toFloat()

            if (progress >= 1f) {
                moveIterator.remove()
                data.holder.itemView.translationY = 0f
                dispatchMoveFinished(data.holder)
            } else {
                isAnimating = true
            }
        }

        if (isAnimating) {
            choreographer.postFrameCallback(this)
        } else {
            dispatchAnimationsFinished()
        }
    }

    // 记得重写其他必要方法,如animateRemove、endAnimation等,保证动画的完整性
    override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
        // 移除动画逻辑可参考插入动画反向实现
        return super.animateRemove(holder)
    }

    override fun endAnimation(holder: RecyclerView.ViewHolder) {
        animatingAddItems.removeAll { it.holder == holder }
        animatingMoveItems.removeAll { it.holder == holder }
        super.endAnimation(holder)
    }
}

然后给你的RecyclerView设置这个动画器:

recyclerView.itemAnimator = PushInsertAnimator()
额外优化建议
  • 动画时长建议设为250-300ms,符合Material Design的动效规范
  • 不要用notifyDataSetChanged(),改用notifyItemInserted(position),这样RecyclerView只会刷新受影响的项,提升性能
  • 如果觉得自定义ItemAnimator太繁琐,可以试试Material Components库中的相关动效组件,但自定义方案的灵活性最高,能完全匹配你想要的推挤效果

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 04:12:00