Jetpack Splashscreen 解析 | 助力新生代 IT 农民工 事半功倍

Jetpack 家族迎来了一位新的成员 Core Splashscreen,所以我也要重新开始写 Jetpack 系列文章了,在这之前写过一系列 Jetpack 文章以及配套的实战应用,包含 App StartupPaging3HiltDataStoreViewBinding 等等实战项目,点击下方链接前去查看。

而今天这篇文章主要介绍 Google 新库 Core Splashscreen ,众所周知在 Android 12 中增加了一个改善用户体验的功能 SplashScreen API,它可为所有应用添加启动画面。包括启动时进入应用的启动动画,以及退出动画。

通过这篇文章你将学习到以下内容

  • Core Splashscreen 解决了什么问题?
  • Core Splashscreen 工作原理?
  • 针对不同的场景,如何在项目中使用 Core Splashscreen?
  • Core Splashscreen 源码分析?

Core Splashscreen 实战项目地址,可以前往 GitHub 查看示例项目 Splashscreen。
https://github.com/hi-dhl/AndroidX-Jetpack-Practice

Core Splashscreen

Core Splashscreen 解决了什么问题?

在 Android 启动过程中会出现白屏 / 黑屏,为了改善这一体验,因此添加启动画面,从而改善视觉上的体验,为了实现这一功能,市面上也有很多实现方法,都有各自的优缺点,因此并不能保证在所有设备上都能够流畅的运行。

其次有的时候需要从本地磁盘或者网络异步加载数据,等待数据加载完之后,才会去渲染 View, 大多数时候,希望将数据加载提前,尽量保证用户进入到首页之后,看到数据,减少用户的等待时间。

在 Android 12 上新增的 SplashScreen API,可以解决这一系列问题,但是缺点是仅限于 Android 12。

Core Splashscreen 因此而诞生了,为 Android 12 新增的 SplashScreen API 提供了向后兼容,可以在 Android 5.0 (API 21) ~ Android 12 (API 31)所有的 API 上使用。来看一下 Google 提供的动画效果。

Core Splashscreen 工作原理

Core Splashscreen 为 Android 12 新增的 SplashScreen API 提供了向后兼容,但是仅仅在以下情况下才会显示启动画面:

  • 冷启动:用户打开 APP 时 APP 进程尚未运行
  • 温启动:APP 进程正在运行,但是 Activity 尚未创建

启动动画只有在以上情况才会显示,但是在热启动期间是不会显示启动画面。

  • 热启动:APP 进程正在运行,Activity 也已经创建,也就说用户按下 Home 键退到后台,直到 Activity 被销毁之前,是不会显示启动画面

如何使用 Core Splashscreen

因为 Core Splashscreen 兼容了 Android 12 新增的 SplashScreen API, 因此需要将 compileSdkVersion 更新到 31 及其以上。

如果你的 SDK 还没有更新到 Android 12, 请先更新。SDK Manager -> 选择 Android 12

android {
    compileSdkVersion 31
}

在模块级别的 build.gradle 文件中添加以下依赖。

implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'

当添加完依赖之后就可以开始使用 Core Splashscreen,只需要三步即可实现显示启动画面。

1. 在 res/values/themes.xml 文件下添加新的主题 Theme.AppSplashScreen

<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
    <item name="windowSplashScreenBackground">@color/purple_200</item>
    <item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
    <item name="postSplashScreenTheme">@style/Theme.AppTheme</item>
</style>

<!-- Base application theme. -->
<style name="Theme.AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <!-- 添加 APP 默认主题 -->
</style>
  • android:windowSplashScreenBackground : 设置背景颜色
  • windowSplashScreenAnimatedIcon : 设置显示在屏幕中间的图标, 如果是通过 AnimationDrawableAnimatedVectorDrawable 创建的对象,可呈现动画效果,则会在页面显示的时候,播放动画
  • postSplashScreenTheme : 设置显示动画不可见时,使用 APP 的默认主题

2. 在 application 节点中,设置上一步添加主题 Theme.AppSplashScreen

<application
    android:theme="@style/Theme.AppSplashScreen">
