Android 12 新APP启动画面(SplashScreen API)简介&&源码分析

Posted guangdeshishe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 12 新APP启动画面(SplashScreen API)简介&&源码分析相关的知识,希望对你有一定的参考价值。

以往的启动画面

  • 默认情况下刚启动APP时会显示一会白色背景
  • 如果把这个启动背景设置为null,则一闪而过的白色会变成黑色
  • 如果把启动Activity设置为背景透明【< item name=“android:windowIsTranslucent”>true</ item>】或者禁用了启动画面【< item name=“android:windowDisablePreview”>true</ item>】;虽然一闪而过的黑色或者白色没有了,但是因为背景透明了就会看到桌面,导致的结果就是感觉APP启动慢了
  • 通常我们会在主题里给它设置一张公司Logo图片【< item name=“android:windowSplashscreenContent”>@drawable/splash</ item>】,这样就感觉APP启动快了

全新的APP启动画面

  • 统一的设计标准,不同APP展现出来的整体样式是一样的
  • 支持通过配置主题的方式更换中间的Logo/动画、背景色、图片的背景色、底部公司品牌Logo等
  • 支持延长显示的时间
  • 支持自定义关闭启动画面的动画

注意事项

  • 【< item name=“android:windowSplashscreenContent”>@drawable/splash< /item>】和【< item name=“android:windowDisablePreview”>true</ item>】在Android 12设备上都失效(已废弃),即使targetSdkVersion没有升级到31也是这样
  • Android 12新启动画面,targetSdkVersion不需要升级到31,但是compileSdkVersion一定要升级到31才可以,否则编译时无法找到主题里这些新增的属性
  • 启动画面的图标/动画应该遵循Adaptive Icon(自适应图标)的规范,不然图片/动画可能会显示异常

使用方法

APP在Android12上默认启动效果

在主题中通过配置自定义启动画面

设置启动画面背景色

<!--设置启动画面背景色-->
<item name="android:windowSplashScreenBackground">#ff9900</item>

效果图:

设置启动画面居中显示的图标或者动画

<!--设置启动画面居中显示的图标或者动画-->
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<!--设置启动画面在关闭之前显示的时长,最长1000毫秒-->
<item name="android:windowSplashScreenAnimationDuration">1000</item>
  • windowSplashScreenAnimationDuration指的是启动画面显示的时间,跟动画的时长无关,也就是如果动画时间超过这个时间,它不会等待动画结束,而是直接关闭;如果希望动画显示时间超过1秒,则需要参考后面【延迟关闭启动画面】部分

效果图:

设置中间显示图标区域的背景色

用于解决图标和背景颜色接近显示不清问题

<!--设置中间显示图标区域的背景色,用于解决图标和背景颜色接近显示不清问题-->
<item name="android:windowSplashScreenIconBackgroundColor">#ff0000</item>

效果图:

设置启动画面底部公司品牌图片

官方不推荐使用,可能是因为底部再加个图片不好看吧

<!--设置启动画面底部公司品牌图片,官方不推荐使用-->
<item name="android:windowSplashScreenBrandingImage">@drawable/ic_launcher_foreground</item>

效果图:

延迟关闭启动画面

有时候希望启动画面能在数据准备好之后才关闭,或者动画时间超过1秒

class MainActivity() : AppCompatActivity() {
	var isDataReady = false
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val contentView = findViewById<View>(android.R.id.content)
        contentView.viewTreeObserver.addOnPreDrawListener(object :
            ViewTreeObserver.OnPreDrawListener {
            override fun onPreDraw(): Boolean {
                if (isDataReady) {//判断是否可以关闭启动动画,可以则返回true
                    contentView.viewTreeObserver.removeOnPreDrawListener(this)
                }
                return isDataReady
            }
        })
        Thread.sleep(5000)//模拟耗时
        isDataReady = true
    }
}

效果图:

定制退出动画

启动画面默认结束后是直接消失的,可能会显得有些突兀,全新的SplashScreen支持定制退出动画

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    splashScreen.setOnExitAnimationListener { splashScreenView ->
        val slideUp = ObjectAnimator.ofFloat(
            splashScreenView,
            View.TRANSLATION_Y,
            0f,
            -splashScreenView.height.toFloat()
        )
        slideUp.interpolator = AnticipateInterpolator()
        slideUp.duration = 2000L
        slideUp.doOnEnd { splashScreenView.remove() }
        slideUp.start()
    }
}

