Android 动态换肤技术原理 | 实践 及总结
实现的效果图
动态换肤一般实现的原理
- 对页面需要换肤的View进行标记
- 在
Activity#setContentView()
加载view时获取到标记的view(后面会说是要怎么获取到) - 创建一个Library项目制作我们的皮肤包(res下的资源名称需要与app使用的一致,换肤就是通过使用的资源名称去
皮肤包
里加载相同名字的资源) - 创建皮肤包对应的
Resources
对象(用于加载皮肤包内的资源) - 点击换肤将我们标记的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
这里使用的是自定义布局加载器LayoutInflater
的LayoutInflater.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布局文件,他传入了我们的布局资源id
和android.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对象
,AssetManager
、PackageManager
的使用 - 加载apk包内的资源,
Resources
的使用
Android 动态换肤技术原理 | 实践 及总结相关推荐
- android 状态栏 背景色_技术一面:说说Android动态换肤实现原理
换肤分为动态换肤和静态换肤 静态换肤 这种换肤的方式,也就是我们所说的内置换肤,就是在APP内部放置多套相同的资源.进行资源的切换. 这种换肤的方式有很多缺点,比如, 灵活性差,只能更换内置的资源.a ...
- 真香定律!Android动态换肤实现原理解析,原理+实战+视频+源码
自己项目中一直都是用的开源的xUtils框架,包括BitmapUtils.DbUtils.ViewUtils和HttpUtils四大模块,这四大模块都是项目中比较常用的.最近决定研究一下xUtils的 ...
- 安卓app开发教程!Android动态换肤实现原理解析,值得收藏!
开头 Android开发,假如开始没有任何的开发经验的话, 千万不要着急,不要想着在短时间内就把一个语言学习好, 因为你之前没有任何的学习经验, 在这个过程中需要有耐心地学习完JAVA的基础知识, 然 ...
- Android动态换肤实现原理解析,原理+实战+视频+源码
前言 本人今年25岁,毕业之后进入一家小型的互联网公司工作,在这原公司呆了3年,直至今年才有了跳槽的想法. 每个程序员 都拥有大厂梦,我也不例外,在小公司待久了,感觉人会荒废掉,太轻松,没有压迫感.因 ...
- android视频编辑sdk!Android动态换肤实现原理解析,灵魂拷问
" 对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡." 高薪的IT行业是众多年轻人的职业梦想,然而,一旦身入其中却发觉没有想像中那么美好.被称为IT蓝领 ...
- 真香定律!Android动态换肤实现原理解析,吐血整理
自己项目中一直都是用的开源的xUtils框架,包括BitmapUtils.DbUtils.ViewUtils和HttpUtils四大模块,这四大模块都是项目中比较常用的.最近决定研究一下xUtils的 ...
- android布局优化!Android动态换肤实现原理解析,灵魂拷问
" 对于程序员来说,如果哪一天开始他停止了学习,那么他的职业生涯便开始宣告消亡." 高薪的IT行业是众多年轻人的职业梦想,然而,一旦身入其中却发觉没有想像中那么美好.被称为IT蓝领 ...
- app保活面试题,Android动态换肤实现原理解析,再不刷题就晚了!
前言 近期被两则消息刷屏,[字节跳动持续大规模招聘,全年校招超过1万人][腾讯有史以来最大规模的校招启动]当然Android岗位也包含在内,因此Android还是有很多机会的.结合往期面试的同学(主要 ...
- Android动态换肤实现原理解析,灵魂拷问
前言 转眼间,2020 年已过去一大半了,2020 年很难,各企业裁员的消息蛮多的,降职,不发年终奖等等.2020 年确实是艰难的一年.然而生活总是要继续,时间不给你丧的机会!如果我们能坚持下来,不断 ...
最新文章
- 直播 | 同源共流:一个优化框架统一与解释图神经网络
- DDoS deflate–简单解决VPS被DDOS/CC攻击
- 使用正则表达式更改编译版本号
- codeforces National Property 2sat模板题
- jdk1.8 ::构造函数_在JDK 8中可通过反射获得构造函数/方法参数元数据
- java基础加强_补充笔记
- 关于CSS浮动(float,clear)的通俗讲解(经验分享)
- Python精简入门级学习(二)
- 你可能不知道的10个Python Pandas的技巧和特性
- if 组件是否存在_LeetCode 817. 链表组件 题解
- 【转】Redis学习---阿里云Redis多线程性能增强版详解
- python屏幕录像专家_可以推荐一款电脑录屏软件吗?
- android协同开发平台,开发协作平台
- 微信小程序轮播图实现(超简单)
- Fluent 全流程求解多孔介质算例
- 解决win10拨号连接下无法开启热点
- 计算机毕设Python+Vue医院人事及科室病区管理(程序+LW+部署)
- 证明:每个有n个顶点的连通图都至少有n-1条边 Show that every connected graph with n vertices has at least n − 1 edges.
- 企业微信手机端可以退出吗?会影响电脑端企业微信吗?
- 安装命令:pip install xlrd ,pandas操作Excel学习笔记__7000