10、动画与布局:LayoutTransition布局动画、ViewGroup子View添加/删除动画、animateLayoutChanges属性

布局动画这个话题,说实话,在早期的Android开发中经常被忽略。我见过太多应用在添加或删除列表项时,子View突然出现或消失,用户体验非常生硬。其实Android早就为我们提供了非常优雅的解决方案——LayoutTransition。

今天我们就来聊聊,如何让ViewGroup中的子View在添加、删除、显示、隐藏时,拥有流畅的过渡动画。

10.1 什么是LayoutTransition?

LayoutTransition,说白了就是ViewGroup在布局发生变化时,自动触发的动画机制。它主要处理四种场景:

  • APPEARING —— 子View被添加时
  • DISAPPEARING —— 子View被移除时
  • CHANGE_APPEARING —— 因其他View出现而受到影响的View
  • CHANGE_DISAPPEARING —— 因其他View消失而受到影响的View

我个人习惯把这四种动画类型称为「入场、退场、被挤开、被拉回」。这样好记多了。

核心要点:LayoutTransition只对ViewGroup的直接子View生效。如果你嵌套了多层布局,只有最外层的子View变化会触发动画。

10.2 基础用法:XML方式

最简单的用法,就是在布局文件中直接设置animateLayoutChanges属性。我在项目中第一次接触这个属性时,真的被惊艳到了——一行代码就能让整个布局动起来。

<LinearLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:animateLayoutChanges="true">

    <Button
        android:id="@+id/btn_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="添加子View"/>

</LinearLayout>

然后你在代码中动态添加或删除子View时,系统会自动播放默认的淡入淡出和位移动画。嗯,这里要注意:默认动画效果比较基础,如果你想定制,就得用代码创建LayoutTransition对象。

10.3 代码定制LayoutTransition

我曾经在一个即时通讯项目中,需要让消息气泡在插入时从右侧滑入,删除时向左滑出。默认的淡入淡出显然不够用。于是我自己定制了一个LayoutTransition。

val transition = LayoutTransition()

// 自定义APPEARING动画:从右侧滑入
val appearingAnim = ObjectAnimator.ofFloat(
    null, "translationX", 300f, 0f
).apply {
    duration = 400
    interpolator = OvershootInterpolator()
}
transition.setAnimator(LayoutTransition.APPEARING, appearingAnim)

// 自定义DISAPPEARING动画:向左滑出并淡出
val disappearingAnim = AnimatorSet().apply {
    playTogether(
        ObjectAnimator.ofFloat(null, "translationX", 0f, -300f),
        ObjectAnimator.ofFloat(null, "alpha", 1f, 0f)
    )
    duration = 300
}
transition.setAnimator(LayoutTransition.DISAPPEARING, disappearingAnim)

// 设置到ViewGroup
container.layoutTransition = transition

你想想看,这里有个坑:setAnimator方法中的第一个参数是null,表示这个动画会应用到所有子View上。如果你想针对不同子View做不同动画,那就得自己写AnimatorListener了。

避坑指南:我曾经在RecyclerView中使用LayoutTransition,结果导致列表滑动卡顿。原因是LayoutTransition会监听所有子View的变化,而RecyclerView的复用机制会频繁触发这些事件。所以,不要在RecyclerView中使用LayoutTransition。用ItemAnimator代替。

10.4 animateLayoutChanges属性的底层原理

为什么一个简单的android:animateLayoutChanges="true"就能让布局动起来?其实它的背后就是LayoutTransition。

当你在XML中设置这个属性时,系统会在ViewGroup初始化时自动创建一个默认的LayoutTransition对象,并赋值给layoutTransition属性。默认的动画效果是:

动画类型 默认效果 持续时间
APPEARING 淡入 + 从上方滑入 300ms
DISAPPEARING 淡出 + 向下方滑出 300ms
CHANGE_APPEARING 位移动画 300ms
CHANGE_DISAPPEARING 位移动画 300ms

说白了,这个属性就是一个「懒人开关」。如果你对动画效果没有特殊要求,用它就够了。但如果你想要更精细的控制,还是得自己创建LayoutTransition。

10.5 实战:动态添加和删除子View

我们来看一个完整的例子。假设你有一个垂直排列的LinearLayout,用户点击按钮时动态添加标签,点击标签上的叉号时删除它。

class TagLayoutActivity : AppCompatActivity() {

    private lateinit var container: LinearLayout
    private var tagCount = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_tag_layout)

        container = findViewById(R.id.container)
        container.layoutTransition = LayoutTransition()

        findViewById<Button>(R.id.btn_add).setOnClickListener {
            addTag("标签 ${++tagCount}")
        }
    }

    private fun addTag(text: String) {
        val tagView = LayoutInflater.from(this)
            .inflate(R.layout.item_tag, container, false) as TextView

        tagView.text = text
        tagView.setOnClickListener {
            // 删除自身
            container.removeView(it)
        }

        container.addView(tagView)
    }
}

这里有个细节:removeView会触发DISAPPEARING动画,而addView会触发APPEARING动画。同时,其他子View的位置变化会触发CHANGE_APPEARING或CHANGE_DISAPPEARING动画。

个人经验:如果你发现动画执行时出现闪烁或跳帧,可以尝试给LayoutTransition设置setStartDelaysetDuration。我一般会把APPEARING的持续时间设为350ms,DISAPPEARING设为250ms。这样入场比退场稍慢,视觉上更自然。

10.6 知识体系总览

为了让你更直观地理解布局动画的整体结构,我画了一张图:

布局动画知识体系 LayoutTransition APPEARING 子View添加时 DISAPPEARING 子View移除时 CHANGE_APPEARING 其他View出现受影响 CHANGE_DISAPPEARING 其他View消失受影响 animateLayoutChanges="true" 是懒人开关

10.7 性能与注意事项

布局动画虽然好用,但也不是万能的。我总结了几条经验:

  • 不要用在RecyclerView中 —— 前面已经说过了,这是血泪教训。
  • 避免在动画执行期间频繁操作布局 —— 如果你在动画还没结束时又添加或删除子View,动画队列会堆积,导致卡顿。
  • 注意内存泄漏 —— LayoutTransition内部持有Animator引用,如果你在Activity销毁时没有清理,可能会造成泄漏。建议在onDestroy中调用container.layoutTransition = null
  • 动画时长不要超过500ms —— 布局动画通常用于列表或标签的增删,用户期望的是快速反馈。太长的动画会让用户觉得卡。

小技巧:如果你想让动画更平滑,可以配合ViewGroup.setClipChildren(false)使用。这样子View在动画过程中不会被父布局裁剪,效果更自然。不过要注意,这个属性会影响绘制性能,只在必要时开启。

好了,布局动画的内容就聊到这里。记住一句话:好的动画让用户感觉不到动画的存在,它只是让交互变得更自然

公众号:蓝海资料掘金营,微信deep3321