回顾:退出时发生了什么
上一篇: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. 低版本兼容
setOnExitAnimationListener 和 setKeepOnScreenCondition 在低版本(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 启动画面的时机和过渡——从”为什么改个变量它就知道该退了”,到”退出的那一瞬间我想让它怎么消失”。
