插件化换肤

作者: w达不溜w | 来源:发表于2022-02-28 14:17 被阅读0次

    插件化换肤的优点

    1)换肤无闪烁,立即生效,无需重启APP,用户体验好
    2)扩展和维护方便,入侵性小,低耦合
    3)插件化开发,任何APP可以是你的皮肤包

    思路

    换肤就是在需要时候替换 View的属性(src、background、textColor等),利用Android加载资源的流程,来加载第三方皮肤包。

    1、收集XML数据

    如何去收集布局文件中的信息呢?通过查看setContentView源码分析,利用View的实例化流程,替换LayoutInflater类中的mFactory2变量,mFactory2在Activity启动之前就已经赋值了,LayoutInflater提供了修改mFactory2的入口(setFactory2方法)

    public void setFactory2(Factory2 factory) {
       //调用setFactory2之后,再次调用会抛出异常
       if (mFactorySet) {
         throw new IllegalStateException("A factory has already been set on this LayoutInflater");
       }
       if (factory == null) {
         throw new NullPointerException("Given factory can not be null");
       }
       //第一次调用mFactorySet会被赋值为true,所以设置自定义的Factory2需要修改mFactorySet为false
       mFactorySet = true;
       if (mFactory == null) {
         mFactory = mFactory2 = factory;
       } else {
         mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
       }
     }
    

    自定义SkinLayoutInflaterFactory用来接管系统的View的生产过程

    public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2{
    
        // 当选择新皮肤后需要替换View与之对应的属性
        // 页面属性管理器
        private SkinAttribute skinAttribute;
        // 用于获取窗口的状态框的信息
        private Activity activity;
            //仿系统AppCompatViewInflater具体实例化View
        private SkinAppCompatViewInflater mAppCompatViewInflater;
    
        public SkinLayoutInflaterFactory(Activity activity) {
            this.activity = activity;
            skinAttribute = new SkinAttribute();
        }
    
        @Override
        public View onCreateView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {  
            if (mAppCompatViewInflater == null) {
                //可以处理不同版本实例化View (仿照系统类AppCompatViewInflater)
                mAppCompatViewInflater = new SkinAppCompatViewInflater();
            }
          //所以这里创建 View,从而修改View属性
            View view = mAppCompatViewInflater.createView(parent, name, context, attrs, false,
                    false,
                    true,
                    VectorEnabledTintResources.shouldBeUsed());
            //这就是我们加入的逻辑,收集xml数据
            if (null != view) {
                //记录属性
                skinAttribute.look(view, attrs);
            }
            return view;
        }
    
    
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    }
    

    使用factory2 设置布局加载工厂

    try {
      //上面提到过:Android布局加载器使用mFactorySet标记是否设置过Factory,如设置过抛出一次
      //所以需要通过反射设置mFactorySet为false
      Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
      field.setAccessible(true);
      field.setBoolean(layoutInflater, false);
    } catch (Exception e) {
      e.printStackTrace();
    }
    //使用factory2设置布局加载工厂
    SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory
      (activity);
    LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
    

    对于android Q上无法二次setFactroy的问题,同样的思路,反射LayoutInflaterCompat中的sCheckedField字段设置为false和修改mFactory2的值

    2、记录需要换肤的属性

    上面在收集xml数据的时候已经记录了View对应的属性,通过遍历筛选并记录需要换肤的属性

    //需要换肤的属性集合
    private static final List<String> mAttributes = new ArrayList<>();
    static {
      mAttributes.add("background");
      mAttributes.add("src");
      mAttributes.add("textColor");
      mAttributes.add("drawableLeft");
      //...
    }
    //遍历筛选需要操作的属性信息
    public void look(View view, AttributeSet attrs) {
      //SkinPair记录一个属性(属性名--对应资源id)
      List<SkinPair> mSkinPars = new ArrayList<>();
      for (int i = 0; i < attrs.getAttributeCount(); i++) {
        //获得属性名,如:textColor
        String attributeName = attrs.getAttributeName(i);
        if (mAttributes.contains(attributeName)) {
          String attributeValue = attrs.getAttributeValue(i);
          // 比如color 以#开头表示写死的颜色 不可用于换肤
          if (attributeValue.startsWith("#")) {
            continue;
          }
          int resId;
          // 以 ?开头的表示使用 属性
          if (attributeValue.startsWith("?")) {
            int attrId = Integer.parseInt(attributeValue.substring(1));
            resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
          } else {
            // 正常以 @ 开头
            resId = Integer.parseInt(attributeValue.substring(1));
          }
          SkinPair skinPair = new SkinPair(attributeName, resId);
          mSkinPars.add(skinPair);
        }
      }
      if (!mSkinPars.isEmpty() || view instanceof SkinViewSupport) {
        //SkinView记录一个View对应多个属性(View--List<SkinPars>)
        SkinView skinView = new SkinView(view, mSkinPars);
        // 如果选择过皮肤 ,调用一次applySkin加载皮肤的资源
        skinView.applySkin();
        //mSkinViews记录多个View(List<SkinView>)
        mSkinViews.add(skinView);
      }
    }
    
    3、加载皮肤包资源

    先来分析Resource创建过程:

      ActivityThread#handleBindApplication()
    > final ContextImpl appContext = ContextImpl.createAppContext(this, data.info)
    > context.setResources(packageInfo.getResources())
    > mResources = ResourcesManager.getInstance().getResources(...)
    > return getOrCreateResources(activityToken, key, classLoader)
    > ResourcesImpl resourcesImpl = createResourcesImpl(key)
    > final AssetManager assets = createAssetManager(key)
    

    最终创建了一个AssetManager对象去加载资源,我们可以利用反射执行AssetManager的addAssetPath方法去设置皮肤包的路径,然后创建一个Resource对象去加载皮肤包中的资源

    try {
      //宿主app的 resources;
      Resources appResource = mContext.getResources();
      //反射创建AssetManager 与 Resource
      AssetManager assetManager = AssetManager.class.newInstance();
      //反射获取addAssetPath方法 资源路径设置 目录或压缩包 
      Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
      //反射调用addAssetPath方法
      addAssetPath.invoke(assetManager, skinPath);
    
      //根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resources
      Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
           (), appResource.getConfiguration());
    
      //获取外部Apk(皮肤包) 包名
      PackageManager mPm = mContext.getPackageManager();
      PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
      String packageName = info.packageName;
      //设置皮肤
      SkinResources.getInstance().applySkin(skinResource, packageName);
    } catch (Exception e) {
      e.printStackTrace();
    }
    
    4、修改属性的值(属性的值来自皮肤包)

    AssetManager中提供了获取资源id的核心方法:

    //获取资源id
    @AnyRes int getResourceIdentifier(@NonNull String name, @Nullable String defType,
                @Nullable String defPackage)
    //通过资源id获取资源类型名,如drawable,mipmap
    @Nullable String getResourceTypeName(@AnyRes int resId)
    //通过资源id获取资源名
    @Nullable String getResourceEntryName(@AnyRes int resId)
    

    上面已经创建到了皮肤包的Resource对象,我们可以根据资源名、资源类型和包名获取皮肤包中的资源id,然后可以皮肤包中资源id获取对应属性值

    public int getIdentifier(int resId){
        String resName=mAppResources.getResourceEntryName(resId);
        String resType=mAppResources.getResourceTypeName(resId);
        int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName);
        return skinId;
    }
    //根据主APP的ID,到皮肤APK文件中去找到对应ID的颜色值
    public int getColor(int resId){
      if(isDefaultSkin){
        return mAppResources.getColor(resId);
      }
      int skinId=getIdentifier(resId);
      if(skinId==0){
        return mAppResources.getColor(resId);
      }
      return mSkinResources.getColor(skinId);
    }
    

    然后对View中所有支持的属性进行修改

    public void applySkin() {
      for (SkinPair skinPair : skinPairs) {
        switch (skinPair.attributeName) {
          case "textColor":
            ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                        (skinPair.resId));
            break;
            //...
          default:
            break;
        }
      }
    }
    

    相关文章

      网友评论

        本文标题:插件化换肤

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