简言:
工作中我们经常会收到产品经理提出手机需要有更换皮肤的功能,现在市面上大多数都是以主题换肤为主的,今天我这里介绍另一种,利用hook技术来实现一键换肤功能。
hook简介:
Hook翻译过来是钩子的意思,我们都知道无论是手机还是电脑运行的时候都依赖系统各种各样的API,当某些API不能满足我们的要求时,我们就得去修改某些api,使之能满足我们的要求。这样api hook就自然而然的出现了。我们可以通过api hook,改变一个系统api的原有功能。基本的方法就是通过hook“接触”到需要修改的api函数入口点,改变它的地址指向新的自定义的函数。当然这种技术同样适用于Android系统,在Android开发中,我们同样能利用Hook的原理让系统某些方法运行时调用的是我们定义的方法,从而满足我们的要求。
哪些资源可以替换呢?
我们可以替换的资源有动画、背景图片、字体、字体颜色、字体大小、音频、视频等,总的来说,res目录下的所有资源都可以被替换掉。
换肤的步骤
Android是如何实例化布局的呢?
详细的流程这篇文章已经写的很详细了Android布局加载流程 这里就不做多的探讨了。下面直接到我们的主题。
实现一键换肤
第一步:创建一个SkinFactory类让它继承LayoutInflater.Factory2,该类的功能是:1、拦截系统创建view的过程,由我们自己创建view;2、收集需要换肤的view;3、换肤操作。下面贴出源码:
public class SkinFactory implements LayoutInflater.Factory2 {
private AppCompatDelegate mDelegate;//预定义一个委托类,它负责按照系统的原有逻辑来创建view
private List<SkinView> listCacheSkinView = new ArrayList<>();//我自定义的list,缓存所有可以换肤的View对象
/**
* 给外部提供一个set方法
*
* @param mDelegate
*/
public void setDelegate(AppCompatDelegate mDelegate) {
this.mDelegate = mDelegate;
}
/**
* Factory2 是继承Factory的,所以,我们这次是主要重写Factory的onCreateView逻辑,就不必理会Factory的重写方法了
*
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
* @param parent
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// TODO: 关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是要全盘接管
View view = mDelegate.createView(parent, name, context, attrs);//系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案
if (view == null) {//万一系统创建出来是空,那么我们来补救
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {//不包含. 说明不带包名,那么我们帮他加上包名
view = createViewByPrefix(context, name, prefixs, attrs);
} else {//包含. 说明 是权限定名的view name,
view = createViewByPrefix(context, name, null, attrs);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//TODO: 关键点2 收集需要换肤的View
collectSkinView(context, attrs, view);
return view;
}
/**
* TODO: 收集需要换肤的控件
* 收集的方式是:通过自定义属性isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中
*/
private void collectSkinView(Context context, AttributeSet attrs, View view) {
// 获取我们自己定义的属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Skinable);
boolean isSupport = a.getBoolean(R.styleable.Skinable_isSupport, false);
if (isSupport) {//找到支持换肤的view
final int Len = attrs.getAttributeCount();
HashMap<String, String> a