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);
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容

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