android 皮肤换肤

很多app都会设置夜间和白天的模式,而实现换肤的方法有很多种,有的必须重新进入才能有效果,有的是动态的,设置了就马上就可以显示。
首先看看通过设置主题的方式来实现换肤
通过设置setTheme(R.style.BlackTheme);来改变字体颜色,背景灯
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">

<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>

<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>

<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>

</style>

可以设置多个主题,然后在进入activity改变主题,但是这中方法,不能时时有效,必须重新进入才能。
所以实际项目中运用更多的是动态换肤。
换皮肤我们需要解决的问题是找到view对象,然后和获取到需要替换的资源文件
第一步获取需要换肤的view,
一种是在Activity的oncreat方法setContentView方法后获取view,但是此时view
已经加载了,我们再去获取修改,就要重新设置一次,而且每个activity都要写大量代码,体验和性能都不好。
我们都知道activity加载布局文件是在
setContentView里添加的。无论是继承 AppCompatActivity 或者Activity最后
都是会执行到LayoutInflater.from(mContext).inflate(resId, contentParent);

然后这里面就是循环解析xml文件最后会执行到

public final View tryCreatView(View parent,String name,Context context,AttributeSet attrs){
if(mFactoty2!=null){
view =mFactoty2.onCreatView(parent,name,context,attrs)
}else if(mFactory != null){
view =mFactoty.onCreatView(name,context,attrs)
}
if(view ==null && mPrivateFacory){
view =mPrivateFacory.onCreatView(parent,name,context,attrs)
}
}

可以看到最终会执行到这个地方,这里面默认mFactoty2 和mFactory 都是空
最后执行到mPrivateFacory 这个地方,但是在这里没有看到这个new出来,但是我们看activity的启动时候在ActivityThread里面的的activity.attach
方法mwindow.getLayoutInflater.setprivateFactory(this),设置了,所以如果我们想
在可以在getLayoutInflater ,设置factory2活着factory去在我们自定义的LayoutInflater.factory 里面去获取view去设置对应的资源属性。

然后重写这个方法。

public View onCreateView(@Nullable View view, @NonNull String name, @NonNull Context context, @NonNull AttributeSet set) {
        Log.d("jun","------>"+name);


        View realView= null;

        if (name.contains(".")) {//表示不是常用的TextView这些控件,不包括自定义,
            // 第三方。v7 ,v4,androidx等库的控件,这些需要单独去适配这里只是说原理,其他的都是差不多的
            realView = createView(name, context, set);
        } else {//系统控件
            for (int i = 0; i < sClassPrefixList.length; i++) {
                realView = createView(sClassPrefixList[i] + name, context, set);
                if (realView != null) {
                    break;
                }
            }
        }
        List<SkinAttr> skinAttrsList = new ArrayList<>();
        for (int i=0;i<set.getAttributeCount();i++){
            String attributeName = set.getAttributeName(i);//属性的名字background
            String attributeValue = set.getAttributeValue(i);//属性的值

            //在这里收集的属性主要是皮肤换肤需要的一些属性,例如background,textColor,src等
            if(isSupportSkinAttr(attributeName)){
                //资源的id,实际就是R文件的id
                int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
                String resName = context.getResources().getResourceName(resId);//这个是完整的路径
                String res = context.getResources().getResourceEntryName(resId);//资源的名字
                String attrType = context.getResources().getResourceTypeName(resId);
                Log.i("jun","res"+res+"name:"+resName+"----attrType"+attrType+"---rId");
                SkinAttr attr = new SkinAttr(attributeName,attrType,res,resId);
                skinAttrsList.add(attr);
            }
        }
        SkinView skinView = new SkinView(view,skinAttrsList);
        skinViews.add(skinView);
        skinView.skinApply();
        return realView;
    }

这样我们获取了所有的view对象和它的属性
然后需要解决的就是如何获取到我们的替换资源。
我们都知道资源的获取是通过context.getResource获取的。

而context是在什么时候创建的了,我们在看activity启动流程时候
在ActivityThread的performlaunchActivity方法中

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }

        ComponentName component = r.intent.getComponent();
        if (component == null) {
            component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
            r.intent.setComponent(component);
        }

        if (r.activityInfo.targetActivity != null) {
            component = new ComponentName(r.activityInfo.packageName,
                    r.activityInfo.targetActivity);
        }

        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
      //下面省略

}

createBaseContextForActivity 会创建basecontext ,可以看出实际 的创建是在
ContextImpl 这里完成的 走到了 createActivityContext 这个方法

 static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
            Configuration overrideConfiguration) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");

        String[] splitDirs = packageInfo.getSplitResDirs();
        ClassLoader classLoader = packageInfo.getClassLoader();
  //省略
   context.setResources(resourcesManager.createBaseTokenResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getOverlayPaths(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader,
                packageInfo.getApplication() == null ? null
                        : packageInfo.getApplication().getResources().getLoaders()));


}

这里设置资源可以得知resourcesManager.createBaseTokenResources( 这里面创建的

最后走到resourcesManager 的

    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
            @Nullable ApkAssetsSupplier apkSupplier) {
        final AssetManager assets = createAssetManager(key, apkSupplier);
        if (assets == null) {
            return null;
        }

        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final Configuration config = generateConfig(key);
        final DisplayMetrics displayMetrics = getDisplayMetrics(generateDisplayId(key), daj);
        final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj);

        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }

我们看到这个点地方会创建AssetManager ,不过33版本的创建Assertmanger 方式改变了,

 private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
            @Nullable ApkAssetsSupplier apkSupplier) {
        final AssetManager.Builder builder = new AssetManager.Builder();

        final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
        for (int i = 0, n = apkKeys.size(); i < n; i++) {
            final ApkKey apkKey = apkKeys.get(i);
            try {
                builder.addApkAssets(
                        (apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey));
            } catch (IOException e) {
                if (apkKey.overlay) {
                    Log.w(TAG, String.format("failed to add overlay path '%s'", apkKey.path), e);
                } else if (apkKey.sharedLib) {
                    Log.w(TAG, String.format(
                            "asset path '%s' does not exist or contains no resources",
                            apkKey.path), e);
                } else {
                    Log.e(TAG, String.format("failed to add asset path '%s'", apkKey.path), e);
                    return null;
                }
            }
        }

        if (key.mLoaders != null) {
            for (final ResourcesLoader loader : key.mLoaders) {
                builder.addLoader(loader);
            }
        }

        return builder.build();
    }

之前的是调用AssetManager的这个方法去把资源路径传递
public int addAssetPath(String path) {
}
把path传到apkkey里面然后添加到这个方法
新版的是 builder.addApkAssets( apkkey)
不过原来的addAssetPath被标为过时的,还可以用。
我们这个地方还是可以按照

首先获取 资源文件

  public boolean loadSkin(String skinPath) {
        //------------拿到skinPackageName----------
        boolean isSuccess =false;
        PackageInfo packageArchiveInfo = mContext.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
        if (packageArchiveInfo == null) {
        } else {
            //----------拿到skin中的Resource对象----------
            AssetManager assets = null;
            skinPackageName = packageArchiveInfo.packageName;
            try {
                assets = AssetManager.class.newInstance();
                Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
                addAssetPath.invoke(assets, skinPath);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            mChooseResources = new Resources(assets, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
            isSuccess =true;

        }
        return isSuccess;
    }

然后将这个mChooseResources 保存,换肤获取资源就通过这个去获取。

结合上面获取到的view,就可以直接进行换肤了。
demo 在这里 代码.

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

推荐阅读更多精彩内容