Android 12:SplashScreen源码分析

在这里插入图片描述

前言

在上一篇文章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)

先来看看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)

再来看看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)

看到根据版本分成了三种处理,先来看看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)

那么在看看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,大家做迁移兼容的时候一定要注意。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Android 12中的splashscreen(启动画面)是一种应用程序启动时显示的短暂界面,用于提供应用程序的品牌标识和加载过程的视觉反馈。与以往的版本不同,Android 12引入了一些新的特性和改进来提升splashscreen的用户体验。 首先,Android 12splashscreen支持更多的自定义选项。开发者可以选择使用单一的图片、动画或者视频作为splashscreen的背景,以展示应用程序的标志性元素或者品牌内容。这增强了应用程序的品牌识别度,并为用户提供了更具吸引力的界面。 其次,Android 12还支持在splashscreen上显示应用程序的加载进度。开发者可以通过进度指示器或动画来告知用户应用程序的加载进度,让用户对应用程序的启动过程有更清晰的认知。这种视觉反馈可以提高用户对应用程序的满意度,并为用户提供更好的等待体验。 此外,Android 12还增加了一个预热阶段(pre-warming)来提升splashscreen的加载速度。当用户点击应用程序图标时,系统会在显示splashscreen之前暂时加载应用程序的一部分数据和资源,以缩短splashscreen的显示时间和启动过程的等待时间。这使得用户可以更快地进入应用程序,提高了用户体验的流畅度。 综上所述,Android 12splashscreen通过自定义选项、加载进度显示和预热阶段等特性和改进,提供了更具吸引力、更好的等待体验和更高的启动速度。这使得应用程序能够更好地展示自己的品牌标识,同时提升用户对应用程序的满意度和流畅度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值