Android 启动画面进阶:自定义退出动画与平滑过渡

回顾:退出时发生了什么

上一篇:Android 启动画面:setKeepOnScreenCondition 到底是怎么工作的?

这一篇往下走一步:当启动画面终于要退出时,怎么接管它的退出过程,做出从 logo 到内容的平滑过渡,而不是”啪”一下生硬切换。

结合上一篇,启动画面的一生大致是这样:

进程启动 → 系统显示 Splash → 每帧检查条件 → 条件满足 → 【退出动画】 → 移除 Splash → 内容可见

默认情况下,【退出动画】这一环是库和系统替你做的:系统版启动画面(Android 12+)会播放一个内置的淡出动画。但如果你想要更有品牌感的过渡——比如图标放大融入内容、整体淡出+轻微缩放——就需要接管这一步,用的就是 setOnExitAnimationListener

核心 API:setOnExitAnimationListener

splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
    // splashScreenViewProvider: SplashScreenViewProvider
}

回调的时机:当条件满足、系统准备移除启动画面时触发。回调里你能拿到一个 SplashScreenViewProvider,它提供了两个关键东西:

属性/方法 说明
provider.view 整个启动画面的 View(android.view.View)
provider.iconView 启动画面的图标 View,可以单独做动画
provider.remove() 手动移除启动画面。必须在动画结束后调用

关键约定:一旦你实现了这个回调,就表示”退出动画由你接管”。如果你不主动调用 remove(),启动画面会一直残留——系统不会帮你清。这是最常见的坑。

入门:一个淡出动画

class MainActivity : ComponentActivity() {
    private var ready = false

    override fun onCreate(savedInstanceState: Bundle?) {
        val splash = installSplashScreen()
        super.onCreate(savedInstanceState)

        splash.setKeepOnScreenCondition { !ready }

        // 接管退出动画:淡出 300ms 后移除
        splash.setOnExitAnimationListener { provider ->
            provider.view
                .animate()
                .alpha(0f)
                .setDuration(300)
                .withEndAction { provider.remove() }
                .start()
        }

        // 异步加载完成后 ready = true,下一帧触发上面的退出动画
    }
}

流程就是:

ready = true
 → 下一帧:keepOnScreenCondition 返回 false
 → 系统触发 OnExitAnimationListener
 → 你的淡出动画开始(300ms)
 → withEndAction 里 provider.remove() 真正移除
 → 内容完全可见

withEndAction { provider.remove() } 这一句绝不能省,否则启动画面会像一块透明的”盖子”一样压在内容上面,拦截触摸。

进阶:图标 + 背景分离动画

iconView 让你可以把 logo 单独拎出来做动画。一个常见的品牌过渡是:图标轻微放大,整个画面淡出,产生”logo 向内容推进”的错觉。

splash.setOnExitAnimationListener { provider ->
    val icon = provider.iconView
    val whole = provider.view

    // 图标:从 1.0 放大到 1.15,同时淡出
    icon.animate()
        .scaleX(1.15f)
        .scaleY(1.15f)
        .alpha(0f)
        .setDuration(350)
        .setInterpolator(AccelerateDecelerateInterpolator())
        .start()

    // 整个画面:稍作延迟后淡出
    whole.animate()
        .alpha(0f)
        .setStartDelay(120)
        .setDuration(250)
        .withEndAction { provider.remove() }
        .start()
}

要点:

  • 两段动画并行,各自独立控制时长/插值器,节奏感比单一淡出好很多。
  • provider.remove() 只需要在最后结束的那一段里调用一次(这里是 whole 的 withEndAction)。

常见坑与注意事项

1. 忘记 remove() → 界面”卡死”

表现:内容出来了,但点不动,或者一直压着一个半透明层。原因就是没调用 provider.remove()。这是最高频的坑,务必把 remove() 挂在动画结束回调里。

2. 在退出动画里做重活

OnExitAnimationListener 跑在主线程。如果在这里做耗时操作(比如同步 IO),会直接卡掉退出动画甚至 ANR。动画里只做 View 动画,重活一律放到外面异步。

3. 和 keepOnScreenCondition 的配合顺序

两者是解耦的:

  • keepOnScreenCondition 决定”何时开始退出”(每帧轮询,返回 false 才放行)。
  • setOnExitAnimationListener 决定”怎么退出”(放行后播放什么动画)。

所以你可以放心组合:用条件函数控制退出时机(比如等 WebView 首帧、加超时兜底),用退出动画控制过渡质感,互不干扰。

4. 低版本兼容

setOnExitAnimationListenersetKeepOnScreenCondition 在低版本(Android 12 以下)由 core-splashscreen 用 SplashScreenView 兜底实现,API 一致,行为基本对齐。真正有差异的只是系统动画的细节,你的代码不用写版本分支。

一个完整的可复制模板

class MainActivity : ComponentActivity() {

    private var ready = false
    private val handler = Handler(Looper.getMainLooper())

    override fun onCreate(savedInstanceState: Bundle?) {
        val splash = installSplashScreen()
        super.onCreate(savedInstanceState)

        // 1) 退出时机:等 ready,最多 3 秒兜底
        splash.setKeepOnScreenCondition { !ready }
        handler.postDelayed({ ready = true }, 3000)

        // 2) 退出动画:图标放大淡出 + 整体淡出
        splash.setOnExitAnimationListener { provider ->
            provider.iconView.animate()
                .scaleX(1.12f).scaleY(1.12f).alpha(0f)
                .setDuration(320)
                .setInterpolator(AccelerateDecelerateInterpolator())
                .start()

            provider.view.animate()
                .alpha(0f)
                .setStartDelay(100)
                .setDuration(240)
                .withEndAction { provider.remove() }
                .start()
        }

        // 3) 你的加载逻辑,完成后 ready = true
        loadContent { ready = true }
    }

    override fun onDestroy() {
        handler.removeCallbacksAndMessages(null)
        super.onDestroy()
    }
}

小结

  • setOnExitAnimationListener 让你接管退出动画,拿到 view / iconView 自由编排。
  • 动画结束必须调用 provider.remove(),否则启动画面残留、拦截触摸。
  • 它和 setKeepOnScreenCondition 职责分离:前者管”何时退出”,后者管”如何退出”。

把这两篇连起来,你就能完整掌控 Android 启动画面的时机和过渡——从”为什么改个变量它就知道该退了”,到”退出的那一瞬间我想让它怎么消失”。