美文网首页
android 皮肤换肤

android 皮肤换肤

作者: BigBigArvin | 来源:发表于2022-11-30 16:40 被阅读0次

    很多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 在这里 代码.

    相关文章

      网友评论

          本文标题:android 皮肤换肤

          本文链接:https://www.haomeiwen.com/subject/nxxrfdtx.html