效果图:

  • splashScreen是Activity中的getSplashScreen()方法返回的

  • 官方说SplashScreenView在动画结束后要remove掉,实际测试发现不remove也是可以的,因为动画结束后启动画面已经被移动到看不到的地方了,不影响后续操作;但是通过查看SplashScreenView的remove方法源码,除了将SplashScreenView设为不可见外,还有图片等资源的回收操作,所以建议还是要调用它的remove方法以回收资源

    class SplashScreenView extends FrameLayout {
    	public void remove() {
    		if (mHasRemoved) {
    			return;
    		}
    		setVisibility(GONE);
    		if (mParceledIconBitmap != null) {
    			if (mIconView instanceof ImageView) {
    				((ImageView) mIconView).setImageDrawable(null);
    			} else if (mIconView != null) {
    				mIconView.setBackground(null);
    			}
    			mParceledIconBitmap.recycle();
    			mParceledIconBitmap = null;
    		}
    		if (mParceledBrandingBitmap != null) {
    			mBrandingImageView.setBackground(null);
    			mParceledBrandingBitmap.recycle();
    			mParceledBrandingBitmap = null;
    		}
    		if (mParceledIconBackgroundBitmap != null) {
    			if (mIconView != null) {
    				mIconView.setBackground(null);
    			}
    			mParceledIconBackgroundBitmap.recycle();
    			mParceledIconBackgroundBitmap = null;
    		}
    		if (mWindow != null) {
    			final DecorView decorView = (DecorView) mWindow.peekDecorView();
    			if (DEBUG) {
    				Log.d(TAG, "remove starting view");
    			}
    			if (decorView != null) {
    				decorView.removeView(this);
    			}
    			restoreSystemUIColors();
    			mWindow = null;
    		}
    		if (mHostActivity != null) {
    			mHostActivity.setSplashScreenView(null);
    			mHostActivity = null;
    		}
    		mHasRemoved = true;
    	}
    }
    

计算启动画面中间的动画剩余时长

上面我们说到可以自定义退出动画,也就是设置splashScreen.setOnExitAnimationListener,这个接口会在将要显示APP主界面时回调;

  • 如果设备性能比较差,可能会出现中间那个图标动画已经结束,但是APP主界面却还没显示的情况,这个时候如果启动画面退出时还做一次动画,会导致APP进入主界面的时间更长,遇到这种情况应该取消退出动画,让用户及时看到主界面会更好一些;

  • 如果设备性能比较好,假如本来设置的启动画面中间图标动画时长1000毫秒,但是只执行了500毫秒的动画就可以开始显示APP主界面动画了,却因为固定的退出动画时长,导致需要等待更久的时间才能看到主界面

所以应该根据启动画面中间图标动画时长执行剩余时间来决定退出动画的时长,这样才能尽快让用户看到APP主界面,并保证好的体验效果

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    splashScreen.setOnExitAnimationListener { splashScreenView ->
        val slideUp = ObjectAnimator.ofFloat(
            splashScreenView,
            View.TRANSLATION_Y,
            0f,
            -splashScreenView.height.toFloat()
        )
        slideUp.interpolator = AnticipateInterpolator()

		//计算合适的退出动画时长
        var targetDuration = 0L
        val animationDuration = splashScreenView.iconAnimationDuration//图标动画时长
        val animationStart = splashScreenView.iconAnimationStart//图标动画开始时间
        if (animationDuration != null && animationStart != null) {
            val remainingDuration = (
                    animationDuration.toMillis() - (System.currentTimeMillis() - animationStart.toEpochMilli())
                    ).coerceAtLeast(0L)//计算剩余时间,如果小于0则赋值0
            targetDuration = remainingDuration
        }
        slideUp.duration = targetDuration
        slideUp.doOnEnd { splashScreenView.remove() }
        slideUp.start()
    }
}
  • 需要注意官网示例代码中的splashScreenView.getIconAnimationDurationMillis()splashScreenView.getIconAnimationStartMillis()在实际测试中,SplashScreenView中并没有发现这两个方法,取而代之的是splashScreenView.getIconAnimationDuration()splashScreenView.getIconAnimationStart();而且这两个方法返回的对象并不是long,而是DurationInstant,需要分别再次调用它们的toMillis()toEpochMilli()方法转换成毫秒(long)
  • 官网示例代码中的SystemClock.uptimeMillis()在实际测试中发现也是不对的,SystemClock.uptimeMillis()返回的是从手机开机时到现在的时间(毫秒),但是getIconAnimationStart()返回的是却是当时手机系统显示的时间
  • 需要注意的是animationDurationiconAnimationStart只有当<item name="android:windowSplashScreenAnimatedIcon">配置的是动画时才不为null,如果配置的只是普通图片,则会返回null,所以计算剩余时长时需要判断非空

