实现的效果图

动态换肤一般实现的原理

  1. 对页面需要换肤的View进行标记
  2. Activity#setContentView()加载view时获取到标记的view(后面会说是要怎么获取到)
  3. 创建一个Library项目制作我们的皮肤包(res下的资源名称需要与app使用的一致,换肤就是通过使用的资源名称去皮肤包里加载相同名字的资源)
  4. 创建皮肤包对应的Resources对象(用于加载皮肤包内的资源)
  5. 点击换肤将我们标记的View的一些属性上设置的值修改为皮肤包里的值,这样就达到换肤的效果

一、对页面需要换肤的View进行标记

这一步是相对简单的,只要自定义一个属性即可;在获取View的时候判断有无这个属性 有就将这个view存起来
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:skin="http://schemas.android.com/apk/azhon-skin"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="16dp"tools:context=".MainActivity"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@color/bg_1"android:text="我是一个TextView"android:textColor="@color/title_1"android:textSize="16sp"skin:enable="true" /><Buttonandroid:id="@+id/btn_dark"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:background="@color/bg_2"android:text="@string/btn_text"android:textColor="@color/title_2"skin:enable="true" /><Buttonandroid:id="@+id/btn_default"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:background="@color/bg_3"android:text="@string/btn_reset_text"android:textColor="@color/title_3" />
</LinearLayout>

skin:enable="true"这个就是自定的一个属性取值为boolean,如果为true就表示在换肤的时候需要去皮肤包加载对应的资源

二、获取在布局标记好的View

这里使用的是自定义布局加载器LayoutInflaterLayoutInflater.Factory2来监听View的创建;下面我们来通过阅读源码来具体说一下为什么使用的这个:

  • 查看AppCompatActivity的setContentView()方法
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
}
  • 接着继续调用了getDelegate()的setContentView()方法
// AppCompatActivity.java
@Override
public void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);
}

getDelegate()获取到的是AppCompatDelegate这个抽象类的实现类,而他的实现类就只有一个AppCompatDelegateImpl

  • 接着调用了AppCompatDelegateImpl的setContentView()
// AppCompatDelegateImpl.java
@Override
public void setContentView(int resId) {ensureSubDecor();ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();//重点就是这样代码,通过布局加载器加载xml文件LayoutInflater.from(mContext).inflate(resId, contentParent);mAppCompatWindowCallback.getWrapped().onContentChanged();
}

阅读到这里就可以看到有用的代码了LayoutInflater.from(mContext).inflate(resId, contentParent)加载我们的xml布局文件,他传入了我们的布局资源idandroid.R.id.content这个ViewGroup;有了解过Activity的布局层次结构的同学肯定就知道是什么了。

  • 接着往下看LayoutInflater的inflate()方法
// LayoutInflater.java
//No.1
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {return inflate(resource, root, root != null);
}//No.2 接着调用了
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {final Resources res = getContext().getResources();if (DEBUG) {Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("+ Integer.toHexString(resource) + ")");}final XmlResourceParser parser = res.getLayout(resource);try {return inflate(parser, root, attachToRoot);} finally {parser.close();}
}//No.3 接着调用了
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");// 省略若干源代码.... if (TAG_MERGE.equals(name)) {if (root == null || !attachToRoot) {throw new InflateException("<merge /> can be used only with a valid "+ "ViewGroup root and attachToRoot=true");}rInflate(parser, root, inflaterContext, attrs, false);} else {// Temp is the root view that was found in the xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;// 省略若干源代码.... }}
}

调用inflate()最终调用了createViewFromTag()这个方法根据布局写的代码开始创建对应的View实体,继续向下查看createViewFromTag()的代码

// LayoutInflater.java
// No.1
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {return createViewFromTag(parent, name, context, attrs, false);
}// No.2
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {// 省略若干源代码.... try {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);}if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {if (-1 == name.indexOf('.')) {view = onCreateView(parent, name, attrs);} else {view = createView(name, null, attrs);}} finally {mConstructorArgs[0] = lastContext;}}return view;} catch (InflateException e) {throw e;} // 省略若干源代码....
}

