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 = 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) {//判斷是否可以關閉啟動動畫,可以則返回truecontentView.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 = 2000LslideUp.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 = 0Lval animationDuration = splashScreenView.iconAnimationDuration//圖標動畫時長val animationStart = splashScreenView.iconAnimationStart//圖標動畫開始時間if (animationDuration != null && animationStart != null) {val remainingDuration = (animationDuration.toMillis() - (System.currentTimeMillis() - animationStart.toEpochMilli())).coerceAtLeast(0L)//計算剩余時間,如果小于0則賦值0targetDuration = remainingDuration}slideUp.duration = targetDurationslideUp.doOnEnd { splashScreenView.remove() }slideUp.start()} }- 需要注意官網示例代碼中的splashScreenView.getIconAnimationDurationMillis()和splashScreenView.getIconAnimationStartMillis()在實際測試中,SplashScreenView中并沒有發現這兩個方法,取而代之的是splashScreenView.getIconAnimationDuration()和splashScreenView.getIconAnimationStart();而且這兩個方法返回的對象并不是long,而是Duration和Instant,需要分別再次調用它們的toMillis()和toEpochMilli()方法轉換成毫秒(long)
- 官網示例代碼中的SystemClock.uptimeMillis()在實際測試中發現也是不對的,SystemClock.uptimeMillis()返回的是從手機開機時到現在的時間(毫秒),但是getIconAnimationStart()返回的是卻是當時手機系統顯示的時間
- 需要注意的是animationDuration和iconAnimationStart只有當<item name="android:windowSplashScreenAnimatedIcon">配置的是動畫時才不為null,如果配置的只是普通圖片,則會返回null,所以計算剩余時長時需要判斷非空
源碼分析
涉及到的主要類
-
SplashScreenView:啟動畫面所顯示的View,繼承自FrameLayout;對應系統布局文件是: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;//界面中間顯示的圖標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啟動畫面將變回舊版
-
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);};... ... //創建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:創建SplashscreenView的實現類
public class SplashscreenContentDrawer {void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info,int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) {...//創建SplashScreenViewSplashScreenView contentView;contentView = makeSplashScreenContentView(context, info, suggestType);...//通知SplashScreenView創建完畢splashScreenViewConsumer.accept(contentView);});}private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai,@StartingWindowType int suggestType) {... //讀取配置的窗口屬性getWindowAttrs(context, mTmpAttrs);...//開始創建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;...//設置中間的圖標/動畫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);//設置底部的品牌圖標if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN&& mTmpAttrs.mBrandingImage != null) {builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,mBrandingImageHeight);}return splashScreenView;}} }
大體類方法調用過程
- 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
自定義退出動畫源碼分析
- 通過Activity獲取用于與SplashscreenView交互的SplashScreen接口;可以看出SplashScreen接口的實現類是SplashScreen的內部類SplashScreenImpl
- 設置退出動畫監聽;可以看到真正的實現類是SplashScreenManagerGlobal;
-
SplashScreenManagerGlobal:它也是SplashScreen的內部類,單例模式,初始化時會向ActivityThread注冊自己,當啟動畫面將要退出時回調它的handOverSplashScreenView方法
- 注冊的監聽全部保存在SplashScreenManagerGlobal的ArrayList列表中
-
ActivityThread在哪里回調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大體調用過程:
- ActivityClientController.splashScreenAttached ->
- ActivityRecord.splashScreenAttachedLocked -> onSplashScreenAttachComplete
- ClientLifecycleManager.scheduleTransaction ->
- TransferSplashScreenViewStateItem.execute(mRequest==HANDOVER_TO)
- ActivityThread.handOverSplashScreenView ->
- SplashScreenGlobal.handOverSplashScreenView -> dispatchOnExitAnimation ->
- ExitAnimationListener.onSplashScreenExit
總結
以上是生活随笔為你收集整理的Android 12 新APP启动画面(SplashScreen API)简介源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu apt-get insta
- 下一篇: Android上实现一个简单的天气预报A