</application>

3. 在调用 setContentView() 方法之前调用 installSplashScreen()

class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by viewbind()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        installSplashScreen()
        with(binding) {
            // init view
        }
    }
}

调用 installSplashScreen() 方法主要将 Activity 与我们添加的主题相关联。这一步完成之后,就可以在 APP 启动过程中,看到刚才设置的图标或者动画了。

扩展功能

让启动动画持久一点

默认情况下当应用绘制第一帧后,启动画面会立即关闭,但是有的时候需要从本地磁盘或者网络异步加载数据,这个时候,希望启动画面能够等到数据加载完回来才结束。可以通过以下方法实现。

splashScreen.setKeepVisibleCondition { !appReady }

// 模拟从本地磁盘或者网络异步加载数据的耗时操作
Handler(Looper.getMainLooper())
    .postDelayed({ appReady = true }, 3000)

调用以上方法,可以让应用暂停绘制第一帧这样启动画面就不会结束,当数据加载完之后,通过更新变量 appReady 来控制是否结束启动画面。

实现退出动画

当然我们也可以添加启动画面的退出动画,即从启动画面优雅的回到应用主界面。

splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
    ......
    // 自定义退出动画
    val translationY = ObjectAnimator.ofFloat(......)
    translationY.doOnEnd { splashScreenViewProvider.remove() }
    translationY.start()
}

效果可以前往 GitHub 查看示例项目 Splashscreen。

GitHub 示例项目:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

Core Splashscreen 源码解析

Core Splashscreen 源码很简单,总共就只有两个类。

  • SplashScreen :主要为实现 SplashScreen API 提供了向后兼容性,用于将 Activity 与主题相关联。
  • SplashScreenViewProvider : 用于控制退出动画(启动画面 -> 应用主界面),当退出动画结束时需要手动调用 SplashScreenViewProvider#remove() 方法

初始化 SplashScreen

通过调用 SplashScreen#installSplashScreen() 方法来进行初始化,将 Activity 与添加的主题相关联。
androidx/core/splashscreen/SplashScreen.kt

public companion object {
    @JvmStatic
    public fun Activity.installSplashScreen(): SplashScreen {
        val splashScreen = SplashScreen(this)
        splashScreen.install()
        return splashScreen
    }
}

private fun install() {
    impl.install()
}

最终都是通过调用 impl.install() 方法来进行初始化,一起来看看成员变量 impl 是如何初始化的。

private val impl = when {
    SDK_INT >= 31 -> Impl31(activity)
    SDK_INT == 30 && PREVIEW_SDK_INT > 0 -> Impl31(activity)
    SDK_INT >= 23 -> Impl23(activity)
    else -> Impl(activity)
}

到这里我们知道了 Google 为了向后兼容,针对于不同版本的系统,分别对应有不同的实现类。最终都是调用 install() 方法来进行初始化的,在 install() 方法内通过解析我们添加的主题,最后通过 activity.setTheme() 方法,将添加的主题和 Activity 关联在一起。

如何让启动动画持久一点

在代码中,我们通过调用 SplashScreen#setKeepVisibleCondition() 方法,让启动动画持久一点,等待数据加完之后,才结束启动动画。一起来看看这个方法。
androidx/core/splashscreen/SplashScreen.kt

public fun setKeepVisibleCondition(condition: KeepOnScreenCondition) {
    // impl:针对于不同版本的系统,分别对应有不同的实现类
    impl.setKeepVisibleCondition(condition)
}

open fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
    ......
    observer.addOnPreDrawListener(object : OnPreDrawListener {
        override fun onPreDraw(): Boolean {
            if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
                return false
            }
            contentView.viewTreeObserver.removeOnPreDrawListener(this)
            // 当开始绘制时,会调用 dispatchOnExitAnimation 方法,结束启动动画
            mSplashScreenViewProvider?.let(::dispatchOnExitAnimation)
            return true
        }
    })
}

最后通过 ViewTreeObserver 来监听视图的变化,当视图将要开始绘制时,会回调 OnPreDrawListener#onPreDraw() 方法。最后调用 dispatchOnExitAnimation 方法,结束启动动画。