代码查看到这里终于看到了开头所说的Factory这个东西,上面代码最终通过调用onCreateView()来创建view;所以我们只需要对LayoutInflater设置一个Factory即可。

先来看看设置setFactory()的方法
// LayoutInflater.java
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);}
}
  • 可以很清楚的看到,如果我们调用了这个方法那么肯定会抛出一个异常IllegalStateException ,A factory has already been set on this LayoutInflater,所以设置之前我们需要通过反射将mFactorySet这个变量置为false

需要注意的一点:

既然是干预View的加载创建,那肯定设置Factory需要在LayoutInflater实例创建之后,在加载创建View之前;而Activity是通过setContentView()加载View所以设置Factory需要在setContentView()之前;这里可以通过Application设置Activity的生命周期监听器,即registerActivityLifecycleCallbacks()

上面bb了一堆现在来上代码了

public class App extends Application {@Overridepublic void onCreate() {super.onCreate();registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {@Overridepublic void onActivityCreated(Activity activity, Bundle savedInstanceState) {setFactory(activity);}@Overridepublic void onActivityStarted(Activity activity) {}@Overridepublic void onActivityResumed(Activity activity) {}@Overridepublic void onActivityPaused(Activity activity) {}@Overridepublic void onActivityStopped(Activity activity) {}@Overridepublic void onActivitySaveInstanceState(Activity activity, Bundle outState) {}@Overridepublic void onActivityDestroyed(Activity activity) {}});}
}
  • 创建SkinFactory.java
