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

Jetpack 家族迎來了一位新的成員 Core Splashscreen,所以我也要重新開始寫 Jetpack 系列文章了,在這之前寫過一系列 Jetpack 文章以及配套的實戰應用,包含 App Startup 、 Paging3 、 HiltDataStoreViewBinding 等等實戰項目,點擊下方鏈接前去查看。

而今天這篇文章主要介紹 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 開發等等網址,歡迎前去查看 為互聯網人而設計導航網站

歷史文章

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容