Android换肤原理

qq 网易云音乐的换肤功能很炫酷,这里总结下换肤原理。

换肤分为两种模式,静态换肤 动态换肤。静态换肤就是把不同皮肤的资源打包到apk中,使用时切换,

这种换肤的弊端就不再多说了(种类固定,apk大)。这里介绍下动态换肤,主要步骤如下:

1,注册监听所有Activity的生命周期

2,监听所有view的创建,找到可以换的属性并存储

3,加载皮肤包(其实是一个只包含资源的apk)

4,执行换肤操作

下面是实现技术细节:

1,注册监听所有Activity的生命周期

application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());

注册所有activity的生命周期,跟BaseActivity比的优势是完全非侵入式,我们自己做activity管理也可用此方法。

2,监听所有view的创建,找到可以换的属性并存储

监听到activty的创建以后就可以设置LayoutInflater.Factory2进行监听所有View的创建过程,

public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    LayoutInflater layoutInflater = LayoutInflater.from(activity);
    //获得Activity的布局加载器
    try {
        //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
        //如设置过抛出一次
        //设置 mFactorySet 标签为false
        Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
        field.setAccessible(true);
        field.setBoolean(layoutInflater, false);
    } catch (Exception e) {
        e.printStackTrace();
    }
    SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory();
    LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
    //注册观察者
    SkinManager.getInstance().addObserver(skinLayoutFactory);
    mLayoutFactoryMap.put(activity, skinLayoutFactory);
}

在SkinLayoutFactory中能监听到此Activity 所有View的创建过程,

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    //反射 classloader
    View view = createViewFromTag(name, context, attrs);
    // 自定义View
    if (null == view) {
        view = createView(name, context, attrs);
    }
    //筛选符合属性的View
    skinAttribute.load(view, attrs);

    return view;
}

此时需要 把可以换肤的属性筛选出来

public void load(View view, AttributeSet attrs) {
    List<SkinPair> skinPairs = new ArrayList<>();
    for (int i = 0; i < attrs.getAttributeCount(); i++) {
        //获得属性名
        String attributeName = attrs.getAttributeName(i);
        //是否符合 需要筛选的属性名
        if (mAttributes.contains(attributeName)) {
            String attributeValue = attrs.getAttributeValue(i);
            //写死了 不管了
            if (attributeValue.startsWith("#")) {
                continue;
            }
            //资源id
            int resId;
            if (attributeValue.startsWith("?")) {
                //attr Id
                int attrId = Integer.parseInt(attributeValue.substring(1));
                //获得 主题 style 中的 对应 attr 的资源id值
                resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
            } else {
                // @12343455332
                resId = Integer.parseInt(attributeValue.substring(1));
            }
            if (resId != 0) {
                //可以被替换的属性
                SkinPair skinPair = new SkinPair(attributeName, resId);
                skinPairs.add(skinPair);
            }
        }
    }

    //将View与之对应的可以动态替换的属性集合 放入 集合中
    if (!skinPairs.isEmpty()) {
        SkinView skinView = new SkinView(view, skinPairs);
        skinView.applySkin();
        mSkinViews.add(skinView);
    }
}

3,加载皮肤包(其实是一个只包含资源的apk)

先来了解下资源的加载 原理


如果是assert目录下的资源给AssertManager资源名字即可找到,如果是资源ID需要通过resources.arsc映射表找到对应资源名字再给到AssetManager.

我们要做的就是给资源资源apk创建一个Resources用于查找资源,怎么做呢:

public void loadSkin(String path) {
    //还原默认皮肤包
    if (TextUtils.isEmpty(path)) {
        SkinPreference.getInstance().setSkin("");
        SkinResources.getInstance().reset();
    } else {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            // 添加资源进入资源管理器
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String
                    .class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager, path);

            Resources resources = application.getResources();
            // 横竖、语言
            Resources skinResource = new Resources(assetManager, resources.getDisplayMetrics(),
                    resources.getConfiguration());
            //获取外部Apk(皮肤包) 包名
            PackageManager mPm = application.getPackageManager();
            PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
                    .GET_ACTIVITIES);
            String packageName = info.packageName;
            SkinResources.getInstance().applySkin(skinResource, packageName);
            //保存当前使用的皮肤包
            SkinPreference.getInstance().setSkin(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //应用皮肤包
    setChanged();
    //通知观察者
    notifyObservers();
}

需要通过AssertManager用反射把资源apk路径设置好,然后传给Resources,就可以使用里边的资源了,

/**
 * 根据资源Id获取到在资源apk中的id
 * @param resId
 * @return
 */
public int getIdentifier(int resId) {
    if (isDefaultSkin) {
        return resId;
    }
    //在皮肤包中不一定就是 当前程序的 id
    //获取对应id 在当前的名称 colorPrimary
    //R.drawable.ic_launcher
    String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
    String resType = mAppResources.getResourceTypeName(resId);//drawable
    int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
    return skinId;
}
 
/**
 * 获取资源apk中图片资源
 * @param resId
 * @return
 */
public Drawable getDrawable(int resId) {
    //如果有皮肤  isDefaultSkin false 没有就是true
    if (isDefaultSkin) {
        return mAppResources.getDrawable(resId);
    }
    int skinId = getIdentifier(resId);
    if (skinId == 0) {
        return mAppResources.getDrawable(resId);
    }
    return mSkinResources.getDrawable(skinId);
}

4,执行换肤操作

找到了要换肤的View和属性,加载了资源Apk接下来就是换肤了:、

public void applySkin() {
    for (SkinPair skinPair : skinPairs) {
        Drawable left = null, top = null, right = null, bottom = null;
        switch (skinPair.attributeName) {
            case "background":
                Object background = SkinResources.getInstance().getBackground(skinPair
                        .resId);
                //Color
                if (background instanceof Integer) {
                    view.setBackgroundColor((Integer) background);
                } else {
                    ViewCompat.setBackground(view, (Drawable) background);
                }
                break;
            case "src":
                background = SkinResources.getInstance().getBackground(skinPair
                        .resId);
                if (background instanceof Integer) {
                    ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                            background));
                } else {
                    ((ImageView) view).setImageDrawable((Drawable) background);
                }
                break;
            case "textColor":
                ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                        (skinPair.resId));
                break;
            case "drawableLeft":
                left = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            case "drawableTop":
                top = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            case "drawableRight":
                right = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            case "drawableBottom":
                bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                break;
            default:
                break;
        }
        if (null != left || null != right || null != top || null != bottom) {
            ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                    bottom);
        }
    }
}
给View的每个可以换肤的属性设置数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值