1.前言
文章末尾有演示的APK链接,感兴趣的同学,可以自行下载体验一下
官方Android 12的Splash Screen文档地址
官方Splash Screen兼容库,支持所有版本系统
本篇文章主要围绕下面三个问题来介绍:
- 我们能从Android 12 SplashScreen API里面学到什么?
- 新出的SplashScreen兼容库又是什么?能做成什么样子?
- 小甲同学:我想看Android12 SplashScreen源码,可以吗?
前方高能预警:一定要记得『点赞❤️+关注❤️+收藏❤️』起来,划走了可就再也找不到了😅😅🙈🙈
进入正题,我们先介绍:SplashScreen如何使用,以及目前会遇到的问题,如何无缝过渡?会出现什么问题,怎么解决?
2.SplashScreen使用
首先我们需要把compileSdk和targetSdk(可选)
升级到31
2.1.Android12版本
(A).主题和外观配置
<!--文章末尾我们会把包含所有示例的链接地址提供出来,如有需要:请翻到文章末尾-->
<!-- values-v31/themes.xml -->
<!--单一颜色填充「启动画面」窗口背景-->
<item name="android:windowSplashScreenBackground">@color/...</item>
<!--「启动画面」中心的图标,
可以配置AnimationDrawable 和 AnimatedVectorDrawable类型的drawable-->
<item name="android:windowSplashScreenAnimatedIcon">@drawable/...</item>
<!--「启动画面」中心图标动画的持续时间,这个属性不会对屏幕显示的实际时间产生任何影响-->
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<!--「启动画面」中心图标后面设置背景-->
<item name="android:windowSplashScreenIconBackgroundColor">@color/...</item>
<!--「启动画面」底部显示的品牌图标-->
<item name="android:windowSplashScreenBrandingImage">@drawable/...</item>
(B).延长启动画面
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
// 模拟一些数据的初始化,再取消挂起
return if (viewModel.isReady) {
// 取消挂起,恢复页面内容绘制
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
// 挂起,内容还没有准备好
false
}
}
}
)
©.关闭启画面的动画
// 自己定制关闭的动画
splashScreen.setOnExitAnimationListener { splashScreenView ->
val slideUp = ObjectAnimator.ofFloat(
// 你们自己控制,自己随便写什么动画,这里我们测试让图标移动
splashScreenView.iconView,
View.TRANSLATION_Y,
0f,
-splashScreenView.height.toFloat()
)
slideUp.interpolator = AnticipateInterpolator()
slideUp.duration = 200L
slideUp.doOnEnd { splashScreenView.remove() }
slideUp.start()
}
(D).遇到的问题
- android:windowSplashScreenBrandingImage 定义的图片尺寸要求是多少?总觉得有点拉伸;
- 使用AnimationDrawable 或者 AnimatedVectorDrawable,来设置中心图标,
会出现“中心图标”消失的情况
,静态图标不会有这种问题出现; - Android12父主题设置
android:windowBackground
被覆盖,看不到效果
问题1: 在源码里面也没有看到具体的值或者比例大小,怎么办呢?
小技巧: 使用一个超大的正方形的图标设置进去测试了一下,拉伸不要紧,我们要的是比例,
然后测量了一下比例为:2.5 : 1,所以设计品牌名图标的时候,可以设置为400 * 160这样的比例为2.5:1的图标
问题2: 针对中心图标会闪现消失的问题做测试,
测试一:静态Icon、测试二:动态Icon
静态中心图标 - 正常
下面我们来测试动态中心图标,为了方便测试出效果,我们覆盖住图标后面的背景色,方便对比,最后发现:测试结果不太理想,效果不行
<!--AnimationDrawable写法-->
<!--没有真机测试,这个写法,效果看起来也挺奇怪的,可能是模拟器且是预览版的问题吧-->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item android:drawable="@mipmap/ic_launcher" android:duration="600" />
<item android:drawable="@drawable/api12_logo" android:duration="200" />
<item android:drawable="@mipmap/ic_launcher" android:duration="200" />
</animation-list>
动态中心图标,不正常
仔细看图标后面的「蓝色背景」
我们再来看一下官方文档中的顺滑效果:
官方效果顺滑
对比官方的效果,猜测可能是模拟器和预览版Android12的问题,主要是没有真机来测试Android12这个效果,不过这难不到我们,如果你的模拟器也有同样问题
,请跟着我们做如下操作:
下面我们使用AnimatedVectorDrawable来制作动态图标,
为了观察出效果:我们打开模拟器的开发者选项,找到Animator时长缩放设置为:动画时长x10
,来往下看效果:
10倍慢放 - 看着才正常
笑脸眼睛动画的矢量图文件 👇👇,点击查看在线制作矢量图动画
<!--仅测试玩耍,感兴趣的可以自己去制作一个-->
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:name="group">
<path
android:name="path_4"
android:pathData="M 11.99 2 C 6.47 2 2 6.48 2 12 C 2 17.52 6.47 22 11.99 22 C 17.52 22 22 17.52 22 12 C 22 6.48 17.52 2 11.99 2 Z M 12 20 C 7.58 20 4 16.42 4 12 C 4 7.58 7.58 4 12 4 C 16.42 4 20 7.58 20 12 C 20 16.42 16.42 20 12 20 Z M 12 17.5 C 14.33 17.5 16.32 16.05 17.12 14 L 15.45 14 C 14.76 15.19 13.48 16 12 16 C 10.52 16 9.25 15.19 8.55 14 L 6.88 14 C 7.68 16.05 9.67 17.5 12 17.5 Z"
android:fillColor="#FFFFFF"/>
</group>
<group android:name="group_1">
<path
android:name="path_1"
android:pathData="M 8.5 9.5 M 7 9.5 C 7 9.102 7.158 8.721 7.439 8.439 C 7.721 8.158 8.102 8 8.5 8 C 8.898 8 9.279 8.158 9.561 8.439 C 9.842 8.721 10 9.102 10 9.5 C 10 9.898 9.842 10.279 9.561 10.561 C 9.279 10.842 8.898 11 8.5 11 C 8.102 11 7.721 10.842 7.439 10.561 C 7.158 10.279 7 9.898 7 9.5"
android:fillColor="#FFFFFF"/>
<path
android:name="path_3"
android:pathData="M 8.5 9.5 M 7 9.5 C 7 9.102 7.158 8.721 7.439 8.439 C 7.721 8.158 8.102 8 8.5 8 C 8.898 8 9.279 8.158 9.561 8.439 C 9.842 8.721 10 9.102 10 9.5 C 10 9.898 9.842 10.279 9.561 10.561 C 9.279 10.842 8.898 11 8.5 11 C 8.102 11 7.721 10.842 7.439 10.561 C 7.158 10.279 7 9.898 7 9.5"
android:fillColor="#FFFFFF"/>
</group>
<group android:name="group_2">
<path
android:name="path"
android:pathData="M 15.5 9.5 M 14 9.5 C 14 9.102 14.158 8.721 14.439 8.439 C 14.721 8.158 15.102 8 15.5 8 C 15.898 8 16.279 8.158 16.561 8.439 C 16.842 8.721 17 9.102 17 9.5 C 17 9.898 16.842 10.279 16.561 10.561 C 16.279 10.842 15.898 11 15.5 11 C 15.102 11 14.721 10.842 14.439 10.561 C 14.158 10.279 14 9.898 14 9.5"
android:fillColor="#FFFFFF"/>
<path
android:name="path_2"
android:pathData="M 15.5 9.5 M 14 9.5 C 14 9.102 14.158 8.721 14.439 8.439 C 14.721 8.158 15.102 8 15.5 8 C 15.898 8 16.279 8.158 16.561 8.439 C 16.842 8.721 17 9.102 17 9.5 C 17 9.898 16.842 10.279 16.561 10.561 C 16.279 10.842 15.898 11 15.5 11 C 15.102 11 14.721 10.842 14.439 10.561 C 14.158 10.279 14 9.898 14 9.5"
android:fillColor="#FFFFFF"/>
</group>
</vector>
</aapt:attr>
<target android:name="group_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="translateX"
android:duration="1000"
android:valueFrom="0"
android:valueTo="7"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="group_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="translateX"
android:duration="1000"
android:valueFrom="0"
android:valueTo="-7"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>
后来我们又用了AnimationDrawable测试了一下慢放效果也不行,你仔细想一下:图片轮播放效果能好吗?
所以:AnimationDrawable不推荐
,我们这里推荐使用:AnimatedVectorDrawable为矢量图添加动画效果
问题3: Android12父主题设置android:windowBackground
被覆盖,看不到效果
不要紧,只要我们的UI设计师(美工)
按照如下尺寸规范来设计,使用静态中心图标,一样可以实现同样效果:
中心图标: 图标内容区域内边距2/3,防止元素被切
品牌名图标: 设计的尺寸比例为:2.5:1
2.2.SplashScreen兼容库
点击查看官方Splash Screen兼容库文档
(A).依赖库
点击查看Core库里面的最新版本
// 可在所有Android版本上使用的兼容库
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
(B).主题和外观配置
- 定义Activity应该使用的主题
<style name="Theme.App" parent="Theme.MaterialComponents.xxxxx.DarkActionBar">
<item name="android:windowBackground">@color/...</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">......</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
- 创建父主题给启动画面使用
<style name="Theme.App.Starting" parent="Theme.SplashScreen.IconBackground">
<item name="android:windowBackground">@drawable/...</item>
<item name="windowSplashScreenBackground">@color/...</item>
<item name="windowSplashScreenAnimationDuration">200</item>
<item name="postSplashScreenTheme">@style/Theme.App</item>
</style>
- AndroidManifest.xml配置Activity的主题
<manifest>
<application android:theme="@style/Theme.App.Starting">
<!-- application和activity,两个选一个配置: @style/Theme.App.Starting -->
<activity android:theme="@style/Theme.App.Starting">
...
©.初始化SplashScreen
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val splashScreen = installSplashScreen()
setContent { ...... }
splashScreen.setKeepVisibleCondition {
!mainViewModel.mockDataLoading()
}
splashScreen.setOnExitAnimationListener(this)
}
(D).中心图标大小修改
<item name="splashScreenIconSize">@dimen/....</item>
(E).遇到的问题
兼容库目前存在的问题
- 没有
android:windowSplashScreenBrandingImage
这个属性 - 配置了中心图标,会裁剪成圆形
- 低版本系统不配置
windowSplashScreenAnimatedIcon
会出现默认的Icon - Android12父主题设置
android:windowBackground
被覆盖,看不到效果
问题1: 是因为兼容库的layout文件目录下面的splash_screen_view.xml没有“品牌名的视图”
,大家点击查看一下,两个布局的xml内容就知道了:
frameworks下面的Android12的splash_screen_view.xml
core-splashscreen下面的兼容库的splash_screen_view.xml
但是我们在Android12即values-v31的themes.xml里面依然可以配置android:windowSplashScreenBrandingImage
这个属性,因为Android12的SplashScreen是集成在frameworks里面的;
问题2: 是因为兼容库里面使用了MaskedDrawable包装了Icon,会裁剪成圆形,图标内容设计要保留2/3的内边距,否则会出现内容被裁剪掉的问题;
如何修复这个裁剪圆形问题呢?
把源码拷贝出来,总共就3个源代码文件,自己复制出来修改删除也可以的
或者,图标设计准则为:内容保留内边距为2/3,防止元素被裁剪
问题3: 写一个透明的drawable.xml然后替换就行了,类似如下方式
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent"/>
<size android:width="0dp" android:height="0dp"/>
</shape>
问题4: Android12父主题设置android:windowBackground
被覆盖,看不到效果
不要紧,只要我们的UI设计师(美工)
按照如下尺寸规范来设计,使用静态中心图标,一样可以实现同样效果:
中心图标: 图标内容区域内边距2/3,防止元素被切
品牌名图标: 设计的尺寸比例为:2.5:1
(F).制作一个启动页
- 1.模仿快手App的启动页
只需要配置父主题的android:windowBackground
Android5.0 ~ Android11 效果
由于我们在文章上面介绍到Android12上,无法为SplashScreen设置父主题的android:windowBackground
,但我们依然可以通过配置静态中心图标来做到一样的效果的,请看下面的效果:
Android12 效果
如果你的UI设计师,给你矢量图,那么你就可以让中心图标在Android12系统上动起来了😆
另外,可以建议UI设计师:统一所有系统上,启动页“中心”图标,居中展示,不然会有点怪
- 2.动态图标启动页
如果设计成动态启动图标,这个需要考虑2个因素:
一、 低版本系统表现效果不一致,有些系统里面,动态图标只在启动页关闭的时候才显示(
亲测Android平板5.1.1系统就是这样的
);
二、 如何兼容低版本系统,是先展示底部品牌名,最后只能等动态图标显示咯?
如果喜欢折腾的同学,可以多测试测试,我在使用Android10.0测试动态图标的时候,效果看着还可以,具体系统下限在哪?
这个看你们产品设计定位了,还有测试妹妹是否同意你们用,效果是否符合你们的产品;
建议的做法是:
- Android 5.0 ~ Android 11.0系统,都统一使用
android:windowBackground
配置启动页背景 - Android12.0 如果UI设计师给你做了矢量图,你可以做动态的中心图标,不给你,使用静态图标也可以的,参考上面:
模拟快手App启动页
为了在模拟器上能正常显示出效果,我们在模拟器的开发者选项,找到Animator时长缩放设置为:动画时长x10
,放慢10倍,缺真机测试😂😂😂😂😂
Android12 动态启动页图标
3.源码分析
我们这里只分析Android12 SplashScreen,兼容库没有太多内容不足500行,感兴趣的同学可以自己阅读一下
我们在XXXActivity
里面第一次用到了splashScreen.setOnExitAnimationListener
,从这里开始往源头开始找,在下面的方法初始化了SplashScreen
//android.app.Activity
private SplashScreen getOrCreateSplashScreen() {
synchronized (this) {
if (mSplashScreen == null) {
mSplashScreen = new SplashScreen.SplashScreenImpl(this);
}
return mSplashScreen;
}
}
我们来看SplashScreenImpl实现类
//android.app.Activity
class SplashScreenImpl implements SplashScreen {
......
//把SplashScreenImpl添加到这个单例类里面
private final SplashScreenManagerGlobal mGlobal;
public SplashScreenImpl(Context context) {
mGlobal = SplashScreenManagerGlobal.getInstance();
}
@Override
public void setOnExitAnimationListener(@NonNull SplashScreen.OnExitAnimationListener listener) {
......
mGlobal.addImpl(this); // 用于后面执行启动画面将退出的回调
}
......
public void setSplashScreenTheme(@StyleRes int themeId) {
......
try {
//设置启动画面的主题
AppGlobals.getPackageManager().setSplashScreenTheme(......);
} catch (RemoteException e) {
Log.w(TAG, "Couldn't persist the starting theme", e);
}
}
}
// 启动画面管理器
class SplashScreenManagerGlobal {
......
// 管理多个闪屏实现
private final ArrayList<SplashScreenImpl> mImpls = new ArrayList<>();
private SplashScreenManagerGlobal() {
// 向此进程注册启动画面管理器
ActivityThread.currentActivityThread().registerSplashScreenManager(this);
}
......
private static final Singleton<SplashScreenManagerGlobal> sInstance =
new Singleton<SplashScreenManagerGlobal>() {
@Override
protected SplashScreenManagerGlobal create() {
return new SplashScreenManagerGlobal();
}
};
private void addImpl(SplashScreenImpl impl) {
synchronized (mGlobalLock) {
mImpls.add(impl);
}
}
private void removeImpl(SplashScreenImpl impl) {
synchronized (mGlobalLock) {
mImpls.remove(impl);
}
}
......
public void handOverSplashScreenView(IBinder token,SplashScreenView splashScreenView) {
//调用的是 => splashScreenView.transferSurface();
transferSurface(splashScreenView);
//回调 => impl.mExitAnimationListener.onSplashScreenExit(view);
dispatchOnExitAnimation(token, splashScreenView);
}
......
}
我们看到初始化SplashScreenManagerGlobal的时候,向此进程注册启动画面管理器
//android.app.ActivityThread
public void registerSplashScreenManager(SplashScreen.SplashScreenManagerGlobal manager) {
synchronized (this) {
mSplashScreenGlobal = manager;
}
}
如何把SplashScreen添加到当前的窗口的呢?
ActivityThread继承ClientTransactionHandler,里面有一个这样的抽象方法:
//android.app.ClientTransactionHandler
public abstract void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
@NonNull SplashScreenViewParcelable parcelable);
ActivityThread肯定会实现这个方法,那么是谁调用了它呢?由于篇幅问题,就不一行一行代码的去介绍分析了,感兴趣的同学,可以自己深入研究,我们下面贴出来调用的流程图看看 谁调用了handleAttachSplashScreenView(),它的调用链流程图如下:
好了,看完流程图,我们再看一下ActivityThread#handleAttachSplashScreenView
//android.app.ActivityThread
@Override
public void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
@Nullable SplashScreenView.SplashScreenViewParcelable parcelable) {
final DecorView decorView = (DecorView) r.window.peekDecorView();
if (parcelable != null && decorView != null) {
createSplashScreen(r, decorView, parcelable);
}
......
}
private void createSplashScreen(ActivityClientRecord r, DecorView decorView,
SplashScreenView.SplashScreenViewParcelable parcelable) {
// 初始化SplashScreenView构建器
final SplashScreenView.Builder builder = new SplashScreenView.Builder(r.activity);
// 从parcelable中获取配置数据,并通过build()初始化SplashScreenView,设置数据
final SplashScreenView view = builder.createFromParcel(parcelable).build();
// 把SplashScreenView添加到DecorView中
decorView.addView(view);
// 设置SystemUI颜色
view.attachHostActivityAndSetSystemUIColors(r.activity, r.window);
// 刷新视图
view.requestLayout();
......
}
核心的部分源码已经分析差不多了,剩下的一些源码,感兴趣的同学可以自己查看阅读
4.总结
- compileSdk升级到31
- 产品中统一使用兼容库SplashScreen
implementation 'androidx.core:core-splashscreen:最新版本'
- 演示示例中资源目录
drawable —— 定义低版本的drawable资源
drawable-v23 —— 定义6.0以上的资源
drawable-v31 —— 定义Android12及以上的资源
values —— 定义默认资源
values-night —— 定义默认深色模式资源
values-v23 —— 定义6.0以上系统资源
values-v31 —— 定义Android12及以上的资源
values-night-v31 —— 定义Android12及以上的深色模式资源
- 启动页图标设计准则
中心图标大图,内容需要保留2/3的内边距,否则图标会被裁剪掉,另外:图标尺寸大小可以修改;
底部品牌名图标:尺寸比例需要为 2.5:1
兼容库SplashScreen低版本不支持设置底部品牌图标,
Android12需要在values-v31目录手动添加如下属性,才可以显示品牌名图标;
<!--兼容库没有这个属性,我们需要在values-v31单独配置一下-->
<item name="android:windowSplashScreenBrandingImage">@drawable/...</item>
-
Android12以下系统可以使用
android:windowBackground
为父主题设置窗口背景,切记不要在Android12及以上系统设置父主题的窗口背景(因为没有效果😅😅)
-
Android12系统以下系统,使用
android:windowBackground
的话,一定要给windowSplashScreenAnimatedIcon
设置一个透明的drawable,否则会出现机器人图标 -
windowSplashScreenBackground 这个属性的颜色一定要注意,配置有问题的话,启动页过渡到主页面的时候,会有这个颜色闪出来,建议和Activity的
android:windowBackground
配置成一样的颜色 -
在启动画面上面,添加一个
“广告或者推广页面”
,代码和效果如下:
override fun onSplashScreenExit(splashScreenViewProvider: SplashScreenViewProvider) {
if(splashScreenViewProvider.view is ViewGroup){
// 在这里添加一个启动页广告或者推广页面
val composeView = ComposeView(this@SplashScreenCompatActivity).apply {
setContent {
SplashAdScreen(onCloseAd = {
splashScreenViewProvider.remove()
})
}
}
(splashScreenViewProvider.view as ViewGroup).addView(composeView)
return
}
}
实践 - 启动页添加广告或者推广页
参考地址
- 在线流程图制作
- 官方文档 Splash screens
- Google官网介绍矢量图
- 在线svg编辑器
- 在线制作矢量图动画
- 在线合并多个GIF制作
文章中示例的演示APK及源码地址:
静态图标启动页 | 动态图标启动页(Android12系统有动画效果) | 启动页加广告 |
---|---|---|
下载:SplashScreen快手启动页效果的apk001 | 下载:SplashScreen快手启动页效果的apk002 | 下载:SplashScreen启动页广告apk |
提取码:7gj2 | 提取码:b6ce | 提取码:fnva |
点击查看:SplashScreen演示示例的源码