0
点赞
收藏
分享

微信扫一扫

Android 12:SplashScreen Compat源码解析

萧萧雨潇潇 2022-03-31 阅读 46

前言

在上一篇文章Android 12 新功能:SplashScreen优化启动体验中我们介绍了Android 12上的一个新功能SplashScreen,同时提到了Google为了兼容低版本也提供了Androidx SplashScreen compat库,但是我们在使用的过程中发现这个库在Android 12和12以下版本表现并不一致,今天我们就从源码来分析一下实现细节。

SplashScreenViewProvider

Androidx SplashScreen compat库的代码其实很少,只有两个类:SplashScreenViewProvider和SplashScreen。

SplashScreenViewProvider是管理view的类,它有一个重要字段impl,如下:

private val impl: ViewImpl = when {
    Build.VERSION.SDK_INT >= 31 -> ViewImpl31(ctx)
    Build.VERSION.SDK_INT == 30 && Build.VERSION.PREVIEW_SDK_INT > 0 -> ViewImpl31(ctx)
    else -> ViewImpl(ctx)
} 

可以看到,如果是版本是31(或者30但是预览版本大于0)则执行ViewImpl31(ctx),否则执行ViewImpl(ctx),这里就可以看出处理的差异了。

先来看看ViewImpl(ctx)

private open class ViewImpl(val activity: Activity) {

    private val _splashScreenView: ViewGroup by lazy {
        FrameLayout.inflate(
            activity,
            R.layout.splash_screen_view,
            null
        ) as ViewGroup
    }

    init {
        val content = activity.findViewById<ViewGroup>(android.R.id.content)
        content.addView(_splashScreenView)
    }

    open val splashScreenView: ViewGroup get() = _splashScreenView
    open val iconView: View get() = splashScreenView.findViewById(R.id.splashscreen_icon_view)
    open val iconAnimationStartMillis: Long get() = 0
    open val iconAnimationDurationMillis: Long get() = 0
    open fun remove() =
        activity.findViewById<ViewGroup>(android.R.id.content).removeView(splashScreenView)
} 

可以看到在低版本上,会加载一个布局splash_screen_view,这个布局很简单,只有一个ImageView:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

  <ImageView
      android:id="@+id/splashscreen_icon_view"
      android:layout_width="@dimen/splashscreen_icon_size"
      android:layout_height="@dimen/splashscreen_icon_size"
      android:layout_gravity="center" />

</FrameLayout> 

然后会将这个布局添加到activity的content上,并获取其中的ImageView。这里也就解释了为什么Androidx SplashScreen compat库不支持windowSplashScreenBrandingImage这个属性,因为在低版本上只有中间的一个ImageView(我想Google后续应该会继续优化这里)。

再来看看ViewImpl31(ctx)

@RequiresApi(31)
private class ViewImpl31(activity: Activity) : ViewImpl(activity) {
    lateinit var platformView: SplashScreenView

    override val splashScreenView get() = platformView

    override val iconView get() = platformView.iconView!!

    override val iconAnimationStartMillis: Long
        get() = platformView.iconAnimationStart?.toEpochMilli() ?: 0

    override val iconAnimationDurationMillis: Long
        get() = platformView.iconAnimationDuration?.toMillis() ?: 0

    override fun remove() = platformView.remove()
} 

这里就不太一样了,并没有加载什么布局,而是直接使用了一个platformView,这个又是从哪来的呢?

答案是构造函数,SplashScreenViewProvider有两个构造函数:

public class SplashScreenViewProvider internal constructor(ctx: Activity) {

    @RequiresApi(31)
    internal constructor(platformView: SplashScreenView, ctx: Activity) : this(ctx) {
        (impl as ViewImpl31).platformView = platformView
    }
    ... 

在低版本上只需要传入activity即可,在31版本上则传入了一个SplashScreenView对象,SplashScreenView这个类就是31版本新添加的。

所以可以看到在31版本上启动页面就使用自带的SplashScreenView,而在低版本上则使用了一个简单的布局来处理,这也导致了低版本上部分功能缺失。

SplashScreen

真正实现启动画面的是SplashScreen类,它也有一个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)
} 

