前言
1、什么是Android的冷启动时间?
冷启动时间是指点击桌面LOGO那一刻起到启动页面的Activity调用onCreate()之前的时间段。
2、冷启动的时间段内发生了什么?
在一个Activity打开时,如果Application还没有启动,系统会创建一个进程,进程的创建和初始化会消耗一些时间,在这个时间中,WindowManager会先加载APP的主题样式里的窗口背景(windowBackground)作为预览元素,然后才会真正的加载布局,如果这个时间过长,会给用户造成一种错觉:APP卡顿不流畅
常见做法
- 将背景设置为APP Logo,市面上大部分APP都是这么做的
1 2 3 |
<style name="AppTheme" parent="BaseTheme"> <item name="android:windowBackground">@drawable/window_splash_screen_content</item> </style> |
- 将背景设置为透明色,当点击桌面LOGO时并不会立即启动APP,而是在桌面停留一会(其实已经启动)
1 2 3 |
<style name="AppTheme" parent="BaseTheme"> <item name="android:windowBackground">@android:color/transparent</item> </style> |
上面做法可以达到秒开效果,但属于掩耳盗铃。
Android 8.0
Google以前不推荐使用闪屏的使用,但是后来很多APP都在使用闪屏,Google希望让启动屏的制作更简单。
Android Oreo中提供了Splash Screen API,允许开发者把一个drawable资源设置为闪屏。
新建values-v26目录:
1 2 3 4 5 |
<resources> <style name="AppTheme" parent="BaseTheme"> <item name="android:windowSplashscreenContent">@drawable/window_splash_screen_content</item> </style> </resources> |
通过windowSplashscreenContent设置的drawable资源将会覆盖在windowBackground顶部,在系统状态栏之下,如果不想受到System Bars限制,请使用全屏主题。
如未设置windowSplashscreenContent,系统仍会使用其他属性来展示闪屏页,如:windowBackground。
缺点
- 无法定义动画
- 无法控制时长
windowBackground设置时机
以<Android API 30 Platform>为例
PhoneWindowManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) { ... try { ... final PhoneWindow win = new PhoneWindow(context); win.setIsStartingWindow(true); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); ... final WindowManager.LayoutParams params = win.getAttributes(); ... addSplashscreenContent(win, context); wm = (WindowManager) context.getSystemService(WINDOW_SERVICE); view = win.getDecorView(); wm.addView(view, params); // Only return the view if it was successfully added to the // window manager... which we can tell by it having a parent. return view.getParent() != null ? new SplashScreenSurface(view, appToken) : null; } ... return null; } private void addSplashscreenContent(PhoneWindow win, Context ctx) { final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window); final int resId = a.getResourceId(R.styleable.Window_windowSplashscreenContent, 0); a.recycle(); if (resId == 0) { return; } final Drawable drawable = ctx.getDrawable(resId); if (drawable == null) { return; } // We wrap this into a view so the system insets get applied to the drawable. final View v = new View(ctx); v.setBackground(drawable); win.setContentView(v); } |
Android 12
Android12 增加了全新的SplashScreen API,可以为所有应用启用新的启动画面,包括启动时的应用运动、显示应用图标的启动画面,以及向应用本身的过渡。
工作原理
当用户启动应用,且应用进程未在运行(冷启动)或Activity尚未创建(温启动)时:
- 系统使用主题和自定义的动画来显示启动画面
- 当应用准备就绪时,会关闭启动画面并显示应用
热启动不会显示启动画面。
动画元素和机制
由窗口背景、动画形式的应用图标和图标背景组成:
- 应用图标(1)应该是矢量可绘制对象,也可以是静态或动画,动画图标会在启动画面显示时自动播放,时长不受限制,但建议不要超过1000毫秒,默认使用Launcher图标
- 图标背景(2)是可选的,在图标与窗口背景之间需要更高的对比度时很有用
- 窗口背景(4)由不透明的单色组成
启动画面动画由进入动画和退出动画组成:
- 进入动画由系统控制,不可自自定义
- 退出动画可自定义,可访问SplashScreenView以及ICON,可以设置任意动画(位移、透明度、颜色等),需要在动画执行完毕时手动移除启动画面
自定义启动画面
新建values-v31目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<resources> <style name="AppTheme" parent="BaseTheme"> <!-- 单色填充背景 --> <item name="android:windowSplashScreenBackground">#F18C1A</item> <!-- 替换ICON --> <item name="android:windowSplashScreenAnimatedIcon">@mipmap/ic_loading_logo</item> <!-- 设置启动画面在关闭之前显示的时长。最长时间为 1000 毫秒 --> <item name="android:windowSplashScreenAnimationDuration">1000</item> <!-- 设置启动画面图标后面的背景 --> <item name="android:windowSplashScreenIconBackgroundColor">#ffffff</item> <!-- 设置要显示在启动画面底部的图片。设计准则建议不要使用品牌图片。 --> <item name="android:windowSplashScreenBrandingImage"> @drawable/window_splash_screen_content </item> </style> </resources> |
让启动画面在屏幕上显示更长时间
当应用绘制第一帧后,启动画面会立即关闭。如果需要异步加载少量数据,可以使用ViewTreeObserver.OnPreDrawListener让应用暂停绘制第一帧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
companion object { const val DURATION = 2000 } private val initTime = SystemClock.uptimeMillis() val content: View = findViewById(android.R.id.content) content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { return if ((SystemClock.uptimeMillis() - initTime) > DURATION) { content.viewTreeObserver.removeOnPreDrawListener(this) true } else false } }) |
自定义退出动画
可以通过getSplashScreen() 来自定义退出动画
1 2 3 |
splashScreen.setOnExitAnimationListener { splashScreenView -> ... } |
向中心缩小退出
1 2 3 4 5 6 7 8 9 10 |
@RequiresApi(Build.VERSION_CODES.S) private fun scaleExit(view: SplashScreenView) { ObjectAnimator.ofFloat(view, View.SCALE_X, View.SCALE_Y, Path().apply { moveTo(1f, 1f) lineTo(0f, 0f) }).apply { doOnEnd { view.remove() } start() } } |
ICON上滑退出
1 2 3 4 5 6 7 8 9 10 11 12 |
@RequiresApi(Build.VERSION_CODES.S) private fun slideUp(view: SplashScreenView) { val iconView = view.iconView ?: return AnimatorSet().apply { playSequentially( ObjectAnimator.ofFloat(iconView, View.TRANSLATION_Y, 0f, 50f), ObjectAnimator.ofFloat(iconView, View.TRANSLATION_Y, 50f, -view.height.toFloat()), ) doOnEnd { view.remove() } start() } } |
Jetpack SplashScreen
Jetpack发布了androidx.core:core-splashscreen:1.0.0 以向后兼容新的 Splash Screen API,此Alpha版本包含向后移植到API 23的所有新API,图标背景除外,全球的Android设备中6及以上的版本占用率达到了9成以上。
如需了解更多信息,请参阅 androidx.core.splashscreen。
关键API | 说明 |
---|---|
SplashScreen | Jetpack版获取定制启动画面入口的类 |
installSplashScreen() | 获取定制入口的静态成员函数 |
setKeepVisibleCondition | 指定保持启动画面展示的条件 |
KeepOnScreenCondition | 实现展示条件的接口 |
setOnExitAnimationListener | 监听启动画面的退出时机 |
OnExitAnimationListener | 启动画面退出的回调接口 |
SplashScreenViewProvider | 定制退场效果的启动画面视图 |
Attr对比 | Jetpack | Android 12 |
---|---|---|
指定目标Activity主题 | postSplashScreenTheme | - |
指定动画ICON | windowSplashScreenAnimatedIcon | android:windowSplashScreenAnimatedIcon |
指定启动画面背景 | windowSplashScreenBackground | android:windowSplashScreenBackground |
指定Icon动画时长 | windowSplashScreenAnimationDuration | android:windowSplashScreenAnimationDuration |
指定Icon背景 | - | android:windowSplashScreenIconBackgroundColor |
指定品牌Logo | - | android:windowSplashScreenBrandingImage |
API对比 | Jetpack | Android 12 |
---|---|---|
启动画面定制入口 | class androidx.core.splashscreen.SplashScreen | interface android.window.SplashScreen |
入口实例获取 | Activity#installSplashScreen() | Activity#getSplashScreen() |
启动画面视图 | SplashScreenViewProvider | SplashScreenView |
退场部分,无论是否运行在12,Jetpack SplashScreen都能达到Android 12同等效果,但进场部分在低版本存在一些局限:
- 暂时不支持ICON动画:AnimatedVectorDrawable
- 暂时不支持ICON背影:IconBackgroundColor
- 暂时不支持品牌LOGO:BrandingImage
面向低版本的进场效果本质是一个LayerDrawable,无法支持Vector Drawable动画、Adaptive Icon背景。
导入依赖
1 |
implementation 'androidx.core:core-splashscreen:1.0.0' |
基础主题配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<resources xmlns:tools="http://schemas.android.com/tools"> <!-- jetpack core splashscreen --> <style name="JetpackSplashBaseTheme" parent="Theme.SplashScreen"> <!-- 默认主题必须扩展自预设主题Theme.SplashScreen --> <!-- 同时覆写一下Icon、Duration和ScreenBackground三个属性 --> <!-- 面向12的主题的部分属性和默认主题是一致的,所以将共通的部分抽出到Base中复用 --> <item name="windowSplashScreenAnimatedIcon">@mipmap/ic_loading_logo</item> <item name="windowSplashScreenAnimationDuration">1000</item> <item name="windowSplashScreenBackground">#f18c1a</item> <!-- 需要配置postSplashScreenTheme属性,主题需要是AppCompat --> <!-- 否则会报异常:You need to use a Theme.AppCompat theme (or descendant) with this activity --> <item name="postSplashScreenTheme">@style/UiTheme</item> </style> <style name="JetpackSplashTheme" parent="JetpackSplashBaseTheme" /> <!-- Base activity ui theme. --> <style name="UiTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Primary brand color. --> <item name="colorPrimary">@color/purple_500</item> <item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorOnPrimary">@color/white</item> <!-- Secondary brand color. --> <item name="colorSecondary">@color/teal_200</item> <item name="colorSecondaryVariant">@color/teal_700</item> <item name="colorOnSecondary">@color/black</item> <!-- Status bar color. --> <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> </style> </resources> |
Android 12主题配置
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<resources> <style name="JetpackSplashTheme" parent="JetpackSplashBaseTheme"> <!-- 可选:替换ICON 但是必须设置,已知,在 小米 Mi 10 Android 13 Miui 14.0.2系统上,如果不设置此项,则应用会一直卡住在启动页面 --> <item name="android:windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_round</item> <!-- 设置启动画面图标后面的背景 --> <item name="android:windowSplashScreenIconBackgroundColor">#ffffff</item> <!-- 设置要显示在启动画面底部的图片。设计准则建议不要使用品牌图片。 --> <item name="android:windowSplashScreenBrandingImage"> @drawable/window_splash_screen_content </item> </style> </resources> |
Manifest配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<application ... android:theme="@style/JetpackSplashTheme"> <!-- Launcher Activity 主题会由Jetpack SplashScreen postSplashScreenTheme指定 --> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 非Launcher Activity需要手动指定theme --> <!-- 或者在setContentView之前动态设置:setTheme(R.style.UiTheme)--> <!-- 否则抛出异常: You need to use a Theme.AppCompat theme (or descendant) with this activity--> <activity android:name=".SecondActivity" android:exported="true" android:theme="@style/UiTheme" /> </application> |
初始化
通过installSplashScreen() 获取SplashScreen实例,installSplashScreen() 必须先用setContentView调用,原因是install函数会获取postSplashScreenTheme 配置的主题,并在检查通过后setTheme 给Activity。
1 2 3 4 5 6 7 |
private lateinit var compatSplashScreen: SplashScreen override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) compatSplashScreen = installSplashScreen() setContentView(R.layout.activity_main) } |
让启动画面在屏幕上显示更长时间
1 2 3 4 5 6 7 8 9 |
companion object { const val DURATION = 2000 } private val initTime = SystemClock.uptimeMillis() private fun compatDelay() { compatSplashScreen.setKeepVisibleCondition { (SystemClock.uptimeMillis() - initTime) < DURATION } } |
自定义退出动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
compatSplashScreen.setOnExitAnimationListener { provider -> val iconView = provider.iconView AnimatorSet().apply { playSequentially( ObjectAnimator.ofFloat(iconView, View.TRANSLATION_Y, 0f, 50f), ObjectAnimator.ofFloat( iconView, View.TRANSLATION_Y, 50f, -provider.view.height.toFloat() ), ) doOnEnd { provider.remove() } start() } } |
启动画面对比
Android 6 | Android 12 |
---|---|
遇到的问题与建议
-
Android 5.0 ~ Android 11.0系统,都统一使用
android:windowBackground
配置启动页背景,使用背景颜色的话,界面过于单调,效果不佳。 -
Android 12.0 如果UI设计师给你做了矢量图,你可以做动态的中心图标,不给你,使用静态图标也可以的。
-
不要假定 splashScreen.setOnExitAnimationListener 一定会被调用。
如果业务代码写在这个函数回调中去启动应用的主界面,会发现使用调试器启动应用,则应用会一直卡在启动画面。跟踪之后,会发现 OnExitAnimationListener 并没有被调用。
这个问题的原因是:只有从桌面启动的应用,才会主动去触发启动过场动画,而且这个动画是可选的,桌面也可以选择不触发启动过场动画。通过调试器打开的应用,是使用ADB命令启动的。这种情况下,是没有启动过场动画的,自然收不到过场动画的结束通知。
解决这个问题的办法,就是把初始化完成的判断代码移动到 compatSplashScreen.setKeepVisibleCondition 函数中,系统会每隔200MS回调一次,直到返回 false 为止。这样我们可以在这个函数中检查一下必要的初始化是否已经完成,如果已经完成了,返回 false 即可。 -
使用 splashScreen.setOnExitAnimationListener 设置了启动动画之后,一定要在合适的时机,主动调用 SplashScreen.OnExitAnimationListener(SplashScreenViewProvider splashScreenViewProvider) 中 splashScreenViewProvider.remove,否则,可能会一直卡住在启动页面。(注意,只是可能,目前测试发现,不调用,不一定会卡住)。
-
已知,在 小米 Mi 10 Android 13 Miui 14.0.2 系统上,如果启用了兼容库,但是不设置 android:windowSplashScreenAnimatedIcon 则应用会一直卡住在启动页面。
参考链接
- Android 12 SplashScreen API 以及Jetpack兼容库
- 启动画面
- Android 12 适配指南——SplashScreen
- 正确实践Jetpack SplashScreen API —— 在所有Android系统上使用总结,内含原理分析
- 将现有的启动画面实现迁移到 Android 12 及更高版本
- PGB-Example-Core-Splashscreen
- splashScreen.setOnExitAnimationListener() not called when using CustomSplashScreen in debug mode
- splashScreen.setOnExitAnimationListener() not called when using CustomSplashScreen with debugger
- 适配splashscreen步骤以及启动卡住不动(白屏)的坑
- Android 12 新APP启动画面(SplashScreen API)简介&&源码分析
- 深度探讨 Jetpack SplashScreen 如何重塑应用启动画面
有幸看到16QAM调制方式的总结,写得真好,想问下博主有继续研究NR物理层相关的内容吗?
木有深入研究了