源码分析

涉及到的主要类

  • SplashScreenView:启动画面所显示的View,继承自FrameLayout;对应系统布局文件是:splash_screen_view.xml

    //splash_screen_view.xml
    <android.window.SplashScreenView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:orientation="vertical">
    
        <View android:id="@+id/splashscreen_icon_view"
              android:layout_height="wrap_content"
              android:layout_width="wrap_content"
              android:layout_gravity="center"
              android:contentDescription="@string/splash_screen_view_icon_description"/>
    
        <View android:id="@+id/splashscreen_branding_view"
              android:layout_height="wrap_content"
              android:layout_width="wrap_content"
              android:layout_gravity="center_horizontal|bottom"
              android:layout_marginBottom="60dp"
              android:contentDescription="@string/splash_screen_view_branding_description"/>
    
    </android.window.SplashScreenView>
    
    public final class SplashScreenView extends FrameLayout {
    	private int mInitBackgroundColor;//界面背景色
    	private View mIconView;//界面中间显示的图标
        private View mBrandingImageView;//底部品牌图标
        private Duration mIconAnimationDuration;//启动画面显示时长
        private Instant mIconAnimationStart;//中间动画开始执行的时间
    	public static class Builder {
    		private Drawable mIconDrawable;//界面中间显示的图标
            private Drawable mIconBackground;//界面中间显示的图标的背景色
            private Drawable mBrandingDrawable;//底部品牌图标
            private Instant mIconAnimationStart;//中间动画开始执行的时间
            private Duration mIconAnimationDuration;//启动画面显示时长
    		
    		public SplashScreenView build() {
    			...
    			final SplashScreenView view = (SplashScreenView)
                        layoutInflater.inflate(R.layout.splash_screen_view, null, false);
                view.mInitBackgroundColor = mBackgroundColor;
    			view.setBackgroundColor(mBackgroundColor);//设置背景色
    			
    			ImageView imageView = view.findViewById(R.id.splashscreen_icon_view);
    			imageView.setImageDrawable(mIconDrawable);设置界面中间图标/动画
    			imageView.setBackground(mIconBackground);//设置中间显示的图标的背景色
    			
    			view.mBrandingImageView = view.findViewById(R.id.splashscreen_branding_view);
    			view.mBrandingImageView.setBackground(mBrandingDrawable);//设置底部品牌图标
    			...
    			return view;
    		}
    	}
    }
    
  • SplashScreen:用于客户端与SplashScreenView交互的接口,比如:自定义启动画面退出时的动画

  • StartingSurfaceController:Android12新增,用于管理创建/释放starting window surface;这个类里面通过系统属性persist.debug.shell_starting_surface的值来决定是使用全新的SplashScreenView还是旧版的启动画面

    • persist.debug.shell_starting_surface在Android12上默认为空,根据源码来看,如果为空,则默认值为true;也就是说Android12上默认是启用新版启动画面的,通过adb命令:adb shell setprop persist.debug.shell_starting_surface false并且重启系统后,可以禁用全新启动画面,所有APP启动画面将变回旧版
    public class StartingSurfaceController {
    	static final boolean DEBUG_ENABLE_SHELL_DRAWER =
                SystemProperties.getBoolean("persist.debug.shell_starting_surface", true);
    			
    	StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, String packageName,
                int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
                int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
            if (!DEBUG_ENABLE_SHELL_DRAWER) {//使用旧版的启动画面
                return mService.mPolicy.addSplashScreen(activity.token, activity.mUserId, packageName,
                        theme, compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
                        overrideConfig, displayId);
            }
    		//使用全新SplashScreenView
            synchronized (mService.mGlobalLock) {
                final Task task = activity.getTask();
                if (task != null && mService.mAtmService.mTaskOrganizerController.addStartingWindow(
                        task, activity, theme, null /* taskSnapshot */)) {
                    return new ShellStartingSurface(task);
                }
            }
            return null;
        }
    }
    
  • StartingSurfaceDrawer:创建SplashScreenView和启动窗口的主要流程

    public class StartingSurfaceDrawer {
    	void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
                @StartingWindowType int suggestType) {
            ... ...
    		//创建启动窗口参数
            final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                    WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
            params.setFitInsetsSides(0);
            params.setFitInsetsTypes(0);
            params.format = PixelFormat.TRANSLUCENT;
    		... ... 
            final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
    		//创建根布局
            final FrameLayout rootLayout = new FrameLayout(context);
            rootLayout.setPadding(0, 0, 0, 0);
            rootLayout.setFitsSystemWindows(false);
            final Runnable setViewSynchronized = () -> {
                SplashScreenView contentView = viewSupplier.get();
    			//将创建好的SplashScreenView添加到根布局
                rootLayout.addView(contentView);
    
            };
    		... ... 
    		//创建SplashscreenView
            mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId,
                    viewSupplier::setView);
    		... ... 
    		final WindowManager wm = context.getSystemService(WindowManager.class);
    		//添加窗口
    		if (addWindow(taskId, appToken, rootLayout, wm, params, suggestType)) {
    			... ...
    		}
        }
    	protected boolean addWindow(int taskId, IBinder appToken, View view, WindowManager wm,
                WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
    		... ... 
            wm.addView(view, params);
            ... ... 
        }
    }
    
  • SplashscreenContentDrawer:创建SplashscreenView的实现类

    public class SplashscreenContentDrawer {
    	void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info,
                int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) {
    			...
    			//创建SplashScreenView
                SplashScreenView contentView;
                    contentView = makeSplashScreenContentView(context, info, suggestType);
    			...
    			//通知SplashScreenView创建完毕
                splashScreenViewConsumer.accept(contentView);
            });
        }
    	private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai,
                @StartingWindowType int suggestType) {
    		... 
    		//读取配置的窗口属性
            getWindowAttrs(context, mTmpAttrs);
    		...
    		//开始创建SplashScreenView
            return new StartingWindowViewBuilder(context, ai)
                    .setWindowBGColor(themeBGColor)
                    .overlayDrawable(legacyDrawable)
                    .chooseStyle(suggestType)
                    .build();
        }
    	private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
    		//读取在themes.xml中配置的属性
            final TypedArray typedArray = context.obtainStyledAttributes(
                    com.android.internal.R.styleable.Window);
            attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
            attrs.mWindowBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
                    R.styleable.Window_windowSplashScreenBackground, def),
                    Color.TRANSPARENT);
            attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
                    R.styleable.Window_windowSplashScreenAnimatedIcon), null);
            attrs.mAnimationDuration = safeReturnAttrDefault((def) -> typedArray.getInt(
                    R.styleable.Window_windowSplashScreenAnimationDuration, def), 0);
            attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable(
                    R.styleable.Window_windowSplashScreenBrandingImage), null);
            attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
                    R.styleable.Window_windowSplashScreenIconBackgroundColor, def),
                    Color.TRANSPARENT);
            typedArray.recycle();
        }
    	private class StartingWindowViewBuilder {
    		SplashScreenView build() {
                Drawable iconDrawable;
                final int animationDuration;
    			...
    			//设置中间的图标/动

    以上是关于Android 12 新APP启动画面(SplashScreen API)简介&&源码分析的主要内容,如果未能解决你的问题,请参考以下文章

    Android 12上全新的应用启动画面,还不适配一下?

    Android 12 启动画面-SplashScreen

    你的Android App又需要适配了:Android 12,全新的App启动动画

    Android 12 启动画面设置KeepVisibleCondition

    Android 12之启动画面Splash Screens -- 适配

    Android 12 启动画面 API 定制