以往的啟影片面
- 默認情況下剛啟動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,而是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.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; ... //設定中間的圖示/影片 if (mTmpAttrs.mSplashScreenIcon != null) { // Using the windowSplashScreenAnimatedIcon attribute iconDrawable = mTmpAttrs.mSplashScreenIcon; animationDuration = mTmpAttrs.mAnimationDuration; // There is no background below the icon, so scale the icon up if (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
class Activity{
public final @NonNull SplashScreen getSplashScreen() {
return getOrCreateSplashScreen();
}
private SplashScreen getOrCreateSplashScreen() {
synchronized (this) {
if (mSplashScreen == null) {
mSplashScreen = new SplashScreen.SplashScreenImpl(this);
}
return mSplashScreen;
}
}
}
- 設定退出影片監聽;可以看到真正的實作類是
SplashScreenManagerGlobal;
class SplashScreenImpl implements SplashScreen {
private OnExitAnimationListener mExitAnimationListener;
private final SplashScreenManagerGlobal mGlobal;
public SplashScreenImpl(Context context) {
mGlobal = SplashScreenManagerGlobal.getInstance();
}
@Override
public void setOnExitAnimationListener(//設定監聽
@NonNull SplashScreen.OnExitAnimationListener listener) {
synchronized (mGlobal.mGlobalLock) {
if (listener != null) {
mExitAnimationListener = listener;
mGlobal.addImpl(this);
}
}
}
@Override
public void clearOnExitAnimationListener() {//取消監聽
synchronized (mGlobal.mGlobalLock) {
mExitAnimationListener = null;
mGlobal.removeImpl(this);
}
}
... ...
}
-
SplashScreenManagerGlobal:它也是
SplashScreen的內部類,單例模式,初始化時會向ActivityThread注冊自己,當啟影片面將要退出時回呼它的handOverSplashScreenView方法- 注冊的監聽全部保存在
SplashScreenManagerGlobal的ArrayList串列中
class SplashScreenManagerGlobal { private final Object mGlobalLock = new Object(); private final ArrayList<SplashScreenImpl> mImpls = new ArrayList<>(); private SplashScreenManagerGlobal() { //向ActivityThread注冊自身,用于回呼handOverSplashScreenView方法 ActivityThread.currentActivityThread().registerSplashScreenManager(this); } public static SplashScreenManagerGlobal getInstance() { return sInstance.get(); } private static final Singleton<SplashScreenManagerGlobal> sInstance = new Singleton<SplashScreenManagerGlobal>() { @Override protected SplashScreenManagerGlobal create() { return new SplashScreenManagerGlobal(); } }; private void addImpl(SplashScreenImpl impl) { synchronized (mGlobalLock) { mImpls.add(impl); } } private void removeImpl(SplashScreenImpl impl) { synchronized (mGlobalLock) { mImpls.remove(impl); } } public void handOverSplashScreenView(@NonNull IBinder token, @NonNull SplashScreenView splashScreenView) { ... ... //處理跳過啟影片面邏輯,分發退出監聽 dispatchOnExitAnimation(token, splashScreenView); } private void dispatchOnExitAnimation(IBinder token, SplashScreenView view) { synchronized (mGlobalLock) { final SplashScreenImpl impl = findImpl(token); impl.mExitAnimationListener.onSplashScreenExit(view); } } } - 注冊的監聽全部保存在
-
ActivityThread在哪里回呼
SplashScreenManagerGlobal.handOverSplashScreenView方法?class ActivityThread{ private SplashScreen.SplashScreenManagerGlobal mSplashScreenGlobal; public void registerSplashScreenManager( @NonNull SplashScreen.SplashScreenManagerGlobal manager) { synchronized (this) { mSplashScreenGlobal = manager; } } @Override public 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/310607.html
標籤:其他
