Android 动态换肤框架原理及DEMO

先看效果图,再讲原理,最后是DEMO地址,我也是整理了别人的资料最终成文

20180327112158776.gif

前言

动态换肤的思路是需要先了解系统资源是如何加截的,然后拦截并替换 即可实现动态换肤

思路

从setContentView进入


image.png

点进setContentView看源码


image.png
image.png

找到createViewFromTag


image.png

操作几乎都在这里


image.png

进入tryCreateView()看看


image.png

那么mFactory2在哪里初始化了?
让我们进入oncreate


image.png
image.png
image.png

那么如何拦截系统的创建流程?

直接使用系统的setFactory2方法


image.png

这个方法必须在super之前调用,因为setFactory2只能执行一次


image.png

如果原来界面上只有一个Textview,经过我下面操作会变成一个Button


image.png

拦截后怎么做

因为这不能每一个activity里面都写一段,写在baseActivity里也比较low。况且如果把功能抽出来让别人使用也不方便。

答案:使用lifecycle实现Aop切面编程,来重写系统的创建过程的代码(复制)


image.png

然后只要activity进入super.onCreate方法就会执行我们的onActivityCreated()。接下来看下onActivityCreated里的代码

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    /**
     *  更新状态栏
     */
    SkinThemeUtils.updateStatusBarColor(activity);

    /**
     *  更新布局视图
     */
    //获得Activity的布局加载器
    LayoutInflater layoutInflater = activity.getLayoutInflater();

    try {
        //因为需在super之前调用,但现在在之后了,需要反射修改一下属性
        //设置 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);
    mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);

    mObserable.addObserver(skinLayoutInflaterFactory);
}

然后进入SkinLayoutInflaterFactory。这下面的onCreateView方法就是系统tryCreateView()里mFactory2.onCreateview的onCreateview

public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {

    private static final String[] mClassPrefixList = {
            "android.widget.",
            "android.webkit.",
            "android.app.",
            "android.view."
    };

    //记录对应VIEW的构造函数
    private static final Class<?>[] mConstructorSignature = new Class[]{
            Context.class, AttributeSet.class};

    private static final HashMap<String, Constructor<? extends View>> mConstructorMap =
            new HashMap<String, Constructor<? extends View>>();

    // 当选择新皮肤后需要替换View与之对应的属性
    // 页面属性管理器
    private SkinAttribute skinAttribute;
    // 用于获取窗口的状态框的信息
    private Activity activity;

    public SkinLayoutInflaterFactory(Activity activity) {
        this.activity = activity;
        skinAttribute = new SkinAttribute();
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        //换肤就是在需要时候替换 View的属性(src、background等)
        //所以这里创建 View,从而修改View属性
        View view = createSDKView(name, context, attrs);
        if (null == view) {
            view = createView(name, context, attrs);
        }
        //这就是我们加入的逻辑
        if (null != view) {
            //加载属性
            skinAttribute.look(view, attrs);
        }
        return view;
    }


    private View createSDKView(String name, Context context, AttributeSet
            attrs) {
        //如果包含 . 则不是SDK中的view 可能是自定义view包括support库中的View
        if (-1 != name.indexOf('.')) {
            return null;
        }
        //不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
        for (int i = 0; i < mClassPrefixList.length; i++) {
            View view = createView(mClassPrefixList[i] + name, context, attrs);
            if (view != null) {
                return view;
            }
        }
        return null;
    }

    private View createView(String name, Context context, AttributeSet
            attrs) {
        Constructor<? extends View> constructor = findConstructor(context, name);
        try {
            return constructor.newInstance(context, attrs);
        } catch (Exception e) {
        }
        return null;
    }


    private Constructor<? extends View> findConstructor(Context context, String name) {
        Constructor<? extends View> constructor = mConstructorMap.get(name);
        if (constructor == null) {
            try {
                Class<? extends View> clazz = context.getClassLoader().loadClass
                        (name).asSubclass(View.class);
                constructor = clazz.getConstructor(mConstructorSignature);
                mConstructorMap.put(name, constructor);
            } catch (Exception e) {
            }
        }
        return constructor;
    }


    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    //如果有人发送通知,这里就会执行
    @Override
    public void update(Observable o, Object arg) {
        SkinThemeUtils.updateStatusBarColor(activity);
        skinAttribute.applySkin();
    }
}


收集view以及属性

进入skinAttribute.look(view, attrs)来进行一个属性的收集

//记录下一个VIEW身上哪几个属性需要换肤textColor/src
public void look(View view, AttributeSet attrs) {
    List<SkinPair> mSkinPars = new ArrayList<>();

    for (int i = 0; i < attrs.getAttributeCount(); i++) {
        //获得属性名  如 textColor background
        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 skinView = new SkinView(view, mSkinPars);
        // 如果选择过皮肤 ,调用 一次 applySkin 加载皮肤的资源
        skinView.applySkin();
        mSkinViews.add(skinView);
    }
}

创建皮肤包

皮肤包其实就是apk。

里面只放了一些资源

image.png

如何使用皮肤包(插件化)

系统的资源如何加载

一般这样来拿资源(Resources)

  getResources().getDrawable(R.drawable.t_window_bg)

还有AsserManager(加载最后走的都是AsserManager)


image.png

使用自己创建的AsserManager来加载资源

/**
 * 记载皮肤并应用
 *
 * @param skinPath 皮肤路径 如果为空则使用默认皮肤
 */
public void loadSkin(String skinPath) {
    if (TextUtils.isEmpty(skinPath)) {
        //还原默认皮肤
        SkinPreference.getInstance().reset();
        SkinResources.getInstance().reset();
    } else {
        try {
            //反射创建AssetManager 与 Resource
            AssetManager assetManager = AssetManager.class.newInstance();
            //资源路径设置 目录或压缩包
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
                    String.class);
            addAssetPath.invoke(assetManager, skinPath);

            //宿主app的 resources;
            Resources appResource = mContext.getResources();
            //根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建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);

            //记录路径
            SkinPreference.getInstance().setSkin(skinPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //通知采集的View 更新皮肤
    //被观察者改变 通知所有观察者
    setChanged();
    notifyObservers(null);
}

这里为什么使用自己创建的AsserManager?

因为防止资源冲突()⬇


image.png

当点击换肤按钮后,通过上方代码,然后通知观察者执行下方代码

/**
 * 对一个View中的所有的属性进行修改
 */
public void applySkin() {
    applySkinSupport();
    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 也可能是 @drawable
                if (background instanceof Integer) {
                    view.setBackgroundColor((int) 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);
        }
    }
}

//通过下方代码来获取资源ID来进行上方代码的设置资源ID
//思路:首先找到app的资源ID,然后拿到资源name ,再通过name拿到皮肤包资源ID
// app的resId
String resName=mAppResources.getResourceEntryName(resId); // 通过app的resId 找到 resName
String resType=mAppResources.getResourceTypeName(resId);// 通过app的resId 找到 类型,layout、drawable
// 获取对应皮肤包的资源Id
int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName)

DEMO 下载

最后是源码地址:给需要的朋友下载
将当中的module: app-jielun-skin app-luhan-skin 打包成APK 之后改名成 app-jielun-skin.skin2 以此类推,放在SDCARD中即可 就可以实现效果图中的样子
https://download.csdn.net/download/weixin_41063597/86892768

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容