浅谈 splashscreen,PS 可用整张图片做背景哦!!越过Google的限制

前言:启动页几乎所有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得源码,仿照写,稍微改动一下即可。

其中得原理就不细说了,就简单的写一下!

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值