实现退出动画

最后一起来看一下,源码中是如何实现退出动画,即从启动画面优雅的回到应用主界面,源码中只是提供了一个 OnExitAnimationListener 接口,将退出动画交给了开发者去实现,一起来看一下SplashScreen#setOnExitAnimationListener() 方法。
androidx/core/splashscreen/SplashScreen.kt

Android 12 以上

override fun setOnExitAnimationListener(
    exitAnimationListener: OnExitAnimationListener
) {
    activity.splashScreen.setOnExitAnimationListener {
        val splashScreenViewProvider = SplashScreenViewProvider(it, activity)
        exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
    }
}

在 Android 12 中是通过系统源码提供的接口 activity.splashScreen.setOnExitAnimationListener ,回调对外暴露的接口 OnExitAnimationListener 让开发者去实现退出动画的效果。

Android 12 以下

open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
    animationListener = exitAnimationListener
    val splashScreenViewProvider = SplashScreenViewProvider(activity)
    ......
    splashScreenViewProvider.view.addOnLayoutChangeListener(
    object : OnLayoutChangeListener {
        override fun onLayoutChange(......) {
            ......
            dispatchOnExitAnimation(splashScreenViewProvider)
        }
    })
}

fun dispatchOnExitAnimation(splashScreenViewProvider: SplashScreenViewProvider) {
    ......
    splashScreenViewProvider.view.postOnAnimation {
        finalListener.onSplashScreenExit(splashScreenViewProvider)
    }
}

通过向屏幕中显示的 View 添加 addOnLayoutChangeListener 方法,来监听布局的变化,当布局会发生改变时,会回调 onLayoutChange 方法,最后通过回调对外暴露的接口 OnExitAnimationListener 让开发者去实现退出动画。

不过这里需要注意的是,最后都需要调用 SplashScreenViewProvider#remove() 方法在合适的时机移除动画,可以在退出动画结束时,调用这个方法。

总结

本文从不同的角度分别分析了 Core Splashscreen。如何在项目中使用 Core Splashscreen,可以前往 GitHub 查看示例项目 Splashscreen。

仓库地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

另外 KtKit 是用 Kotlin 语言编写的小巧而实用工具库,包含了项目中常用的一系列工具,我添加了许多新的功能,包含了很多 Kotlin 技巧。文章分析可前往查看 为数不多的人知道的 Kotlin 技巧以及解析(三)

监听 EditText

将 Flow 通过 lifecycleScope 将 EditText 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会断开它们之间的引用,有效的避免内存泄漏。

......
// 监听 TextWatcher#onTextChanged 的回调函数
editText.textChange(lifecycleScope) {
    Log.e(TAG, "textChange = $it")
}

// 监听 TextWatcher#beforeTextChanged 的回调函数
editText.textChangeWithbefore(lifecycleScope) {
    Log.e(TAG, "textChangeWithbefore = $it")
}

// 监听 TextWatcher#afterTextChanged 的回调函数
editText.textChangeWithAfter(lifecycleScope) {
    Log.e(TAG, "textChangeWithbefore = $it")
}
......

监听蜂窝网络变化

lifecycleScope.launch {
    listenCellular().collect {
        Log.e(TAG, "listenNetwork = $it")
    }
}

监听 wifi 网络的变化

lifecycleScope.launch {
    listenWifi().collect {
        Log.e(TAG, "listenNetwork = $it")
    }
}

监听蓝牙网络的变化

lifecycleScope.launch {
    listenNetworkFlow().collect {
        Log.e(TAG, "listenNetwork = $it")
    }
}

更多 API 使用方式点击这里前往查看:

如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR ❤️❤️❤️

如果有帮助 点个赞 就是对我最大的鼓励

代码不止,文章不停

持续分享最新的技术


最后推荐我一直在更新维护的项目和网站:

  • 个人博客,将所有文章进行分类,欢迎前去查看 https://hi-dhl.com

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

  • 剑指 offer 及国内外大厂面试题解:在线阅读

  • LeetCode 系列题解:在线阅读

  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis

  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation

  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

历史文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容