16、动画与过渡(Transition):Activity过渡动画、Fragment过渡动画、共享元素过渡

动画做得好,用户觉得顺滑。过渡做得好,用户觉得自然。但很多人把这两件事混为一谈。

我个人的理解是:动画是单个视图的表演,过渡是多个页面之间的衔接。今天我们就来聊聊 Android 里的过渡(Transition),包括 Activity 之间、Fragment 之间,以及那个让人又爱又恨的共享元素过渡。

核心观点:过渡动画不是锦上添花,而是用户体验的骨架。好的过渡让用户知道「我从哪来,到哪去」。

Android 过渡动画体系 Activity 过渡 Fragment 过渡 共享元素过渡 overridePendingTransition ActivityOptions.makeSceneTransition setCustomAnimations TransitionInflater transitionName 配对 ChangeBounds / ChangeImageTransform ⚠ 常见坑点 transitionName 不匹配 / 动画与生命周期冲突 / 共享元素未设置背景

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 必须在 startActivityfinish 之后立即调用,否则动画不会生效。嗯,说白了就是系统需要紧跟着事务提交来执行动画。

小技巧:如果你用 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 必须在 replaceadd 之前调用。我曾经因为顺序写反了,动画死活不生效,查了半天才发现是调用顺序的问题。

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_123cover_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 不能重复

最后说一句:过渡动画不是越多越好。我见过一些应用,每个页面切换都来一个旋转加缩放,看得人头晕。好的过渡是「润物细无声」的——用户觉得流畅,但说不出哪里做了动画。这才是高手。


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