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设置setStartDelay和setDuration。我一般会把APPEARING的持续时间设为350ms,DISAPPEARING设为250ms。这样入场比退场稍慢,视觉上更自然。
10.6 知识体系总览
为了让你更直观地理解布局动画的整体结构,我画了一张图:
10.7 性能与注意事项
布局动画虽然好用,但也不是万能的。我总结了几条经验:
- 不要用在RecyclerView中 —— 前面已经说过了,这是血泪教训。
- 避免在动画执行期间频繁操作布局 —— 如果你在动画还没结束时又添加或删除子View,动画队列会堆积,导致卡顿。
- 注意内存泄漏 —— LayoutTransition内部持有Animator引用,如果你在Activity销毁时没有清理,可能会造成泄漏。建议在onDestroy中调用
container.layoutTransition = null。 - 动画时长不要超过500ms —— 布局动画通常用于列表或标签的增删,用户期望的是快速反馈。太长的动画会让用户觉得卡。
小技巧:如果你想让动画更平滑,可以配合ViewGroup.setClipChildren(false)使用。这样子View在动画过程中不会被父布局裁剪,效果更自然。不过要注意,这个属性会影响绘制性能,只在必要时开启。
好了,布局动画的内容就聊到这里。记住一句话:好的动画让用户感觉不到动画的存在,它只是让交互变得更自然。