Android插件式换肤实现

问题分析

换皮功能有两种,一种是白天黑夜模式,一种是多套皮肤的替换,也就是以插件形式实现资源的获取,白天黑夜的模式是通过更换一个主题(Theme),这里不做扩展,而多套皮肤有两种实现方式,一种是本地存放多套不同样式的图片切换(不太实际),一种则是通过获取一个apk中的资源去做到,本文讨论的就是这种方式,实现之前有以下几个问题解决
1.系统怎么实现图片替换
2.怎么获取apk中资源
3.获取之后怎么设置到app中实现换肤

解决问题

系统实现图片替换

ImageView的src设置,怎么实现?

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

从ImageView的构造函数进来,看到以下源码

        initImageView();

        // ImageView is not important by default, unless app developer overrode attribute.
        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
        }

        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
        saveAttributeDataForStyleable(context, R.styleable.ImageView,
                attrs, a, defStyleAttr, defStyleRes);
        //这个地方重点,点进去看方法实现
        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
            setImageDrawable(d);
        }

从getDrawable进去,可以清楚看到 mResources.loadDrawable的方法

    public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }

        final TypedValue value = mValue;
        if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
            if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                throw new UnsupportedOperationException(
                        "Failed to resolve attribute at index " + index + ": " + value);
            }

            if (density > 0) {
                // If the density is overridden, the value in the TypedArray will not reflect this.
                // Do a separate lookup of the resourceId with the density override.
                mResources.getValueForDensity(value.resourceId, density, value, true);
            }
            //资源获取,重点
            return mResources.loadDrawable(value, value.resourceId, density, mTheme);
        }
        return null;
    }

既然资源加载是通过Resources调用loadDrawable去读取,那我们能否自己实例化一个去实现?

获取apk中的资源

由源码可知,Resource创建有两种
1.无参

/**
     * Only for creating the System resources.
     */
    @UnsupportedAppUsage
    private Resources() {
        this(null);

        final DisplayMetrics metrics = new DisplayMetrics();
        metrics.setToDefaults();

        final Configuration config = new Configuration();
        config.setToDefaults();

        mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                new DisplayAdjustments());
    }

2.带参

/**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @deprecated Resources should not be constructed by apps.
     * See {@link android.content.Context#createConfigurationContext(Configuration)}.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    @Deprecated
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

而我们需要实现带参的Resource,metrics以及config用系统的即可,再看看AssetManager 源码怎么实例化


        public AssetManager build() {
            // Retrieving the system ApkAssets forces their creation as well.
            final ApkAssets[] systemApkAssets = getSystem().getApkAssets();

            final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
            final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];

            System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);

            final int userApkAssetCount = mUserApkAssets.size();
            for (int i = 0; i < userApkAssetCount; i++) {
                apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
            }

            // Calling this constructor prevents creation of system ApkAssets, which we took care
            // of in this Builder.
            final AssetManager assetManager = new AssetManager(false /*sentinel*/);
            //重点,设置资源路劲,也就是apk地址
            assetManager.mApkAssets = apkAssets;
            AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                    false /*invalidateCaches*/);
            return assetManager;
        }

因此,我们完全可以自己构造一个Resource

app中实现换肤

既然参数可以自己实例化,那换肤可以通过以下方法实现

	  int drawableId = resource.getIdentifier("图片名","drawable","包名");
      Drawable drawable = resource.getDrawable(drawableId);
      mImageIv.setImageDrawable(drawable);

总结

所有的资源通过Resource类加载,构建的时候直接new,需要传入参数为AssetManager,最终通过AssetManager实例化

        try {
            // 读取本地的一个 .skin里面的资源
            Resources superRes = getResources();
            // 创建AssetManager
            AssetManager asset = AssetManager.class.newInstance();

            // 添加本地下载好的资源皮肤,反射实现
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            // method.setAccessible(true); 如果是私有的
            // 反射执行方法
            method.invoke(asset, Environment.getExternalStorageDirectory().getAbsolutePath()+
                    File.separator + "red.skin");

            Resources resource = new Resources(asset,superRes.getDisplayMetrics(),
                    superRes.getConfiguration());

            // 获取资源 id
            int drawableId = resource.getIdentifier("图片名","drawable","包名");

            Drawable drawable = resource.getDrawable(drawableId);

            mImageIv.setImageDrawable(drawable);
        } catch (Exception e) {
            e.printStackTrace();
        }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值