16、动画与过渡(Transition):Activity过渡动画、Fragment过渡动画、共享元素过渡
动画做得好,用户觉得顺滑。过渡做得好,用户觉得自然。但很多人把这两件事混为一谈。
我个人的理解是:动画是单个视图的表演,过渡是多个页面之间的衔接。今天我们就来聊聊 Android 里的过渡(Transition),包括 Activity 之间、Fragment 之间,以及那个让人又爱又恨的共享元素过渡。
核心观点:过渡动画不是锦上添花,而是用户体验的骨架。好的过渡让用户知道「我从哪来,到哪去」。
16.1 Activity 过渡动画
Activity 之间的过渡,说白了就是两个窗口的切换。早期我们用 overridePendingTransition,后来有了更现代的 ActivityOptions。
16.1.1 传统方式:overridePendingTransition
这个方法从 API 5 就有了。你调用 startActivity() 或 finish() 之后,紧跟着调用它就行。
// 启动 Activity
startActivity(Intent(this, DetailActivity::class.java))
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
// 返回时
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
我在项目中遇到过一个问题:overridePendingTransition 必须在 startActivity 或 finish 之后立即调用,否则动画不会生效。嗯,说白了就是系统需要紧跟着事务提交来执行动画。
小技巧:如果你用 Kotlin 的扩展函数,可以封装一个 Activity.startActivityWithAnim(),省得每次都写两行。
16.1.2 现代方式:ActivityOptions + 场景过渡
从 Android 5.0(API 21)开始,Google 引入了更强大的场景过渡(Scene Transition)。你可以在主题里声明,也可以用代码动态设置。
// 主题中启用
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowActivityTransitions">true</item>
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/fade</item>
</style>
// 或者代码设置
window.enterTransition = Explode()
window.exitTransition = Fade()
启动时用 ActivityOptions.makeSceneTransitionAnimation() 包裹 Intent:
val options = ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(Intent(this, DetailActivity::class.java), options.toBundle())
我个人习惯用代码设置,因为主题里写死了不好调试。你想想看,如果某个页面不需要过渡,你还得单独写个主题覆盖,多麻烦。
16.2 Fragment 过渡动画
Fragment 的过渡比 Activity 灵活得多。你可以给进入、退出、弹出分别指定动画。
16.2.1 使用 setCustomAnimations
supportFragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.slide_in_right, // 进入
R.anim.slide_out_left, // 退出
R.anim.slide_in_left, // 弹出进入
R.anim.slide_out_right // 弹出退出
)
.replace(R.id.container, DetailFragment())
.addToBackStack(null)
.commit()
这里有个坑:setCustomAnimations 必须在 replace 或 add 之前调用。我曾经因为顺序写反了,动画死活不生效,查了半天才发现是调用顺序的问题。
16.2.2 使用 Transition 框架
如果你想要更丰富的效果,可以用 TransitionInflater 加载 XML 过渡资源:
// Fragment 中
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.slide_right)
exitTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.fade)
}
注意:Fragment 的 Transition 框架要求 API 21+。如果你的 minSdk 低于 21,记得做版本判断,或者用兼容库中的 FragmentTransaction.setCustomAnimations()。
16.3 共享元素过渡(Shared Element Transition)
这是过渡动画里最炫酷的部分。两个页面之间,同一个元素「飞」过去,用户视觉上感觉是连续的。说白了,就是让用户觉得「我没跳转,我只是放大了这个图片」。
16.3.1 基本用法
首先,给两个页面的共享元素设置相同的 transitionName:
<!-- 列表页的 ImageView -->
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="80dp"
android:layout_height="80dp"
android:transitionName="avatar_${userId}"
... />
<!-- 详情页的 ImageView -->
<ImageView
android:id="@+id/iv_avatar_large"
android:layout_width="200dp"
android:layout_height="200dp"
android:transitionName="avatar_${userId}"
... />
然后启动 Activity 时指定共享元素:
val options = ActivityOptions.makeSceneTransitionAnimation(
this,
ivAvatar, // 起始 View
"avatar_${userId}" // transitionName
)
startActivity(Intent(this, DetailActivity::class.java), options.toBundle())
16.3.2 多个共享元素
有时候你需要同时过渡多个元素,比如头像和标题一起飞过去。用 Pair 搞定:
val options = ActivityOptions.makeSceneTransitionAnimation(
this,
Pair(ivAvatar, "avatar_${userId}"),
Pair(tvName, "name_${userId}")
)
startActivity(Intent(this, DetailActivity::class.java), options.toBundle())
关键点:共享元素的 transitionName 必须全局唯一。我习惯用 类型_ID 的命名方式,比如 avatar_123、cover_456,避免冲突。
16.3.3 自定义共享元素过渡
默认的共享元素过渡是 ChangeBounds(改变位置和大小)。但如果你要过渡图片,最好加上 ChangeImageTransform:
// 详情页 Fragment 中
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = TransitionSet().apply {
addTransition(ChangeBounds())
addTransition(ChangeImageTransform())
duration = 300
}
}
我在项目中遇到过一个问题:共享元素过渡时,图片会先闪一下白色背景,然后才显示。后来发现是因为 ImageView 没有设置背景色,系统在过渡时无法正确计算边界。解决方案是给 ImageView 设置一个 android:background,哪怕只是透明色。
16.3.4 Fragment 中的共享元素
Fragment 之间也可以共享元素,用法类似:
supportFragmentManager.beginTransaction()
.addSharedElement(ivAvatar, "avatar_${userId}")
.addSharedElement(tvName, "name_${userId}")
.replace(R.id.container, DetailFragment())
.addToBackStack(null)
.commit()
避坑指南:我曾经在 Fragment 共享元素过渡时,发现动画执行到一半就卡住了。原因是目标 Fragment 的 onCreateView 里没有及时返回共享元素的 View。记住:共享元素的 View 必须在 onCreateView 中立即返回,不能延迟加载。
16.4 实战经验总结
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 简单 Activity 跳转 | overridePendingTransition |
必须在 startActivity/finish 后立即调用 |
| 复杂 Activity 过渡 | ActivityOptions.makeSceneTransitionAnimation |
需要 API 21+,主题中启用 windowActivityTransitions |
| Fragment 切换 | setCustomAnimations |
动画资源必须放在 res/anim 目录 |
| 图片列表到详情 | 共享元素 + ChangeImageTransform | transitionName 唯一,ImageView 设置背景色 |
| 多元素同时过渡 | Pair 方式传递多个共享元素 | 每个元素的 transitionName 不能重复 |
最后说一句:过渡动画不是越多越好。我见过一些应用,每个页面切换都来一个旋转加缩放,看得人头晕。好的过渡是「润物细无声」的——用户觉得流畅,但说不出哪里做了动画。这才是高手。