背景
Android 每年都会发布一个release版本,在个别版本上,api会出现较为明显变化,影响功能或者体验,这时候就需要去适配新版本。
Android 12上闪屏动画发生了变化,谷歌定制了官方闪屏动画样式,不允许去随意改动。
相关文档为:
官方闪屏文档、
SplashScreen compat 库
兼容方案
12版本的api,增加了一些新属性,闪屏动画使用了这些属性,且无法应用这些属性之外的属性。
<attr name="windowSplashScreenBackground" format="color"/>
<attr name="windowSplashScreenAnimatedIcon" format="reference"/>
<attr name="windowSplashScreenAnimationDuration" format="integer"/>
<attr name="windowSplashScreenBrandingImage" format="reference"/>
<attr name="windowSplashScreenIconBackgroundColor" format="color"/>
android 12 对应的是sdk 31 ,故适配 需要修改 compileSdkVersion 31 ,才能使用这些属性。
compileSdkVersion31以下
我对于增加values-v31下的style样式和 drawable-v31下的background 分别做了尝试,都无法修改闪屏动画。
既然无法修改,尝试适配到一样的样式。然后从源码找到了这样一段代码。
SplashScreenView build(){
final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
final int scaledIconDpi =
(int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
iconDrawable = mHighResIconProvider.getIcon(
mActivityInfo, densityDpi, scaledIconDpi);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (!processAdaptiveIcon(iconDrawable)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"The icon is not an AdaptiveIconDrawable");
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "legacy_icon_factory");
final ShapeIconFactory factory = new ShapeIconFactory(
SplashscreenContentDrawer.this.mContext,
scaledIconDpi, mFinalIconSize);
final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
createIconDrawable(new BitmapDrawable(bitmap), true,
mHighResIconProvider.mLoadInDetail);
}
return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask);
}
private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
Consumer<Runnable> uiThreadInitTask) {
Drawable foreground = null;
Drawable background = null;
if (iconDrawable != null) {
foreground = iconDrawable.length > 0 ? iconDrawable[0] : null;
background = iconDrawable.length > 1 ? iconDrawable[1] : null;
}
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon");
final ContextThemeWrapper wrapper = createViewContextWrapper(mContext);
final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper)
.setBackgroundColor(mThemeColor)
.setOverlayDrawable(mOverlayDrawable)
.setIconSize(iconSize)
.setIconBackground(background)
.setCenterViewDrawable(foreground)
.setUiThreadInitConsumer(uiThreadInitTask)
.setAllowHandleSolidColor(mAllowHandleSolidColor);
if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
&& mTmpAttrs.mBrandingImage != null) {
builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,
mBrandingImageHeight);
}
final SplashScreenView splashScreenView = builder.build();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
return splashScreenView;
}
我尝试将这一段逻辑抠出来,
public class Compat12Drawable {
private static final float NO_BACKGROUND_SCALE = 192f / 160;
private int mIconSize;
private int mDefaultIconSize;
private int mFinalIconSize;
private Context mContext;
private Handler mSplashscreenWorkerHandler;
Drawable[] drawable_dst;
public Compat12Drawable(Context mContext) {
this.mContext = mContext;
mSplashscreenWorkerHandler = new Handler(Looper.myLooper());
}
public Drawable[] getDrawable_dst() {
return drawable_dst;
}
public int getFinalIconSize() {
return mFinalIconSize;
}
Drawable iconDrawable;
int mThemeColor = 0;
public void build() {
// init param
int id_starting_surface_icon_size = Resources.getSystem().getIdentifier("starting_surface_icon_size", "dimen", "android");
mIconSize = (int) Resources.getSystem().getDimensionPixelSize(id_starting_surface_icon_size);
mFinalIconSize = mIconSize;
mThemeColor = mContext.getResources().getColor(R.color.white);
int starting_surface_default_icon_size = Resources.getSystem().getIdentifier("starting_surface_default_icon_size", "dimen", "android");
mDefaultIconSize = (int) Resources.getSystem().getDimensionPixelSize(starting_surface_default_icon_size);
//get density
final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
final int scaledIconDpi =
(int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
HighResIconProvider mHighResIconProvider = new HighResIconProvider(mContext, new IconProvider(mContext));
ActivityInfo mActivityInfo = new ActivityInfo();
mActivityInfo.applicationInfo = mContext.getApplicationInfo();
// mActivityInfo.icon=R.mipmap.ic_launcher_bj;
iconDrawable = mHighResIconProvider.getIcon(
mActivityInfo, densityDpi, scaledIconDpi);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!(iconDrawable instanceof AdaptiveIconDrawable)) {
System.out.println("AdaptiveIconDrawable ! ");
}else {
System.out.println("AdaptiveIconDrawable is");
}
}
final ShapeIconFactory factory = new ShapeIconFactory(
mContext,
scaledIconDpi, mFinalIconSize);
final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable);
drawable_dst = createIconDrawable(new BitmapDrawable(bitmap), true /* legacy */, false /* loadInDetail */);
System.out.println("drawable_dst:"+drawable_dst.length);
}
private Drawable[] createIconDrawable(Drawable iconDrawable, boolean legacy,
boolean loadInDetail) {
if (legacy){
return SplashscreenIconDrawableFactory.makeLegacyIconDrawable(
iconDrawable, mDefaultIconSize, mFinalIconSize, loadInDetail,
mSplashscreenWorkerHandler);
}else {
return SplashscreenIconDrawableFactory.makeIconDrawable(
mThemeColor, mThemeColor, iconDrawable, mDefaultIconSize,
mFinalIconSize, loadInDetail, mSplashscreenWorkerHandler);
}
}
private class ShapeIconFactory extends BaseIconFactory {
protected ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
super(context, fillResIconDpi, iconBitmapSize, true /* shapeDetection */);
}
}
}
将该逻辑获取的getDrawable_dst()[0]设置为imageview的ImageDrawable,发现在pixel手机上drawable和系统样式相似,但是图标大小会变小一点,在国产厂商的oppo手机上是明显不一样的,在oppo上闪屏图标很大,用代码获取的图标大小却很小。
ImageView imageView = findViewById(R.id.splashscreen_icon_view_my);
final ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.width = compat12Drawable.getFinalIconSize();
params.height = compat12Drawable.getFinalIconSize();
imageView.setLayoutParams(params);
if (compat12Drawable.getDrawable_dst()!=null&&compat12Drawable.getDrawable_dst().length>0){
// imageView.setBackgroundColor(Color.YELLOW);
imageView.setImageDrawable(compat12Drawable.getDrawable_dst()[0]);
}
经过以上试验,不升级compileSdkVersion31的话,兼容31动画和闪屏一致时存在困难的,在排查系统逻辑、分析兼容厂商逻辑、xml文件适配上都是存在问题。
compileSdkVersion31及以上
增加values-v31下的style样式,可以改变闪屏动画,实现步骤:
1.修改编译版本
agp 4.x 及以下版本,修改build.gradle的 compileSdkVersion 31
agp 7.x,修改build.gradle的 compileSdk 31
2.引用core-splashscreen 库
implementation 'androidx.core:core-splashscreen:1.0.0'
3.修改项目的theme
如果要保留31之下的样式,则增加values-v31或者drawable-v31,去适配31版本的样式,
如果不需要保留,则直接替换默认的theme。
<style name="LaunchTheme" parent="Theme.SplashScreen">
<!-- global compat NoActionBar -->
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<!-- 向下兼容。 -->
<item name="windowSplashScreenBackground">@color/white</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/launch_background</item>
<!-- <item name="postSplashScreenTheme">@style/LaunchThemeSrc</item>-->
</style>
postSplashScreenTheme 如果设置的话,会在启动之后自动替换成postSplashScreenTheme中指向的样式,造成应用闪屏切换的效果,体验不太好,建议不要设置。可设置的属性如下:
<attr name="windowSplashScreenBackground" format="color"/> 单色填充背景
<attr name="windowSplashScreenAnimatedIcon" format="reference"/> 窗口中心的启动画面图标
<attr name="windowSplashScreenAnimationDuration" format="integer"/> 启动画面图标动画的时长
<attr name="windowSplashScreenBrandingImage" format="reference"/> 启动画面图标后面的背景
<attr name="windowSplashScreenIconBackgroundColor" format="color"/> 显示在启动画面底部的品牌logo图片
4.在启动页添加代码调用
SplashScreen.installSplashScreen(this);
刚刚说到的postSplashScreenTheme的逻辑就在该方法中实现的。
open fun install() {
val typedValue = TypedValue()
val currentTheme = activity.theme
if (currentTheme.resolveAttribute(
R.attr.windowSplashScreenBackground,
typedValue,
true
)
) {
backgroundResId = typedValue.resourceId
backgroundColor = typedValue.data
}
if (currentTheme.resolveAttribute(
R.attr.windowSplashScreenAnimatedIcon,
typedValue,
true
)
) {
icon = currentTheme.getDrawable(typedValue.resourceId)
}
if (currentTheme.resolveAttribute(R.attr.splashScreenIconSize, typedValue, true)) {
hasBackground =
typedValue.resourceId == R.dimen.splashscreen_icon_size_with_background
}
setPostSplashScreenTheme(currentTheme, typedValue)
}
protected fun setPostSplashScreenTheme(
currentTheme: Resources.Theme,
typedValue: TypedValue
) {
if (currentTheme.resolveAttribute(R.attr.postSplashScreenTheme, typedValue, true)) {
finalThemeId = typedValue.resourceId
if (finalThemeId != 0) {
activity.setTheme(finalThemeId)
}
}
}
另外,还可以设置退出动画,详细资料可查阅官方文档
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener(new android.window.SplashScreen.OnExitAnimationListener() {
@Override
public void onSplashScreenExit(@NonNull SplashScreenView view) {
System.out.println("onSplashScreenExit");
bitmap = view.getDrawingCache();
}
});
}
总结
现在看来,适配12不是必须的,在未来是必然的趋势,
一些特殊场景也是有需求的,比如我们的app,启动之后第一个页面是FlutterActivity,通过provideSplashScreen提供Flutter页面的闪屏支持。这个如果在31之下是没办法和闪屏保持统一的,冷启动会出现闪屏变化的情况。
@Override
public SplashScreen provideSplashScreen() {
Drawable manifestSplashDrawable = getSplashScreenFromManifest();
if (manifestSplashDrawable != null) {
return new DrawableSplashScreen(manifestSplashDrawable, ImageView.ScaleType.FIT_XY, 0);
} else {
return new DrawableSplashScreen(ResourcesCompat.getDrawable(getResources(), R.drawable.flutter_background, null), ImageView.ScaleType.FIT_XY, 0);
}
}