看到根据版本分成了三种处理,先来看看Impl31(activity)

@RequiresApi(31) // TODO(188897399) Update to "S" once finalized
private class Impl31(activity: Activity) : Impl(activity) {
    var preDrawListener: OnPreDrawListener? = null

    override fun install() {
        setPostSplashScreenTheme(activity.theme, TypedValue())
    }

    override fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
        ...
    }

    override fun setOnExitAnimationListener(
        exitAnimationListener: OnExitAnimationListener
    ) {
        activity.splashScreen.setOnExitAnimationListener {
            val splashScreenViewProvider = SplashScreenViewProvider(it, activity)
            exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
        }
    }
} 

首先看在install中执行了setPostSplashScreenTheme(activity.theme, TypedValue())

protected fun setPostSplashScreenTheme(
    currentTheme: Resources.Theme,
    typedValue: TypedValue
) {
    if (currentTheme.resolveAttribute(R.attr.postSplashScreenTheme, typedValue, true)) {
        finalThemeId = typedValue.resourceId
        if (finalThemeId != 0) {
            activity.setTheme(finalThemeId)
        }
    } else {
        throw Resources.NotFoundException(
            "Cannot set AppTheme. No theme value defined for attribute " +
                activity.resources.getResourceName(R.attr.postSplashScreenTheme)
        )
    }
} 

可以看到这里对activity的theme进行了重新设置,这样activity就不会使用SplashScreen的样式,而是使用postSplashScreenTheme设置的样式,保证了样式的正确性,避免了很多问题,简化了迁移处理。

然后在setOnExitAnimationListener函数中执行了activity.splashScreen.setOnExitAnimationListener,这个splashScreen是31版本上Activity新增的函数,可以自动创建一个SplashScreen对象(注意和我们现在讲的不是一个类)并返回:

public final @NonNull SplashScreen getSplashScreen() {
    return getOrCreateSplashScreen();
}

private SplashScreen getOrCreateSplashScreen() {
    synchronized (this) {
        if (mSplashScreen == null) {
            mSplashScreen = new SplashScreen.SplashScreenImpl(this);
        }
        return mSplashScreen;
    }
} 

在它的setOnExitAnimationListener回调中,创建了一个SplashScreenViewProvider(这里传入了已经创建好的SplashScreenView)。所以可以看到在31版本上,Androidx SplashScreen compat库并没有进行太多处理,而是全部托管给新版本自带的SplashScreen功能。

那么在看看Impl23(activity)

private class Impl23(activity: Activity) : Impl(activity) {
    override fun adjustInsets(
        view: View,
        splashScreenViewProvider: SplashScreenViewProvider
    ) {
        // Offset the icon if the insets have changed
        val rootWindowInsets = view.rootWindowInsets
        val ty =
            rootWindowInsets.systemWindowInsetTop - rootWindowInsets.systemWindowInsetBottom
        splashScreenViewProvider.iconView.translationY = -ty.toFloat() / 2f
    }
} 

没有做什么,但是它继承了Impl,所以我们看Impl的源码:

private open class Impl(val activity: Activity) {
    ...

    open fun install() {
        ...
        setPostSplashScreenTheme(currentTheme, typedValue)
    }

    protected fun setPostSplashScreenTheme(
        currentTheme: Resources.Theme,
        typedValue: TypedValue
    ) {
        ...
    }

    open fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
        ...
    }

    open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
        animationListener = exitAnimationListener

        val splashScreenViewProvider = SplashScreenViewProvider(activity)
        val finalBackgroundResId = backgroundResId
        val finalBackgroundColor = backgroundColor
        if (finalBackgroundResId != null && finalBackgroundResId != Resources.ID_NULL) {
            splashScreenViewProvider.view.setBackgroundResource(finalBackgroundResId)
        } else if (finalBackgroundColor != null) {
            splashScreenViewProvider.view.setBackgroundColor(finalBackgroundColor)
        } else {
            splashScreenViewProvider.view.background = activity.window.decorView.background
        }

        splashScreenViewProvider.view.findViewById<ImageView>(R.id.splashscreen_icon_view)
            .setBackgroundResource(icon)

        splashScreenViewProvider.view.addOnLayoutChangeListener(
            object : OnLayoutChangeListener {
                override fun onLayoutChange(
                    view: View,
                    left: Int,
                    top: Int,
                    right: Int,
                    bottom: Int,
                    oldLeft: Int,
                    oldTop: Int,
                    oldRight: Int,
                    oldBottom: Int
                ) {
                    adjustInsets(view, splashScreenViewProvider)
                    if (!view.isAttachedToWindow) {
                        return
                    }

                    view.removeOnLayoutChangeListener(this)
                    if (!splashScreenWaitPredicate.shouldKeepOnScreen()) {
                        dispatchOnExitAnimation(splashScreenViewProvider)
                    } else {
                        mSplashScreenViewProvider = splashScreenViewProvider
                    }
                }
            })
    }

    ...
} 

代码很多,可以看到install同样执行了setPostSplashScreenTheme,保证了activity的样式。

我们重点来看看setOnExitAnimationListener函数,可以看到这里与Impl31完全不同,因为在低版本上activity没有SplashScreen,所以这里直接创建了SplashScreenViewProvider,然后对其中的布局进行填充处理。注意这行代码:

splashScreenViewProvider.view.findViewById<ImageView>(R.id.splashscreen_icon_view).setBackgroundResource(icon) 

R.id.splashscreen_icon_view就是上面提到的布局中的那个ImageView,可以看到启动图片是以背景的方式设置给它的,而且没有其他处理了。所以在低版本上并没有圆形遮罩,同时设置动画也是无效的,因为没有启动,Google在后续版本应该会继续优化这里。

总结

综上,我们可以看到,虽然Androidx SplashScreen compat库向后兼容,但是因为在低版本上布局和处理都比较简单,所以低版本上的效果实际上并不如Android 12,大家做迁移兼容的时候一定要注意。

最后

按照国际惯例,给大家分享一套十分好用的Android进阶资料:《全网最全Android开发笔记》。

整个笔记一共8大模块、729个知识点,3382页,66万字,可以说覆盖了当下Android开发最前沿的技术点,和阿里、腾讯、字节等等大厂面试看重的技术。

图片

图片

因为所包含的内容足够多,所以,这份笔记不仅仅可以用来当学习资料,还可以当工具书用。

如果你需要了解某个知识点,不管是Shift+F 搜索,还是按目录进行检索,都能用最快的速度找到你要的内容。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照整个知识体系编排的。

(一)架构师必备Java基础

1、深入理解Java泛型

2、注解深入浅出

3、并发编程

4、数据传输与序列化

5、Java虚拟机原理

6、高效IO

……

图片

(二)设计思想解读开源框架

1、热修复设计

2、插件化框架设计

3、组件化框架设计

4、图片加载框架

5、网络访问框架设计

6、RXJava响应式编程框架设计

……

图片

(三)360°全方位性能优化

1、设计思想与代码质量优化

2、程序性能优化

  • 启动速度与执行效率优化
  • 布局检测与优化
  • 内存优化
  • 耗电优化
  • 网络传输与数据储存优化
  • APK大小优化

3、开发效率优化

  • 分布式版本控制系统Git
  • 自动化构建系统Gradle

……

图片

(四)Android框架体系架构

1、高级UI晋升

2、Android内核组件

3、大型项目必备IPC

4、数据持久与序列化

5、Framework内核解析

……

图片

(五)NDK模块开发

1、NDK开发之C/C++入门

2、JNI模块开发

3、Linux编程

4、底层图片处理

5、音视频开发

6、机器学习

……

图片

(六)Flutter学习进阶

1、Flutter跨平台开发概述

2、Windows中Flutter开发环境搭建

3、编写你的第一个Flutter APP

4、Flutter Dart语言系统入门

……

图片

(七)微信小程序开发

1、小程序概述及入门

2、小程序UI开发

3、API操作

4、购物商场项目实战

……

图片

(八)kotlin从入门到精通

1、准备开始

2、基础

3、类和对象

4、函数和lambda表达式

5、其他

……

图片

举报

相关推荐

0 条评论