<!--设置启动画面背景色-->
<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
只有当
配置的是动画时才不为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;
}
}
}
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
大体调用过程: