深度探讨 Jetpack SplashScreen 如何重塑应用启动画面(2)

简介: 深度探讨 Jetpack SplashScreen 如何重塑应用启动画面(2)

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 上的进场效果:image.gif

可以看到高低版本上是比较接近的进场画面,只不过 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 上启动画面的延长效果:

2Zmh5D.gif可以看到高低版本上都成功实现了启动画面的延迟退出。

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 上的退场效果:

image.gif

可以看到 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 的上移和淡出动画,来看一下效果:

image.gif

可以看到 12 上的 Brand Logo 是不动的,整体上都是一个 Icon 上移和淡出的效果。

3.3.5 控制退场动画的时长

设备性能或状态会影响 App 开始描画的时间,为了让用户早点看到实际内容,可以灵活控制退场动画的时长。比如当描画得晚,可以考虑不展示退场动画或执行极短的固定时长;当描画得早,进场动画可能尚未结束,将剩余的时长交接给退场部分。

1832b220aa754cd18c504acc7686a560.png

主要通过 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 效果,可以看到是几乎完全一致的非常流畅、丝滑的启动体验。

image.gif

但这种做法违背了 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 库的源码进行解读。

1832b220aa754cd18c504acc7686a560.png

总体的原理分为进场和退场两个部分。


进场部分的画面针对 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 即可。

1832b220aa754cd18c504acc7686a560.png

4.4 延长启动画面

通过 setKeepVisibleCondition() 可以延长启动画面的展示,无关运行的版本,原理都是向 ContentView 的 ViewTreeObserver 注册OnPreDrawListener回调来实现。


系统在描画前先回调 onPreDraw(),获取是否放行描画的条件,此处将回调 KeepVisibleCondition 的逻辑。如果不放行,在下次屏幕刷新的时候继续回调,直到满足条件开始描画,启动 Window 消失。描画放行的时候,低版本额外需要手动调用 dispatchOnExitAnimation 来执行退出回调,12 则由系统自行执行。


需要注意:这个时候退场用的自定义视图仍然还没添加上来,只是延迟了 SplashScreenWindow 的退出而已。

1832b220aa754cd18c504acc7686a560.png

4.5 退场画面的回调

setOnExitAnimationListener() 可以监听退场时机。


运行在低版本上的时候,需要手动加载启动画面的布局到 ContentView中,并将之前设置的 Window Background 和 Icon 等属性显示。然后添加 Layout change 回调,在布局完毕的时候通过 adjustInsets 特殊处理将 Icon 位置调整一下。判断启动画面保持条件是否达到,达到的话调用 onSplashScreenExit。


运行在 12 上的时候,布局不需要手动准备,通过 12 专用的系统接口,将视图缓存到 Provider 里即可,后续的 Exit 也由系统执行。

1832b220aa754cd18c504acc7686a560.png

4.6 adjustInsets 的特殊处理

面向低版本的退场画面在布局成功后会调用的特殊处理。


进场部分的是 Window Drawable,Icon 是居中的。但退场部分是向 ContentView中手动添加的 Framelayout 布局,Icon 在布局里是居中的,但由于 StatusBar 和 NavigationBar 高度不一样,Icon 在整个 Window 里是偏下的。


如果不加干预的话,进场过渡到退场的时候,Icon 会发生跳跃。

1832b220aa754cd18c504acc7686a560.png

源码通过 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 库的玩法!

1672186222698.png最后一个着重说一下,这是我之前采用 Jetpack Compose 写的 Flappy Bird 小游戏。我抽空给它适配了 SplashScreen 库的功能。可以看到游戏启动的时候小鸟渐渐飞进来,之后小鸟向上淡出到游戏界面的效果。

image.gif

FAQ

1672186284590.png

1672186304760.png

推荐阅读

之前针对 Android 12 的 SplashScreen API 和 Jetpack SplashScreen 库分别写过更为详细的解读文章,感兴趣的朋友可补充食用~


Android 12上全新的应用启动画面,还不适配一下?

Jetpack新成员SplashScreen:打造全新的App启动画面

同时在 GDG 社区说 活动上也分享过本文的话题,可以到 B站 上观看视频回放:


「社区说」《使用 Jetpack SplashScreen 打造全新的应用启动效果》


