Android 12 新APP启动画面(SplashScreen API)简介源码分析
以往的啟動(dòng)畫面
- 默認(rèn)情況下剛啟動(dòng)APP時(shí)會(huì)顯示一會(huì)白色背景
- 如果把這個(gè)啟動(dòng)背景設(shè)置為null,則一閃而過的白色會(huì)變成黑色
- 如果把啟動(dòng)Activity設(shè)置為背景透明【< item name=“android:windowIsTranslucent”>true</ item>】或者禁用了啟動(dòng)畫面【< item name=“android:windowDisablePreview”>true</ item>】;雖然一閃而過的黑色或者白色沒有了,但是因?yàn)楸尘巴该髁司蜁?huì)看到桌面,導(dǎo)致的結(jié)果就是感覺APP啟動(dòng)慢了
- 通常我們會(huì)在主題里給它設(shè)置一張公司Logo圖片【< item name=“android:windowSplashscreenContent”>@drawable/splash</ item>】,這樣就感覺APP啟動(dòng)快了
全新的APP啟動(dòng)畫面
- 統(tǒng)一的設(shè)計(jì)標(biāo)準(zhǔn),不同APP展現(xiàn)出來的整體樣式是一樣的
- 支持通過配置主題的方式更換中間的Logo/動(dòng)畫、背景色、圖片的背景色、底部公司品牌Logo等
- 支持延長顯示的時(shí)間
- 支持自定義關(guān)閉啟動(dòng)畫面的動(dòng)畫
注意事項(xiàng)
- 【< item name=“android:windowSplashscreenContent”>@drawable/splash< /item>】和【< item name=“android:windowDisablePreview”>true</ item>】在Android 12設(shè)備上都失效(已廢棄),即使targetSdkVersion沒有升級到31也是這樣
- Android 12新啟動(dòng)畫面,targetSdkVersion不需要升級到31,但是compileSdkVersion一定要升級到31才可以,否則編譯時(shí)無法找到主題里這些新增的屬性
- 啟動(dòng)畫面的圖標(biāo)/動(dòng)畫應(yīng)該遵循Adaptive Icon(自適應(yīng)圖標(biāo))的規(guī)范,不然圖片/動(dòng)畫可能會(huì)顯示異常
使用方法
APP在Android12上默認(rèn)啟動(dòng)效果
在主題中通過配置自定義啟動(dòng)畫面
設(shè)置啟動(dòng)畫面背景色
<!--設(shè)置啟動(dòng)畫面背景色--> <item name="android:windowSplashScreenBackground">#ff9900</item>效果圖:
設(shè)置啟動(dòng)畫面居中顯示的圖標(biāo)或者動(dòng)畫
<!--設(shè)置啟動(dòng)畫面居中顯示的圖標(biāo)或者動(dòng)畫--> <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item> <!--設(shè)置啟動(dòng)畫面在關(guān)閉之前顯示的時(shí)長,最長1000毫秒--> <item name="android:windowSplashScreenAnimationDuration">1000</item>- windowSplashScreenAnimationDuration指的是啟動(dòng)畫面顯示的時(shí)間,跟動(dòng)畫的時(shí)長無關(guān),也就是如果動(dòng)畫時(shí)間超過這個(gè)時(shí)間,它不會(huì)等待動(dòng)畫結(jié)束,而是直接關(guān)閉;如果希望動(dòng)畫顯示時(shí)間超過1秒,則需要參考后面【延遲關(guān)閉啟動(dòng)畫面】部分
效果圖:
設(shè)置中間顯示圖標(biāo)區(qū)域的背景色
用于解決圖標(biāo)和背景顏色接近顯示不清問題
<!--設(shè)置中間顯示圖標(biāo)區(qū)域的背景色,用于解決圖標(biāo)和背景顏色接近顯示不清問題--> <item name="android:windowSplashScreenIconBackgroundColor">#ff0000</item>效果圖:
設(shè)置啟動(dòng)畫面底部公司品牌圖片
官方不推薦使用,可能是因?yàn)榈撞吭偌觽€(gè)圖片不好看吧
<!--設(shè)置啟動(dòng)畫面底部公司品牌圖片,官方不推薦使用--> <item name="android:windowSplashScreenBrandingImage">@drawable/ic_launcher_foreground</item>效果圖:
延遲關(guān)閉啟動(dòng)畫面
有時(shí)候希望啟動(dòng)畫面能在數(shù)據(jù)準(zhǔn)備好之后才關(guān)閉,或者動(dòng)畫時(shí)間超過1秒
class MainActivity() : AppCompatActivity() {var isDataReady = falseoverride 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) {//判斷是否可以關(guān)閉啟動(dòng)動(dòng)畫,可以則返回truecontentView.viewTreeObserver.removeOnPreDrawListener(this)}return isDataReady}})Thread.sleep(5000)//模擬耗時(shí)isDataReady = true} }效果圖:
定制退出動(dòng)畫
啟動(dòng)畫面默認(rèn)結(jié)束后是直接消失的,可能會(huì)顯得有些突兀,全新的SplashScreen支持定制退出動(dòng)畫
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 = 2000LslideUp.doOnEnd { splashScreenView.remove() }slideUp.start()} }效果圖:
-
splashScreen是Activity中的getSplashScreen()方法返回的
-
官方說SplashScreenView在動(dòng)畫結(jié)束后要remove掉,實(shí)際測試發(fā)現(xiàn)不remove也是可以的,因?yàn)閯?dòng)畫結(jié)束后啟動(dòng)畫面已經(jīng)被移動(dòng)到看不到的地方了,不影響后續(xù)操作;但是通過查看SplashScreenView的remove方法源碼,除了將SplashScreenView設(shè)為不可見外,還有圖片等資源的回收操作,所以建議還是要調(diào)用它的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;} }
計(jì)算啟動(dòng)畫面中間的動(dòng)畫剩余時(shí)長
上面我們說到可以自定義退出動(dòng)畫,也就是設(shè)置splashScreen.setOnExitAnimationListener,這個(gè)接口會(huì)在將要顯示APP主界面時(shí)回調(diào);
-
如果設(shè)備性能比較差,可能會(huì)出現(xiàn)中間那個(gè)圖標(biāo)動(dòng)畫已經(jīng)結(jié)束,但是APP主界面卻還沒顯示的情況,這個(gè)時(shí)候如果啟動(dòng)畫面退出時(shí)還做一次動(dòng)畫,會(huì)導(dǎo)致APP進(jìn)入主界面的時(shí)間更長,遇到這種情況應(yīng)該取消退出動(dòng)畫,讓用戶及時(shí)看到主界面會(huì)更好一些;
-
如果設(shè)備性能比較好,假如本來設(shè)置的啟動(dòng)畫面中間圖標(biāo)動(dòng)畫時(shí)長1000毫秒,但是只執(zhí)行了500毫秒的動(dòng)畫就可以開始顯示APP主界面動(dòng)畫了,卻因?yàn)楣潭ǖ耐顺鰟?dòng)畫時(shí)長,導(dǎo)致需要等待更久的時(shí)間才能看到主界面
所以應(yīng)該根據(jù)啟動(dòng)畫面中間圖標(biāo)動(dòng)畫時(shí)長執(zhí)行剩余時(shí)間來決定退出動(dòng)畫的時(shí)長,這樣才能盡快讓用戶看到APP主界面,并保證好的體驗(yàn)效果
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()//計(jì)算合適的退出動(dòng)畫時(shí)長var targetDuration = 0Lval animationDuration = splashScreenView.iconAnimationDuration//圖標(biāo)動(dòng)畫時(shí)長val animationStart = splashScreenView.iconAnimationStart//圖標(biāo)動(dòng)畫開始時(shí)間if (animationDuration != null && animationStart != null) {val remainingDuration = (animationDuration.toMillis() - (System.currentTimeMillis() - animationStart.toEpochMilli())).coerceAtLeast(0L)//計(jì)算剩余時(shí)間,如果小于0則賦值0targetDuration = remainingDuration}slideUp.duration = targetDurationslideUp.doOnEnd { splashScreenView.remove() }slideUp.start()} }- 需要注意官網(wǎng)示例代碼中的splashScreenView.getIconAnimationDurationMillis()和splashScreenView.getIconAnimationStartMillis()在實(shí)際測試中,SplashScreenView中并沒有發(fā)現(xiàn)這兩個(gè)方法,取而代之的是splashScreenView.getIconAnimationDuration()和splashScreenView.getIconAnimationStart();而且這兩個(gè)方法返回的對象并不是long,而是Duration和Instant,需要分別再次調(diào)用它們的toMillis()和toEpochMilli()方法轉(zhuǎn)換成毫秒(long)
- 官網(wǎng)示例代碼中的SystemClock.uptimeMillis()在實(shí)際測試中發(fā)現(xiàn)也是不對的,SystemClock.uptimeMillis()返回的是從手機(jī)開機(jī)時(shí)到現(xiàn)在的時(shí)間(毫秒),但是getIconAnimationStart()返回的是卻是當(dāng)時(shí)手機(jī)系統(tǒng)顯示的時(shí)間
- 需要注意的是animationDuration和iconAnimationStart只有當(dāng)<item name="android:windowSplashScreenAnimatedIcon">配置的是動(dòng)畫時(shí)才不為null,如果配置的只是普通圖片,則會(huì)返回null,所以計(jì)算剩余時(shí)長時(shí)需要判斷非空
源碼分析
涉及到的主要類
-
SplashScreenView:啟動(dòng)畫面所顯示的View,繼承自FrameLayout;對應(yīng)系統(tǒng)布局文件是:splash_screen_view.xml
//splash_screen_view.xml <android.window.SplashScreenViewxmlns: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;//界面中間顯示的圖標(biāo)private View mBrandingImageView;//底部品牌圖標(biāo)private Duration mIconAnimationDuration;//啟動(dòng)畫面顯示時(shí)長private Instant mIconAnimationStart;//中間動(dòng)畫開始執(zhí)行的時(shí)間public static class Builder {private Drawable mIconDrawable;//界面中間顯示的圖標(biāo)private Drawable mIconBackground;//界面中間顯示的圖標(biāo)的背景色private Drawable mBrandingDrawable;//底部品牌圖標(biāo)private Instant mIconAnimationStart;//中間動(dòng)畫開始執(zhí)行的時(shí)間private Duration mIconAnimationDuration;//啟動(dòng)畫面顯示時(shí)長public SplashScreenView build() {...final SplashScreenView view = (SplashScreenView)layoutInflater.inflate(R.layout.splash_screen_view, null, false);view.mInitBackgroundColor = mBackgroundColor;view.setBackgroundColor(mBackgroundColor);//設(shè)置背景色ImageView imageView = view.findViewById(R.id.splashscreen_icon_view);imageView.setImageDrawable(mIconDrawable);設(shè)置界面中間圖標(biāo)/動(dòng)畫imageView.setBackground(mIconBackground);//設(shè)置中間顯示的圖標(biāo)的背景色view.mBrandingImageView = view.findViewById(R.id.splashscreen_branding_view);view.mBrandingImageView.setBackground(mBrandingDrawable);//設(shè)置底部品牌圖標(biāo)...return view;}} } -
SplashScreen:用于客戶端與SplashScreenView交互的接口,比如:自定義啟動(dòng)畫面退出時(shí)的動(dòng)畫
-
StartingSurfaceController:Android12新增,用于管理創(chuàng)建/釋放starting window surface;這個(gè)類里面通過系統(tǒng)屬性persist.debug.shell_starting_surface的值來決定是使用全新的SplashScreenView還是舊版的啟動(dòng)畫面
- persist.debug.shell_starting_surface在Android12上默認(rèn)為空,根據(jù)源碼來看,如果為空,則默認(rèn)值為true;也就是說Android12上默認(rèn)是啟用新版啟動(dòng)畫面的,通過adb命令:adb shell setprop persist.debug.shell_starting_surface false并且重啟系統(tǒng)后,可以禁用全新啟動(dòng)畫面,所有APP啟動(dòng)畫面將變回舊版
-
StartingSurfaceDrawer:創(chuàng)建SplashScreenView和啟動(dòng)窗口的主要流程
public class StartingSurfaceDrawer {void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,@StartingWindowType int suggestType) {... ...//創(chuàng)建啟動(dòng)窗口參數(shù)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();//創(chuàng)建根布局final FrameLayout rootLayout = new FrameLayout(context);rootLayout.setPadding(0, 0, 0, 0);rootLayout.setFitsSystemWindows(false);final Runnable setViewSynchronized = () -> {SplashScreenView contentView = viewSupplier.get();//將創(chuàng)建好的SplashScreenView添加到根布局rootLayout.addView(contentView);};... ... //創(chuàng)建SplashscreenViewmSplashscreenContentDrawer.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:創(chuàng)建SplashscreenView的實(shí)現(xiàn)類
public class SplashscreenContentDrawer {void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info,int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) {...//創(chuàng)建SplashScreenViewSplashScreenView contentView;contentView = makeSplashScreenContentView(context, info, suggestType);...//通知SplashScreenView創(chuàng)建完畢splashScreenViewConsumer.accept(contentView);});}private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai,@StartingWindowType int suggestType) {... //讀取配置的窗口屬性getWindowAttrs(context, mTmpAttrs);...//開始創(chuàng)建SplashScreenViewreturn 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;...//設(shè)置中間的圖標(biāo)/動(dòng)畫if (mTmpAttrs.mSplashScreenIcon != null) {// Using the windowSplashScreenAnimatedIcon attributeiconDrawable = mTmpAttrs.mSplashScreenIcon;animationDuration = mTmpAttrs.mAnimationDuration;// There is no background below the icon, so scale the icon upif (mTmpAttrs.mIconBgColor == Color.TRANSPARENT|| mTmpAttrs.mIconBgColor == mThemeColor) {mFinalIconSize *= NO_BACKGROUND_SCALE;}createIconDrawable(iconDrawable, false);} ...return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration);}private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,int animationDuration) {final SplashScreenView.Builder builder = new SplashScreenView.Builder(mContext).setBackgroundColor(mThemeColor).setOverlayDrawable(mOverlayDrawable).setIconSize(iconSize).setIconBackground(background).setCenterViewDrawable(foreground).setAnimationDurationMillis(animationDuration);//設(shè)置底部的品牌圖標(biāo)if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN&& mTmpAttrs.mBrandingImage != null) {builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,mBrandingImageHeight);}return splashScreenView;}} }
大體類方法調(diào)用過程
- ActivityRecord.showStartingWindow -> addStartingWindow -> scheduleAddStartingWindow ->
- AddStartingWindow.run
- SplashScreenStartingData.createStartingSurface ->
- StartingSurfaceController.createSplashScreenStartingSurface ->
- TaskOrganizerController.addStartingWindow
- TaskOrganizerController.TaskOrganizerState.addStartingWindow
- TaskOrganizerController.TaskOrganizerCallbacks.addStartingWindow
- TaskOrganizer.addStartingWindow
- StartingWindowController.addStartingWindow
- StartingSurfaceDrawer.addSplashScreenStartingWindow
- SplashscreenContentDrawer.createContentView -> makeSplashScreenContentView ->
- getWindowAttrs -> StartingWindowViewBuilder.build -> fillViewWithIcon
- SplashScreenView.Builder
- StartingSurfaceDrawer.addWindow
自定義退出動(dòng)畫源碼分析
- 通過Activity獲取用于與SplashscreenView交互的SplashScreen接口;可以看出SplashScreen接口的實(shí)現(xiàn)類是SplashScreen的內(nèi)部類SplashScreenImpl
- 設(shè)置退出動(dòng)畫監(jiān)聽;可以看到真正的實(shí)現(xiàn)類是SplashScreenManagerGlobal;
-
SplashScreenManagerGlobal:它也是SplashScreen的內(nèi)部類,單例模式,初始化時(shí)會(huì)向ActivityThread注冊自己,當(dāng)啟動(dòng)畫面將要退出時(shí)回調(diào)它的handOverSplashScreenView方法
- 注冊的監(jiān)聽全部保存在SplashScreenManagerGlobal的ArrayList列表中
-
ActivityThread在哪里回調(diào)SplashScreenManagerGlobal.handOverSplashScreenView方法?
class ActivityThread{private SplashScreen.SplashScreenManagerGlobal mSplashScreenGlobal;public void registerSplashScreenManager(@NonNull SplashScreen.SplashScreenManagerGlobal manager) {synchronized (this) {mSplashScreenGlobal = manager;}}@Overridepublic void handOverSplashScreenView(@NonNull ActivityClientRecord r) {final SplashScreenView v = r.activity.getSplashScreenView();if (v == null) {return;}synchronized (this) {if (mSplashScreenGlobal != null) {mSplashScreenGlobal.handOverSplashScreenView(r.token, v);}}} } -
ActivityThread.handOverSplashScreenView大體調(diào)用過程:
- ActivityClientController.splashScreenAttached ->
- ActivityRecord.splashScreenAttachedLocked -> onSplashScreenAttachComplete
- ClientLifecycleManager.scheduleTransaction ->
- TransferSplashScreenViewStateItem.execute(mRequest==HANDOVER_TO)
- ActivityThread.handOverSplashScreenView ->
- SplashScreenGlobal.handOverSplashScreenView -> dispatchOnExitAnimation ->
- ExitAnimationListener.onSplashScreenExit
總結(jié)
以上是生活随笔為你收集整理的Android 12 新APP启动画面(SplashScreen API)简介源码分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu apt-get insta
- 下一篇: 让你的Excel更精彩 让你的工作更轻松