美文网首页Android Other
Android 动态换肤框架原理及DEMO

Android 动态换肤框架原理及DEMO

作者: WilburLi | 来源:发表于2022-11-02 17:33 被阅读0次

先看效果图,再讲原理,最后是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

相关文章

  • Android 动态换肤框架原理及DEMO

    先看效果图,再讲原理,最后是DEMO地址,我也是整理了别人的资料最终成文 前言 动态换肤的思路是需要先了解系统资源...

  • Android动态换肤框架-换肤原理

    注:下文源码有删减,截图只体现主流程 1、换肤原理 换肤就是替换资源(文字、颜色、图片等),而换肤基本有两种模式:...

  • Android动态换肤框架-实现换肤

    1、换肤流程 2、采集流程 3、Android资源查找流程 4、采集需要换肤的控件 换肤我们需要换所有可能需要换的...

  • Android-Skin-Loader源码解析

    源码 一:简介 Android-Skin-Loader是一个通过动态加载技术实现换肤的框架;解决换肤的两个核心问题...

  • Android动态换肤原理解析及实践

    前言: 本文主要讲述如何在项目中,在不重启应用的情况下,实现动态换肤的效果。换肤这块做的比较好的,有网易云音乐,q...

  • 【靶点突破】网易云换肤方案探讨

    【靶点突破】网易云换肤方案探讨 老方案 网易云音乐换肤方案原理 动手实现一个网易云换肤方案的demo 动手打造换肤...

  • Android-skin-loader 换肤总结

    前言 最近有个换肤的需求。基于github上的这个开源框架Android-Skin-Loader。这个框架的换肤机...

  • 动态换肤框架

    换肤模式 内置换肤 在APK包中存在多种资源(图片、颜色值等)用于换肤切换自由度低,APK文件大一般用于没有其他需...

  • Android主题换肤框架 无缝切换

    Android-Skin-Loader 一个通过动态加载本地皮肤包进行换肤的皮肤框架 工程目录介绍 用法 1. 在...

  • Android高级进阶之-动态换肤原理及实现

    话说什么是动态换肤?这里举个例子:在APP中可以下载某一个皮肤包,然后应用起来整个APP的界面就发生了改变,诸如某...

网友评论

    本文标题:Android 动态换肤框架原理及DEMO

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