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
相关产品推荐
相关产品推荐

