正确实践Jetpack SplashScreen API —— 在所有Android系统上使用总结,内含原理分析

5,223

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
                }
            }
        }
    )

(C).关闭启画面的动画

// 自己定制关闭的动画
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">
...

(C).初始化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
    }
}

实践 - 启动页添加广告或者推广页

参考地址

文章中示例的演示APK及源码地址:

静态图标启动页动态图标启动页
(Android12系统有动画效果)
启动页加广告
下载:SplashScreen快手启动页效果的apk001下载:SplashScreen快手启动页效果的apk002下载:SplashScreen启动页广告apk
提取码:7gj2提取码:b6ce提取码:fnva

点击查看:SplashScreen演示示例的源码