public final class SkinFactory implements LayoutInflater.Factory2 {private static final String TAG = "SkinFactory";private static final String[] classPrefixList = {"android.view.", "android.widget.", "android.webkit."};private static final String NAME_SPACE = "http://schemas.android.com/apk/azhon-skin";private static final String ATTRIBUTE = "enable";@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {//找到布局使用属性(skin:enable="true")标记需要换肤的viewboolean isSkinView = attrs.getAttributeBooleanValue(NAME_SPACE, ATTRIBUTE, false);//如果不是换肤的View就直接不处理if (!isSkinView) return null;View view = null;//name不包含.的说明是系统的控件if (-1 == name.indexOf('.')) {for (String prefix : classPrefixList) {view = createView(name, prefix, context, attrs);if (view != null) break;}} else {view = createView(name, null, context, attrs);}LogUtil.d(TAG, "onCreateView: 加载换肤View成功..." + view);return view;}/*** 创建系统自带View*/private View createView(String name, String prefix, Context context, AttributeSet attrs) {View view = null;try {view = LayoutInflater.from(context).createView(name, prefix, attrs);} catch (ClassNotFoundException e) {//}return view;}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}
}
  • 设置Factory
/*** 设置布局解析Factory* 需要将LayoutInflater的mFactorySet变量设置为false*/
private void setFactory(Activity activity) {try {LayoutInflater inflater = activity.getLayoutInflater();Field field = LayoutInflater.class.getDeclaredField("mFactorySet");field.setAccessible(true);field.setBoolean(inflater, false);//设置自己的FactoryLayoutInflaterCompat.setFactory2(inflater, new SkinFactory());} catch (Exception e) {e.printStackTrace();}
}

在SkinFactory#onCreateView()中就可以获取到我们标记的View了,这里需要保存换肤的View,需要替换的属性和属性的值

三、创建一个Library项目制作皮肤包资源

  • app默认的颜色资源
  • 对应的皮肤包如下:
  • 作为皮肤包只需要res目录可以将java的目录代码全部删除
  • 皮肤包中定义的资源名称必须与主app定义的一模一样
  • 然后通过AS的菜单——>Build——>Build Bundle(s) / APK(s)——> Build APK(s)就可以打包出来了

四、有了皮肤包资源就可以创建Resources对象拿到res/下的所有资源

  • 创建Resources对象
/*** 创建皮肤包的Resources** @param path 皮肤包路径*/
public void createResources(Context context, String path) {try {AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.invoke(assetManager, path);Resources resources = context.getResources();//创建对象Resources skinResources = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());//获取皮肤包(也就是apk)的包名PackageManager packageManager = context.getPackageManager();PackageInfo packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);String skinPackageName = packageInfo.packageName;} catch (Exception e) {e.printStackTrace();}
}
  • path 就是皮肤包路径了/sdcard/Android/data/com.azhon.dynamicskin/cache/dark.skin
  • 通过PackageManager获取皮肤包的包名,包名在获取皮肤包内的资源时会用到

五、加载皮肤包内的资源,下面通过一个示例来讲解

  • 我们需要替换这个TextView的background,textColor这两个属性
<TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@color/bg_1"android:text="我是一个TextView"android:textColor="@color/title_1"android:textSize="16sp"skin:enable="true" />

在自定义的SkinFactory中就可以获取每一个属性和属性对应的值,如下:

这里的@开头值后面的数字就是res下的资源对应的Id(也是就是R文件的Id)

先介绍一个重要的(api)方法

int resId = resources.getIdentifier(String name, String defType, String defPackage);
第一个参数:资源的名字,例如:bg_1、titile_1
第二个参数:资源类型,例如:drawable、color、string
第三个参数:resources资源对应的包名

根据资源id加载皮肤包内对应的资源

  • 封装的方法
/*** 根据资源Id获取资源的名称* @param resources    app自身的资源对象* @param skinResources 皮肤包创建的资源对象* @param id            当前使用的资源id*/
public static int getResourcesIdByName(Resources resources,Resources skinResources, String packageName, int id) {String[] res = getResourcesById(resources, id);//使用皮肤包创建的Resources加载资源return skinResources.getIdentifier(res[0], res[1], packageName);
}/*** 根据资源Id获取资源的名称** @param id 资源id* @return 资源名称*/
public static String[] getResourcesById(Resources resources, int id) {String entryName = resources.getResourceEntryName(id);String typeName = resources.getResourceTypeName(id);return new String[]{entryName, typeName};
}
  • 获取对应皮肤包内的资源id(2130968664就是获取到的资源id)
int skinResId = getResourcesIdByName(context.getResources(),skinResources,skinPackageName,2130968664);
  • 获取到了资源的id,但是这个值是不能直接使用的需要在进一步操作
  • 上面通过getResourcesById()这个方法知道了这个资源id是属于color类型的了,所以只要在调用一次getColor即可
int color = skinResources.getColor(skinResId);

通过上面几步就成功的拿到了皮肤包内对应的资源,最后就只要调用TextView的setTextColor(color)就可以成功的替换文字的颜色了,同理替换background也是一样的。

  • Resources也还提供了许多其它的方法:

Demo示例下载地址

需要将项目根目录的 dark.skin 文件拷贝至/sdcard/Android/data/com.azhon.dynamicskin/cache/目录下

六、总结

  • 干预View的加载创建,Factory的原理和使用
  • 对一个apk包创建对应的Resources对象AssetManagerPackageManager的使用
  • 加载apk包内的资源,Resources的使用

Android 动态换肤技术原理 | 实践 及总结相关推荐

  1. android 状态栏 背景色_技术一面:说说Android动态换肤实现原理

    换肤分为动态换肤和静态换肤 静态换肤 这种换肤的方式,也就是我们所说的内置换肤,就是在APP内部放置多套相同的资源.进行资源的切换. 这种换肤的方式有很多缺点,比如, 灵活性差,只能更换内置的资源.a ...

  2. 真香定律!Android动态换肤实现原理解析,原理+实战+视频+源码

    自己项目中一直都是用的开源的xUtils框架,包括BitmapUtils.DbUtils.ViewUtils和HttpUtils四大模块,这四大模块都是项目中比较常用的.最近决定研究一下xUtils的 ...

  3. 安卓app开发教程!Android动态换肤实现原理解析,值得收藏!

    开头 Android开发,假如开始没有任何的开发经验的话, 千万不要着急,不要想着在短时间内就把一个语言学习好, 因为你之前没有任何的学习经验, 在这个过程中需要有耐心地学习完JAVA的基础知识, 然 ...

  4. Android动态换肤实现原理解析,原理+实战+视频+源码

    前言 本人今年25岁,毕业之后进入一家小型的互联网公司工作,在这原公司呆了3年,直至今年才有了跳槽的想法. 每个程序员 都拥有大厂梦,我也不例外,在小公司待久了,感觉人会荒废掉,太轻松,没有压迫感.因 ...

  5. android视频编辑sdk!Android动态换肤实现原理解析,灵魂拷问

    " 对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡." 高薪的IT行业是众多年轻人的职业梦想,然而,一旦身入其中却发觉没有想像中那么美好.被称为IT蓝领 ...

  6. 真香定律!Android动态换肤实现原理解析,吐血整理

    自己项目中一直都是用的开源的xUtils框架,包括BitmapUtils.DbUtils.ViewUtils和HttpUtils四大模块,这四大模块都是项目中比较常用的.最近决定研究一下xUtils的 ...

  7. android布局优化!Android动态换肤实现原理解析,灵魂拷问

    " 对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡." 高薪的IT行业是众多年轻人的职业梦想,然而,一旦身入其中却发觉没有想像中那么美好.被称为IT蓝领 ...

  8. app保活面试题,Android动态换肤实现原理解析,再不刷题就晚了!

    前言 近期被两则消息刷屏,[字节跳动持续大规模招聘,全年校招超过1万人][腾讯有史以来最大规模的校招启动]当然Android岗位也包含在内,因此Android还是有很多机会的.结合往期面试的同学(主要 ...

  9. Android动态换肤实现原理解析,灵魂拷问

    前言 转眼间,2020 年已过去一大半了,2020 年很难,各企业裁员的消息蛮多的,降职,不发年终奖等等.2020 年确实是艰难的一年.然而生活总是要继续,时间不给你丧的机会!如果我们能坚持下来,不断 ...

最新文章

  1. 直播 | 同源共流:一个优化框架统一与解释图神经网络
  2. DDoS deflate–简单解决VPS被DDOS/CC攻击
  3. 使用正则表达式更改编译版本号
  4. codeforces National Property 2sat模板题
  5. jdk1.8 ::构造函数_在JDK 8中可通过反射获得构造函数/方法参数元数据
  6. java基础加强_补充笔记
  7. 关于CSS浮动(float,clear)的通俗讲解(经验分享)
  8. Python精简入门级学习(二)
  9. 你可能不知道的10个Python Pandas的技巧和特性
  10. if 组件是否存在_LeetCode 817. 链表组件 题解
  11. 【转】Redis学习---阿里云Redis多线程性能增强版详解
  12. python屏幕录像专家_可以推荐一款电脑录屏软件吗?
  13. android协同开发平台,开发协作平台
  14. 微信小程序轮播图实现(超简单)
  15. Fluent 全流程求解多孔介质算例
  16. 解决win10拨号连接下无法开启热点
  17. 计算机毕设Python+Vue医院人事及科室病区管理(程序+LW+部署)
  18. 证明:每个有n个顶点的连通图都至少有n-1条边 Show that every connected graph with n vertices has at least n − 1 edges.
  19. 企业微信手机端可以退出吗?会影响电脑端企业微信吗?
  20. 安装命令:pip install xlrd ,pandas操作Excel学习笔记__7000

热门文章

  1. python数据处理_读取txt数据并绘图
  2. 使用 journalctl 查看日志
  3. python字符串操作编程题_python开发基础02-字符串操作方法练习题
  4. 发布微信小程序体验版的流程
  5. ImageJ二次开发-Java
  6. 写一个函数,输入一个十六进制数,输出相应的十进制数。-----(自己编程)
  7. 【正则表达式】判断是否为名字
  8. html5的设置图片来回浮动,图片浮动的怪异问题和解决方案
  9. C/C++ RPG游戏
  10. 为了忘却的纪念,我的天龙游戏生涯