阅读 887

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

公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。

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。 github.com/hi-dhl/Andr…

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 ❤️❤️❤️


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

代码不止,文章不停

欢迎关注公众号:ByteCode,持续分享最新的技术



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

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

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

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

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

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

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

历史文章

文章分类
Android