3.3 SplashScreen 库的使用
3.3.1 打造进场效果
进场效果的部分只涉及到资源方面的配置。
首先扩展自 SplashScreen 库的预设主题作成一个 Base 主题,指定诸如启动画面背景、目标 Activity 背景等方面的共同属性。
<style name="SplashScreenTheme.Base" parent="Theme.SplashScreen"> ... <item name="windowSplashScreenBackground">@color/splashBackground</item> <item name="postSplashScreenTheme">@style/TargetScreenTheme</item> </style>
其他的一些属性需要针对低版本和 12 作区分。比如 12上可以指定其独有的动画 Icon,Icon 背景和 Brand logo,而低版本上指定一个静态 Icon 即可。但为了效果接近,可以指定 App 的 Adaptive Icon。
<!-- version:12- --> <style name="SplashScreenTheme" parent="SplashScreenTheme.Base"> <item name="windowSplashScreenAnimatedIcon">@mipmap/ic_icon_adaptive</item> </style> <!-- version:12+ --> <style name="SplashScreenTheme" parent="SplashScreenTheme.Base"> <item name="windowSplashScreenAnimatedIcon">@drawable/ic_icon_animated</item> <item name="android:windowSplashScreenIconBackgroundColor">@color/iconBackground</item> <item name="android:windowSplashScreenBrandingImage">@drawable/ic_brand</item> </style>
我们来看一下分别运行在 Android 8 和 12 上的进场效果:
可以看到高低版本上是比较接近的进场画面,只不过 12 上多了特有的 Kotlin
的组合动画和一个 TechMerger
字样的 Brand Logo。
3.3.2 延长启动画面
随着 App 第一帧的开始描画,SplashScreenWindow 即将消失。如果背面的业务逻辑尚未准备完毕,那体验不是很好,鱼骨屏什么的就是用来优化这个问题。
当然对于启动画面来讲,现在可以通过 SplashScreen 库的 API 来灵活控制启动画面的时长,确保内容好了再退出。这个 API 就是 installSplashScreen()。
通过这个静态函数可以拿到定制的入口,之后可以调用 setKeepVisibleCondition() 设置启动画面保持展示的条件,条件可以 ViewModel 的耗时加载相结合。这里提供的是一个 ViewModel 实例初始化 2s 之后再退出启动画面的一个模拟逻辑。
注意:由于 installSplashScreen 函数内部将调用 setTheme 反映实际的主题,所以需要在 setContentView 之前调用
class JetpackSplashActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... splashScreen = installSplashScreen() setContentView(binding.root) splashScreen.setKeepVisibleCondition { !viewModel.isDataReady() } } } class MyViewModel(application: Application): AndroidViewModel(application) { companion object { const val WORK_DURATION = 2000L } private val initTime = SystemClock.uptimeMillis() fun isDataReady() = SystemClock.uptimeMillis() - initTime > WORK_DURATION }
我们来看一下分别运行在 Android 8 和 12 上启动画面的延长效果:
可以看到高低版本上都成功实现了启动画面的延迟退出。
3.3.3 打造整体退场效果
当启动画面退出的时候如果能提供一个无缝过渡到目标内容的动画,体验会更好。我们可以利用 SplashScreen 库的 setOnExitAnimationListener 来针对进场画面的整体视图实现一个退场的动画。
比如这里我们定制一个 SplashScreen 整体的下移淡出效果。
注意:记得在动画结束的时候调用 SplashScreenViewProvider 的 remove() 及时将启动画面的视图移除,否则可能覆盖在实际画面上,遮挡内容。
class JetpackSplashActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... splashScreen.setOnExitAnimationListener { splashScreenViewProvider -> showSplashExitAnimator(splashScreenViewProvider.view) { splashScreenViewProvider.remove() } } private fun showSplashExitAnimator(splashScreenView: View, onExit: () -> Unit = {}) { ... AnimatorSet().run { ... playTogether(slideDown, alphaOut) doOnEnd { onExit() } } } }
我们来看一下分别运行在 Android 8 和 12 上的退场效果:
可以看到 12 上的 Brand Logo 是一起执行的退场动画,总的来说高低版本上都实现了几乎一致的整体下移和淡出的效果。
3.3.4 打造 Icon 独有的退场效果
如果觉得整体的退场动画太过突兀或夸张,还可以针对 App Icon 作单独的退场效果。基本逻辑和定制整体的退场效果差不多。,区别在于执行动画的对象由 view
变成了 IconView
。
同样要注意在动画结束的时候调用 remove()
。
class JetpackSplashActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... splashScreen.setOnExitAnimationListener { splashScreenViewProvider -> showSplashIconExitAnimator(splashScreenViewProvider.iconView) { splashScreenViewProvider.remove() } } private fun showSplashIconExitAnimator(iconView: View, onExit: () -> Unit = {}) { ... AnimatorSet().run { ... playTogether(alphaOut, scaleOut, slideUp) doOnEnd { onExit() } } } }
这里延时的是针对 Icon 的上移和淡出动画,来看一下效果:
可以看到 12 上的 Brand Logo 是不动的,整体上都是一个 Icon 上移和淡出的效果。
3.3.5 控制退场动画的时长
设备性能或状态会影响 App 开始描画的时间,为了让用户早点看到实际内容,可以灵活控制退场动画的时长。比如当描画得晚,可以考虑不展示退场动画或执行极短的固定时长;当描画得早,进场动画可能尚未结束,将剩余的时长交接给退场部分。
主要通过 SplashScreen 库返回的进场动画开始时刻(iconAnimationStartMillis)和总时长(iconAnimationDurationMillis)的 API,与退场回调的当前时刻进行计算即可。
需要注意的是,针对 12 之前的版本,SplashScreen 库的进场部分不支持 Icon 动画,所以上述的两个属性总是返回 0,需要特别处理一下。
private fun getRemainingDuration(provider: SplashScreenViewProvider): Long { val animationDuration = provider.iconAnimationDurationMillis val animationStart = provider.iconAnimationStartMillis return if (animationDuration == 0L || animationStart == 0L) defaultExitDuration else (animationDuration - SystemClock.uptimeMillis() + animationStart) .coerceAtLeast(0L) }
3.4 Lottie 支持 SplashScreen 吗?
Lottie 是跨平台的动画库,非常好用。那么 SplashScreen 库支持吗?
并不支持,因为 SplashScreen 库只能配置 Drawable 文件,不可以替换 View。而 Lottie 的效果完全依赖于自定义的 AnimationView。
但 SplashScreen 的设计者提供了一个魔改思路:
拷贝 Lottie Json 的第一帧,做成 SVG 并转换为 Animated Vetor Drawable,设置到 Splash Icon
目标布局正中放入执行 Icon 动画的 View
在 Splash 退出的时候将 LottieAnimationView 解析 Json 并执行动画
动画结束后记得将真正的视图展示
代码示例:
splashScreen.setOnExitAnimationListener { vp -> val lottieView = findViewById<LottieAnimationView>(R.id.animationView) ... lottieView.postDelayed({ vp.view.alpha = 0f vp.iconView.alpha = 0f lottieView!!.playAnimation() }, delay) lottieView.addAnimatorListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { imageView.visibility = View.VISIBLE } }) }
我们来看一下高低版本上魔改之后支持 Lottie 的 SplashScreen 效果,可以看到是几乎完全一致的非常流畅、丝滑的启动体验。
但这种做法违背了 SplashScreen 库的设计初衷,不建议使用,这里只是提供一种思路。
更详细的说明可以参考官方的 DEMO 介绍:https://github.com/vcaen/splashscreen-sample
4. SplashScreen 库的实现原理
接下来了解一下 SplashScreen 库如何兼容低版本,实现几乎一致的启动效果。
4.1 总体原理
写这个资料的时候 Android 12 的源码尚未公开,最近公开了之后看了一眼,发现 SplashScreen 的实现非常繁杂。
这里简单提一下关键地方,SplashScreenWindow 退出的时候,系统会通过 AIDL 将封装了启动画面的信息的序列化对象传递给 App 进程。App 将对象反序列化并创建退场视图即 SplashScreenView,然后添加到 DecorView 上去。之后 App 即可对这个视图作退场效果的定制。
更多全面的细节,感兴趣的朋友可自行研究。本次主要Jetpack SplashScreen 库的源码进行解读。
总体的原理分为进场和退场两个部分。
进场部分的画面针对 12 之前的版本是 windowBackground 思路,针对 12 是系统专属的 SplashScreen 系属性实现的。
退场部分,在 12 之前是自定义的 FrameLayout 添加到 Activity 的 ContentView 上,12 则是反序列化的 SplashScreenView 添加到了 DecorView 上。
4.2 进场画面的原理
进场画面的原理完全依赖于主题的配置,面向低版本的话和之前的常规做法是一样的思路,即提供一个读取我们配置的画面资源的 LayerListDrawable 放置到 windowBackground 中。
<style name="Theme.SplashScreen" parent="Theme.SplashScreenBase"> ... </style> <style name="Theme.SplashScreenBase" parent="android:Theme.NoTitleBar"> <item name="android:windowBackground">@drawable/compat_splash_screen</item> ... </style>
<!--compat_splash_screen.xml--> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:gravity="fill"> <color android:color="?attr/windowSplashScreenBackground" /> </item> <item android:drawable="?attr/windowSplashScreenAnimatedIcon" ... /> </layer-list>
面向 12 的话,则是由系统回调专属的属性去构建启动画面的视图。
<style name="Theme.SplashScreen" parent="android:Theme.DeviceDefault.NoActionBar"> <item name="android:windowSplashScreenAnimatedIcon">?windowSplashScreenAnimatedIcon</item> <item name="android:windowSplashScreenBackground">?windowSplashScreenBackground</item> ... </style>
4.3 定制入口的初始化
获取 SplashScreen 实例的 API 是 installSplashScreen()。其运行在低版本上的话,需要额外读取和缓存 Icon 和 Background 的配置,然后读取并设置目标 Activity 的 Theme。12 上的话,Window 背景,Icon 和 Branding 等属性由系统控制,只需要配置目标 Activity 的 Theme 即可。
4.4 延长启动画面
通过 setKeepVisibleCondition() 可以延长启动画面的展示,无关运行的版本,原理都是向 ContentView 的 ViewTreeObserver 注册OnPreDrawListener回调来实现。
系统在描画前先回调 onPreDraw(),获取是否放行描画的条件,此处将回调 KeepVisibleCondition 的逻辑。如果不放行,在下次屏幕刷新的时候继续回调,直到满足条件开始描画,启动 Window 消失。描画放行的时候,低版本额外需要手动调用 dispatchOnExitAnimation 来执行退出回调,12 则由系统自行执行。
需要注意:这个时候退场用的自定义视图仍然还没添加上来,只是延迟了 SplashScreenWindow 的退出而已。
4.5 退场画面的回调
setOnExitAnimationListener() 可以监听退场时机。
运行在低版本上的时候,需要手动加载启动画面的布局到 ContentView中,并将之前设置的 Window Background 和 Icon 等属性显示。然后添加 Layout change 回调,在布局完毕的时候通过 adjustInsets 特殊处理将 Icon 位置调整一下。判断启动画面保持条件是否达到,达到的话调用 onSplashScreenExit。
运行在 12 上的时候,布局不需要手动准备,通过 12 专用的系统接口,将视图缓存到 Provider 里即可,后续的 Exit 也由系统执行。
4.6 adjustInsets 的特殊处理
面向低版本的退场画面在布局成功后会调用的特殊处理。
进场部分的是 Window Drawable,Icon 是居中的。但退场部分是向 ContentView中手动添加的 Framelayout 布局,Icon 在布局里是居中的,但由于 StatusBar 和 NavigationBar 高度不一样,Icon 在整个 Window 里是偏下的。
如果不加干预的话,进场过渡到退场的时候,Icon 会发生跳跃。
源码通过 windowInsets API 获取状态栏和导航栏的高度,取差值的一半交由 IconView 去移动到 window 中间。
private class Impl23(activity: Activity) : Impl(activity) { override fun adjustInsets( ... ) { // Offset the icon if the insets have changed val rootWindowInsets = view.rootWindowInsets val ty = rootWindowInsets.systemWindowInsetTop - rootWindowInsets.systemWindowInsetBottom splashScreenViewProvider.iconView.translationY = -ty.toFloat() / 2f } }
结语
到这里我们探讨了启动画面的必要性、回顾了启动画面打造的常规做法、介绍了 Android 12 上 SplashScreen API、以及详细了解了 Jetpack SplashScreen 库的目的、使用细节和实现原理!
可以看到 SplashScreen 库简单又清晰,可以帮助我们灵活、高效地重塑启动画面,主要体现在这么几个方面:
配置图标动画、图标背景、品牌 Logo 等新元素,打造丰富的进场效果
适当地调节启动画面的展示时间,以配合后台的加载
针对整体或 Icon 视图,灵活打造无缝衔接的退场效果
灵活控制退场动画的有无和时长,自然地过渡到目标内容
值得提醒的是:启动画面只是过渡,动画效果避免突兀,更不要过多占用用户时间!
参考资料&资源分享
主要分享一下使用到的参考资料以及分享不错的 DEMO,大家可以通过这些资料和 DEMO 切实感受和实践下 SplashScreen 库的玩法!
最后一个着重说一下,这是我之前采用 Jetpack Compose
写的 Flappy Bird
小游戏。我抽空给它适配了 SplashScreen 库的功能。可以看到游戏启动的时候小鸟渐渐飞进来,之后小鸟向上淡出到游戏界面的效果。
FAQ
推荐阅读
之前针对 Android 12 的 SplashScreen API 和 Jetpack SplashScreen 库分别写过更为详细的解读文章,感兴趣的朋友可补充食用~
Android 12上全新的应用启动画面,还不适配一下?
Jetpack新成员SplashScreen:打造全新的App启动画面
同时在 GDG 社区说 活动上也分享过本文的话题,可以到 B站 上观看视频回放:
「社区说」《使用 Jetpack SplashScreen 打造全新的应用启动效果》