Android插件化换肤

目标

1、启动时能够自动加载皮肤包
2、能动态进行皮肤切换
3、能支持在线下载皮肤

思路

利用Android App加载资源的流程,来加载第三方皮肤包。

皮肤包加载流程

1、C++层读取资源文件(类似于一个数据库表,有属性名、id和对应资源/路径的对应关系)

    private static native long nativeLoad(@FormatType int format, @NonNull String path,
            @PropertyFlags int flags, @Nullable AssetsProvider asset) throws IOException;

2、通过AssetManager.addAssetPath加载皮肤包里面的资源文件

	// 这个方法只能通过反射才能调到
    @Deprecated
    @UnsupportedAppUsage
    public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }  
    // API 30 以上可以直接setApkAssets(ApkAssets[], boolean),该类里面重载了许多工厂方法,可以直接构建对应的Assets
    // 之后再通过AssetManager.setApkAssets即可完成设置
    public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
        return loadFromPath(path, 0 /* flags */);
    }

3、构造Resource对象

    @Deprecated
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

4、通过自己的Inflater,在Inflater是记录需要换肤的属性

5、执行换肤(setBackground,setBackgroundColor等)

View Inflate流程

1、在Activity中SetContentView,或者在Fragment中使用传过来的Inflater进行inflater。
2、不论使用那种方式,最终会都会以 LayoutInflater.from(mContext).inflate(resId, contentParent);这种方式调用
3、进一步走到 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
4、进一步走到:我们看到这里有两个factory,factory2默认是为空的,mPrivateFactory是安卓提供的。

    @UnsupportedAppUsage(trackingBug = 122360734)
    @Nullable
    public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        return view;
    }

5、我们可以使用一个FactoryImpl继承上述Factory,然后重写onCreateView(),注入到Android中的Inflater中,但在setFactory的时候,我们发现有个标记在第一次使用后被设置为true了,导致我们无法在之后自己setFactory,这里需要通过反射将这个标记设置为true。

    public void setFactory(Factory factory) {
        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;
        if (mFactory == null) {
            mFactory = factory;
        } else {
            mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
        }
    }

    /**
     * Like {@link #setFactory}, but allows you to set a {@link Factory2}
     * interface.
     */
    public void setFactory2(Factory2 factory) {
        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;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

6、通过反射将mFactorySet设置为false即可调用,LayoutInflaterCompat.setFactory/setFactory2了。

        LayoutInflater layoutInflater = activity.getLayoutInflater();
        try {
            @SuppressLint("SoonBlockedPrivateApi")
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
        } catch (Exception e) {
            e.printStackTrace();
        }

        SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity);
        LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
        mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);
        mObservable.addObserver(skinLayoutInflaterFactory);

然后我们只需要在SkinLayoutInflaterFactory.onCreateView的时候,通过AttributeSet遍历到所有需要换肤的属性,然后记录到List,之后在对应的位置applySkin()就可以了。
我们记录属性的数据结构如下:

skinAttribute         含List<SkinView>
SkinViewVIewList<SkinPair>
SkinPair				含attrName和resId,例如android:background和@drawable/abc_vector_test

7、根据attrName,attrValue来决定要不要设置换肤,attrName需要mAttributes ,同时attrValue.charAt(0)不应该是"#",#类型的id不能被直接换肤,因为是直接写死的。之后将attrName和attrValue封装成SkinPair即可。

    private static final List<String> mAttributes = new ArrayList<>();

    static {
        mAttributes.add("background");
        mAttributes.add("src");
        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }

8、有了attrName和attrValue即可获取对应的颜色或者drawable等资源。

    /**
     * 1. 通过原始app中的resId(@color/xxx)获取到名字,类型
     * 2. 根据名字类型在资源包中获取到对应的resId
     * R.color.Red(100001) -> Color/Red -> R.color.Red(20013)
     */
    public int getIdentifier(int resId) {
        if (isDefaultSkin) {
            return resId;
        }
        String resName = mAppResources.getResourceEntryName(resId);
        String resType = mAppResources.getResourceTypeName(resId);
        return mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
    }

    /**
     * 根据Id获取到资源包里面对应的color
     */
    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);
    }

9、在点击按钮的时候applySkin,遍历对应的SkinView然后set对应的资源接口,在onCreateView的时候也需要applySkin一次。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值