Android 12:SplashScreen Compat源码解析
Posted BennuCTech
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 12:SplashScreen Compat源码解析相关的知识,希望对你有一定的参考价值。
前言
在上一篇文章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,大家做迁移兼容的时候一定要注意。
关注公众号:BennuCTech,获取更多干货!
以上是关于Android 12:SplashScreen Compat源码解析的主要内容,如果未能解决你的问题,请参考以下文章
Android 12 SplashScreen API快速入门
Android 12 SplashScreen API快速入门