相关文章
|
7月前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
6月前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
7月前
|
Java 数据库 Android开发
构建高效Android应用:Kotlin与Jetpack的完美结合
【5月更文挑战第28天】 在现代移动开发领域,Android平台以其广泛的用户基础和开放性受到开发者青睐。随着技术的不断进步,Kotlin语言以其简洁性和功能性成为Android开发的首选。而Android Jetpack组件则为开发者提供了一套高质量的设计架构、工具和UI组件,以简化应用程序的开发过程。本文将探讨如何利用Kotlin语言和Android Jetpack组件共同构建一个高效的Android应用程序,涵盖从语言特性到架构模式的全面分析,并提供具体的实践指导。
|
7月前
|
XML Java Android开发
安卓开发新趋势:Jetpack Compose的兴起与应用
【5月更文挑战第25天】随着移动开发技术的不断演进,安卓平台的创新也在持续推进。近年来,一个名为Jetpack Compose的新工具集引起了开发者社区的广泛关注。本文将深入探讨Jetpack Compose的核心概念、优势以及它对现有安卓开发模式的影响,并分析其在实际项目中的应用潜力。
|
7月前
|
安全 数据库 Android开发
构建高效Android应用:采用Kotlin与Jetpack的实践指南
【5月更文挑战第22天】 在移动开发领域,Android系统因其开放性和广泛的用户基础而备受开发者青睐。随着技术的不断演进,Kotlin语言以其简洁性和功能性成为Android开发的首选语言。本文将深入探讨如何结合Kotlin和Android Jetpack组件来构建一个高效且易于维护的Android应用。我们将重点讨论如何使用Jetpack的核心组件,如LiveData、ViewModel和Room,以及Kotlin的语言特性来优化代码结构,提高应用性能,并简化数据管理。通过具体案例分析,本文旨在为开发者提供一套实用的技术指导,帮助他们在竞争激烈的市场中脱颖而出。
|
7月前
|
物联网 区块链 Android开发
构建高效Android应用:Kotlin与Jetpack的实践之路未来技术的融合潮流:区块链、物联网与虚拟现实的交汇点
【5月更文挑战第30天】 在移动开发领域,效率和性能始终是开发者追求的核心。随着技术的不断进步,Kotlin语言以其简洁性和现代化特性成为Android开发的新宠。与此同时,Jetpack组件为应用开发提供了一套经过实践检验的库、工具和指南,旨在简化复杂任务并帮助提高应用质量。本文将深入探索如何通过Kotlin结合Jetpack组件来构建一个既高效又稳定的Android应用,并分享在此过程中的最佳实践和常见陷阱。
|
7月前
|
安全 Java Android开发
构建高效Android应用:Kotlin与Jetpack实践指南
【5月更文挑战第29天】 在移动开发的世界中,效率和性能始终是核心诉求。随着技术的演进,Kotlin语言以其简洁性和功能性成为Android开发的首选。结合Jetpack组件的推广,开发者得以构建更高效、可维护且易于测试的应用。本文将深入探讨利用Kotlin语言特性以及Jetpack架构组件来优化Android应用的策略和技巧,旨在帮助开发者提升应用质量并降低维护成本。
|
7月前
|
持续交付 Android开发 开发者
构建高性能微服务架构:后端开发的终极指南构建高效Android应用:Kotlin与Jetpack的完美结合
【5月更文挑战第28天】 在现代软件开发的浪潮中,微服务架构已经成为了设计灵活、可扩展且易于维护系统的重要模式。本文将深入探讨如何构建一个高性能的微服务架构,涵盖从基础概念理解到实践策略部署的全过程。我们将讨论关键的设计原则、技术选型、性能优化技巧以及安全性考虑,旨在为后端开发者提供一个全面的指南,帮助他们构建出能够适应快速变化的市场需求和技术挑战的系统。 【5月更文挑战第28天】 在移动开发的世界中,效率和性能是衡量一个应用成功与否的关键因素。本文将深入探讨如何通过结合Kotlin语言和Android Jetpack组件,来构建一个既高效又易维护的Android应用。我们将透过实际案例分析
|
7月前
|
安全 测试技术 Android开发
构建高效Android应用:Kotlin与Jetpack的实践指南
【5月更文挑战第27天】 在移动开发的世界中,效率和性能是衡量一个应用成功与否的关键因素。对于Android开发者来说,Kotlin语言和Jetpack组件套件的出现,不仅优化了开发流程,还提升了应用的性能和稳定性。本文将深入探讨如何通过Kotlin语言结合Jetpack组件,构建出既高效又稳定的Android应用。我们将从语言特性、架构组件到实际开发案例,一步步解析Kotlin和Jetpack的协同作用,帮助开发者打造出更优质的应用体验。
|
7月前
|
安全 Java Android开发
构建高效Android应用:Kotlin与Jetpack的实践指南
【5月更文挑战第20天】 在移动开发的世界中,效率和性能始终是开发者追求的核心目标。随着技术的不断进步,Kotlin语言以其简洁、安全和实用的特性成为了Android开发的首选语言。与此同时,Android Jetpack组件的推出,为开发者提供了一套高质量的库、工具和指南,以简化应用程序的开发过程。本文将探讨如何结合Kotlin语言和Jetpack组件来构建一个高效的Android应用,涵盖从项目初始化到性能优化的全过程。