前言:启动页几乎所有app都会有,常规做法就是用activity和fragment来实现启动页,通常启动页有很多定制化,比如背景、动态图等。Google后来就出了androidx.core:core-splashscreen:1.0.0
但使用或了解过这个api都知道,有一定的限制,接下来我们简单的刨析一下为什么,最后我们再写个可定制的。
splashscreen源码不多
一、SplashScreen、SplashScreenViewProvider
负责启动页的整个逻辑,有两个impl,分别对31以上或以下做兼容,
31以上原理就是修改activity的SplashScreenView的icon
31以下就是修改rootview,新增一个splashScreenView add到rootview里(有个splash_screen_view布局)
但谷歌只提供了纯颜色的背景修改,中间icon,和底部的图片修改,具体可以看values的定义
二、MaskedDrawable
负责icon的裁剪,这里好坑,要理解他源码的说才行,在SplashScreen有个
private const val MASK_FACTOR = 2 / 3f
他是用于裁剪icon的把icon裁剪2/3,所有做图的时候就要注意了
举个例子:图片非透明区域占2/3大小,且居中显示。比如:图片大小240*240,非透明区域则为160*160
三、ThemeUtils
主要是Android31后的一些主题兼容
这里就简单说明带过一下吧。
重点来了!! 如何可以启动页定制成图片背景或动图之类的?
只需要修改SplashScreen,splash_screen_view.xml,themes主题,MaskedDrawable可以不要了,直接上源码吧
一、SplashScreen
@SuppressLint("CustomSplashScreen")
class SplashScreen private constructor(activity: Activity) {
private val impl = when {
SDK_INT >= 31 -> Impl31(activity)
else -> Impl(activity)
}
public companion object {
@JvmStatic
public fun Activity.installSplashScreenCustom(): SplashScreen {
val splashScreen = SplashScreen(this)
splashScreen.install()
return splashScreen
}
}
public fun setKeepOnScreenCondition(condition: KeepOnScreenCondition) {
impl.setKeepOnScreenCondition(condition)
}
@SuppressWarnings("ExecutorRegistration") // Always runs on the MainThread
public fun setOnExitAnimationListener(listener: OnExitAnimationListener) {
impl.setOnExitAnimationListener(listener)
}
private fun install() {
impl.install()
}
public fun interface OnExitAnimationListener {
@MainThread
public fun onSplashScreenExit(splashScreenViewProvider: SplashScreenViewProvider)
}
public fun interface KeepOnScreenCondition {
@MainThread
public fun shouldKeepOnScreen(): Boolean
}
private open class Impl(val activity: Activity) {
var finalThemeId: Int = 0
var backgroundResId: Int? = null
var backgroundColor: Int? = null
var splashScreenWaitPredicate = KeepOnScreenCondition { false }
private var animationListener: OnExitAnimationListener? = null
private var mSplashScreenViewProvider: SplashScreenViewProvider? = null
open fun install() {
val typedValue = TypedValue()
val currentTheme = activity.theme
if (currentTheme.resolveAttribute(
R.attr.windowSplashScreenBackground_Custom,
typedValue,
true
)
) {
backgroundResId = typedValue.resourceId
backgroundColor = typedValue.data
}
setPostSplashScreenTheme(currentTheme, typedValue)
}
protected fun setPostSplashScreenTheme(
currentTheme: Resources.Theme,
typedValue: TypedValue
) {
if (currentTheme.resolveAttribute(
R.attr.postSplashScreenTheme_Custom,
typedValue,
true
)
) {
finalThemeId = typedValue.resourceId
if (finalThemeId != 0) {
activity.setTheme(finalThemeId)
}
}
}
open fun setKeepOnScreenCondition(keepOnScreenCondition: KeepOnScreenCondition) {
splashScreenWaitPredicate = keepOnScreenCondition
val contentView = activity.findViewById<View>(android.R.id.content)
val observer = contentView.viewTreeObserver
observer.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
return false
}
contentView.viewTreeObserver.removeOnPreDrawListener(this)
mSplashScreenViewProvider?.let(::dispatchOnExitAnimation)
return true
}
})
}
open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
animationListener = exitAnimationListener
val splashScreenViewProvider = SplashScreenViewProvider(activity)
val finalBackgroundResId = backgroundResId
val finalBackgroundColor = backgroundColor
val splashScreenView = splashScreenViewProvider.view
if (finalBackgroundResId != null && finalBackgroundResId != Resources.ID_NULL) {
splashScreenView.setBackgroundResource(finalBackgroundResId)
} else if (finalBackgroundColor != null) {
splashScreenView.setBackgroundColor(finalBackgroundColor)
} else {
splashScreenView.background = activity.window.decorView.background
}
splashScreenView.addOnLayoutChangeListener(
object : OnLayoutChangeListener {
override fun onLayoutChange(
view: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
if (!view.isAttachedToWindow) {
return
}
view.removeOnLayoutChangeListener(this)
if (!splashScreenWaitPredicate.shouldKeepOnScreen()) {
dispatchOnExitAnimation(splashScreenViewProvider)
} else {
mSplashScreenViewProvider = splashScreenViewProvider
}
}
})
}
fun dispatchOnExitAnimation(splashScreenViewProvider: SplashScreenViewProvider) {
val finalListener = animationListener ?: return
animationListener = null
splashScreenViewProvider.view.postOnAnimation {
splashScreenViewProvider.view.bringToFront()
finalListener.onSplashScreenExit(splashScreenViewProvider)
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
private class Impl31(activity: Activity) : Impl(activity) {
var preDrawListener: OnPreDrawListener? = null
var mDecorFitWindowInsets = true
val hierarchyListener = object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
if (child is SplashScreenView) {
mDecorFitWindowInsets = computeDecorFitsWindow(child)
(activity.window.decorView as ViewGroup).setOnHierarchyChangeListener(null)
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
// no-op
}
}
fun computeDecorFitsWindow(child: SplashScreenView): Boolean {
val inWindowInsets = WindowInsets.Builder().build()
val outLocalInsets = Rect(
Int.MIN_VALUE, Int.MIN_VALUE, Int.MAX_VALUE,
Int.MAX_VALUE
)
return !(inWindowInsets === child.rootView.computeSystemWindowInsets
(inWindowInsets, outLocalInsets) && outLocalInsets.isEmpty)
}
override fun install() {
setPostSplashScreenTheme(activity.theme, TypedValue())
(activity.window.decorView as ViewGroup).setOnHierarchyChangeListener(
hierarchyListener
)
}
override fun setKeepOnScreenCondition(keepOnScreenCondition: KeepOnScreenCondition) {
splashScreenWaitPredicate = keepOnScreenCondition
val contentView = activity.findViewById<View>(android.R.id.content)
val observer = contentView.viewTreeObserver
if (preDrawListener != null && observer.isAlive) {
observer.removeOnPreDrawListener(preDrawListener)
}
preDrawListener = object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
return false
}
contentView.viewTreeObserver.removeOnPreDrawListener(this)
return true
}
}
observer.addOnPreDrawListener(preDrawListener)
}
override fun setOnExitAnimationListener(
exitAnimationListener: OnExitAnimationListener
) {
activity.splashScreen.setOnExitAnimationListener { splashScreenView ->
applyAppSystemUiTheme()
val splashScreenViewProvider = SplashScreenViewProvider(splashScreenView, activity)
exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
}
}
private fun applyAppSystemUiTheme() {
val tv = TypedValue()
val theme = activity.theme
val window = activity.window
if (theme.resolveAttribute(attr.statusBarColor, tv, true)) {
window.statusBarColor = tv.data
}
if (theme.resolveAttribute(attr.navigationBarColor, tv, true)) {
window.navigationBarColor = tv.data
}
if (theme.resolveAttribute(attr.windowDrawsSystemBarBackgrounds, tv, true)) {
if (tv.data != 0) {
window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
} else {
window.clearFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
}
if (theme.resolveAttribute(attr.enforceNavigationBarContrast, tv, true)) {
window.isNavigationBarContrastEnforced = tv.data != 0
}
if (theme.resolveAttribute(attr.enforceStatusBarContrast, tv, true)) {
window.isStatusBarContrastEnforced = tv.data != 0
}
val decorView = window.decorView as ViewGroup
ThemeUtils.Api31.applyThemesSystemBarAppearance(theme, decorView, tv)
decorView.setOnHierarchyChangeListener(null)
window.setDecorFitsSystemWindows(mDecorFitWindowInsets)
}
}
}
二、创建splash_screen_view.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="false"
android:fitsSystemWindows="false">
<ImageView
android:id="@+id/splashscreen_icon_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/bg_splash_screen" />
</FrameLayout>
三、定义主题
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="postSplashScreenTheme_Custom" format="reference" />
<attr name="windowSplashScreenBackground_Custom" format="reference" />
<style name="Theme.SplashScreen_Custom" parent="Theme.SplashScreen.Common_Custom">
<item name="postSplashScreenTheme_Custom">?android:attr/theme</item>
<item name="windowSplashScreenBackground_Custom">@drawable/bg_splash_screen</item>
</style>
<style name="Theme.SplashScreen.Common_Custom" parent="android:Theme.DeviceDefault.Light.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@drawable/bg_splash_screen</item>
<item name="android:opacity">opaque</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:fitsSystemWindows">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
</resources>
四、使用
<!--启动页主题-->
<style name="SplashTheme" parent="Theme.SplashScreen_Custom">
<!--当SplashScreen结束时,复原AppTheme-->
<item name="postSplashScreenTheme_Custom">@style/AppTheme</item>
</style>
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreenCustom()
super.onCreate(savedInstanceState)
}
细心同学会发现bg_splash_screen那里来的?
bg_splash_screen就是你想用的启动页背景啦!!!
例子:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<stroke android:color="@color/white" />
</shape>
</item>
<item>
<bitmap
android:gravity="fill"
android:src="@mipmap/bg_splash" />
</item>
<item android:top="220dp">
<bitmap
android:gravity="top|clip_horizontal"
android:src="@mipmap/ic_logo" />
</item>
</layer-list>
这里改动得比较粗糙,有兴趣其实可以看一下androidx.core:core-splashscreen:1.0.0得源码,仿照写,稍微改动一下即可。
其中得原理就不细说了,就简单的写一下!