Android 12闪屏适配

背景

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);
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 遇到的坑 按官方文档设置完之后,debug运行,或者直接点击Run运行,闪屏页的logo不显示,清掉后台,从桌面点...
    我爱田Hebe阅读 3,545评论 3 8
  • 当我在做 Android 版本适配工作的时候很痛苦,那个时候我在想有没有一个文档,将所有的关于 Android 版...
    i小灰阅读 52,689评论 1 58
  • 大家好,我叫八两,来自37手游安卓团队。前不久,9月21号,谷歌更新了 Android 12 Beta5 版本的说...
    八两技术阅读 5,056评论 2 10
  • Android 12 需要更新适配点并不多,本篇主要介绍最常见的两个需要适配的点:android:exported...
    我爱田Hebe阅读 1,221评论 0 8
  • Android 12 需要更新适配点并不多,本篇主要介绍最常见的两个需要适配的点:android:exported...
    恋猫月亮阅读 907评论 0 3