1.原理简述
上篇说道外置资源文件的生成,可以和原来自带的资源文件使用两套Resources
进行替换,对于新打开的页面可以根据标签对每个View
进行判断并赋值,之前的页面会有无法更新的情况,并且对每个View
都重写或主动调用相关的设置属性的方法会很麻烦。
那么就得从根源上去处理View
属性的创建和缓存的逻辑,就是说,从View
的创建上入手,获取到相关的属性,用缓存存储相关页面的View
属性信息,更换主题的时候通知所有的缓存中的View
即可,当然这些View
也要及时清除,避免泄露。
而这些都是基于本篇的主角LayoutInflater
去处理的
1.LayoutInflater
我们平时用LayoutInflater
最多的场合是列表视图中,使用
View view = LayoutInflater.from(context).inflate(resId,null)
这个方式去获取一个View
的实例,其实Activity的setContentView
也是这么处理的
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
setContentView
方法中会查找到DecordView
中id为R.id.content
的容器,并把当前的resId
和contentParent
作为参数传递,这里就相当于创建一个View同时把这个View添加到contentParent
这个容器中来
而且这个方法我们常用的有两种重载方式
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
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) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
我们常用的传null的方式其实最终调用了inflate(resource, null, false)
这个重载方法
然后在这个方法里去创建当前指定的布局id的解析器XmlResourceParser
,并把传参的另外两个root
,root!=null
,传到下一个方法中去
private static final String TAG_MERGE = "merge";
private static final String TAG_INCLUDE = "include";
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
//获取到当前的AttributeSet接口
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (TAG_MERGE.equals(name)) { //merge标签
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 {
//实例化一个View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
//调用ViewGroup的方法生成一个默认的LayoutParams参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) { //如果为ture,则给View设置这个LayoutParams
temp.setLayoutParams(params);
}
}
//把当前的View作为父容器,遍历里面的所有子View
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
//如果父容器不为空,同时attach为true,则添加到父容器里
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
throw ie;
} catch (Exception e) {
throw ie;
} finally {
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
return result;
}
}
这里会通过Xml.asAttributeSet(parser)
方法生成当前的AttributeSet
对象,我们常用的AttributeSet
并不是字面意义上的集合,而是一个接口,方法调用也是适用传入内部的Parser
去处理的,比如XmlPullAttributes
class XmlPullAttributes implements AttributeSet {
@UnsupportedAppUsage
public XmlPullAttributes(XmlPullParser parser) {
mParser = parser;
}
public int getAttributeCount() {
return mParser.getAttributeCount();
}
public String getAttributeNamespace (int index) {
return mParser.getAttributeNamespace(index);
}
public String getAttributeName(int index) {
return mParser.getAttributeName(index);
}
public String getAttributeValue(int index) {
return mParser.getAttributeValue(index);
}
public String getAttributeValue(String namespace, String name) {
return mParser.getAttributeValue(namespace, name);
}
public boolean getAttributeBooleanValue(String namespace, String attribute,
boolean defaultValue) {
return XmlUtils.convertValueToBoolean(
getAttributeValue(namespace, attribute), defaultValue);
}
......
}
使用的传入的Parser去获取相应的属性,也就是说获取的属性会根据当前解析到的标签而变化,相当的灵活,这也是后面为什么这个属性Xml.asAttributeSet(parser)
只初始化一次,但当解析不同的标签的View时,能正确获取到相关属性的原因。
然后创建View的方法createViewFromTag
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
......
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
throw ie;
} catch (Exception e) {
throw ie;
}
}
这里分两步去创建
1.
通过tryCreateView
创建,如果创建成功直接返回
2.
第一步创建失败,那么使用默认的onCreateView
或者createView
方法进行创建,这两个方法差异判断是是否包含包名分隔符.
。一般系统自带的如TextView
直接书写就可以,而自定义的View一般都需要制定完整的路径,比如<com.test.CusView ... />
,这里就是根据这两种情况分别处理;其实也很简单,系统的会自动拼接前缀
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
......
try {
if (constructor == null) {
//反射后强转为View
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
//mConstructorArgs是一个有两个值的Object集合,初始都为空
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
//把attrs赋值给args
args[1] = attrs;
......
try {
//这里的args里有两个值,第一个是context,第二个是attrs
final View view = constructor.newInstance(args);
......
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
} catch (NoSuchMethodException e) {
throw ie;
} catch (ClassCastException e) {
throw ie;
} catch (ClassNotFoundException e) {
throw e;
} catch (Exception e) {
throw ie;
} finally {
}
}
而最终createView
会通过反射创建出真正的View实例,这个是在第一步创建失败的情况,那么再看下第一步的处理
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
.......
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);
}
return view;
}
也很简单,只是把创建View的过程交付给Factory
接口去处理,可以通过setFactory
方法设置处理的工厂,Factory2
相比于Factory
多了一个当前的父容器parent
; 我们通常修改的setFactory2
方法
public void setFactory2(Factory2 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 = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
可以看出有一个mFactorySet
标签,默认是false,设置过一次就变成true;也就是说这里只能设置一次,如果想要覆盖之前设置的怎么办,那就只能反射修改mFactorySet
成false
就可以了。
同时也可以看出,只要设置了mFactory2
,如果mFactory
为空则赋值为mFactory2
,如果不为空,那么就把这两个进行合并,生成一个FactoryMerger
的工厂,这里面的前两个参数传的是当前要设置的factory
,第三个参数传的是mFactory
,而最后一个是之前的mFactory2
,初始默认是应该是空的。
private static class FactoryMerger implements Factory2 {
private final Factory mF1, mF2;
private final Factory2 mF12, mF22;
FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
mF1 = f1;
mF2 = f2;
mF12 = f12;
mF22 = f22;
}
@Nullable
public View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
View v = mF1.onCreateView(name, context, attrs);
if (v != null) return v;
return mF2.onCreateView(name, context, attrs);
}
@Nullable
public View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs) {
View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
: mF1.onCreateView(name, context, attrs);
if (v != null) return v;
return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
: mF2.onCreateView(name, context, attrs);
}
}
其实Activity在创建的时候已经添加了一个默认的Factory
//androidx.appcompat.app.AppCompatActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
//androidx.appcompat.app.AppCompatDelegateImpl
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
在AppCompatActivity
的onCreate
方法中会通过installViewFactory
设置一个默认的Factory2
//AppCompatDelegateImpl
class AppCompatDelegateImpl extends AppCompatDelegate
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
......
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP,true,VectorEnabledTintResources.shouldBeUsed()
);
}
}
//AppCompatViewInflater
public class AppCompatViewInflater {
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
......
View view = null;
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
.......
default:
view = createView(context, name, attrs);
}
if (view == null && originalContext != context) {
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
checkOnClickListener(view, attrs);
}
return view;
}
@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
@NonNull
protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
return new AppCompatImageView(context, attrs);
}
.......
}
可以看出,AppCompatActivity
中其实是重写了Factory
把之前控件替换成AppCompat
相关的View, 那么我们通过setFactory
也是可以监听到所有的View的创建过程的,而创建过程中我们是可以获取到相关的attrs
属性的,比如background
,textColor
等
换句话说,自定义Factory2
可以接管View的创建过程,并且每个View布局中定义的属性都是可以拿到的,那么通过这个设置就可以获取每个页面所对应的需要更换主题的控件的集合,可以统一管理,根据不同资源进行匹配的方法也只需要写一套即可;
fun setDefaultFactorys(activity: Activity) {
try {
val name = activity.javaClass.name
val factory = SkinLayoutFactory(name)
val filed = LayoutInflater::class.java.getDeclaredField("mFactorySet")
filed.isAccessible = true
val inflater = LayoutInflater.from(activity)
filed.set(inflater, false)
inflater.factory2 = factory
} catch (e: Exception) {
e.printStackTrace()
}
}
class SkinLayoutFactory constructor(val activityName: String) : LayoutInflater.Factory2 {
private val constructorMap: HashMap<String, Constructor<*>> = HashMap()
private val constructorSignature = arrayOf(
Context::class.java, AttributeSet::class.java
)
private val prefixList = arrayOf("android.view.", "android.widget.")
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
return onCreateView(name, context, attrs)
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
if (name.indexOf(".") != -1) {
return createView(context, name, attrs)
} else {
return createInnerView(context, name, attrs)
}
}
fun createInnerView(context: Context, name: String, attrs: AttributeSet?): View? {
var view: View? = null
for (i in prefixList.indices) {
val realViewName = "${prefixList[i]}$name"
view = createView(context, realViewName, attrs)
if (view != null) return view
}
return null
}
fun createView(context: Context, name: String, attrs: AttributeSet?): View? {
try {
var consturctorUse: Constructor<*>? = null
if (constructorMap.containsKey(name)) {
consturctorUse = constructorMap[name]
} else {
val clazz = Class.forName(name)
if (clazz != null) {
consturctorUse = clazz.getConstructor(*constructorSignature)
if (consturctorUse != null) {
consturctorUse.isAccessible = true
constructorMap[name] = consturctorUse
} else {
throw IllegalArgumentException("构造函数缺失")
}
}
}
consturctorUse?.apply {
val view = consturctorUse!!.newInstance(context, attrs) as View
SkinManager.getInstance().doSaveSkinItem(activityName, view, attrs)
return view
}
} catch (e: Exception) {
}
return null
}
}
这里很简单的使用了一个反射的方式修改mFactorySet
属性为false
,并自定义了一个Factory2
的实现类,然后把这个Factory设置给当前的LayoutInflater
,为了方便处理,在每个需要进行主题替换的Activity的setContentView
前进行修改,通过自定义的Factory
进行View属性的收集,然后在Activity的onDestroy
方法中进行数据清除。
这个可以通过我们常用Application.ActivityLifecycleCallbacks
进行注册处理,比如在onActivityCreated
中进行上面的setDefaultFactorys
方法的调用;经常有人觉得onActivityCreated
在onCreate
方法执行完后调用,这时候已经执行完setContentView
,会有问题;其实不然,看下这个方法回调的时机
//ndroid.app.Activity
protected void onCreate(@Nullable Bundle savedInstanceState) {
......
dispatchActivityCreated(savedInstanceState);
......
}
private void dispatchActivityCreated(@Nullable Bundle savedInstanceState) {
getApplication().dispatchActivityCreated(this, savedInstanceState);
......
}
这里的getApplication().dispatchActivityCreated
就是这个回调的时间点
可以看出这是在父类Activity
中的onCreate
进行处理的,我们通常都是重写并调用super.onCreate()
,这个时候已经完成onActivityCreated
的回调了,也就保证了在setContentView
之前进行调用;
ActivityLifecycleCallbacks
中的回调基本都是在父类方法中进行的,我们子类重写这个方法调用super
的时候已经完成了
然后根据我们自定义的Factory
,我们可以捕获到我们定义的View的信息
private val defaultRef = arrayOf(
"textColor",
"background"
)
fun doSaveSkinItem(activityName: String, view: View, attr: AttributeSet?) {
var skinStore: SkinStore? = null
attr?.apply {
for (i in 0 until attributeCount) {
val name = attr.getAttributeName(i)
val value = attr.getAttributeValue(i)
if (defaultRef.contains(name)) {
if (value.startsWith("@")) {
//测试只考虑@标示符
val resId = value.substring(1).toInt()
if (resId != 0) {
if (skinStore == null) {
skinStore = SkinStore(view, name, resId)
} else {
skinStore!!.addNesSkinItem(name, resId)
}
}
}
}
}
skinStore?.apply {
addTo(activityName, skinStore!!)
}
}
}
比如我们可以根据属性进行筛选,这里通过getAttributeValue
获取的都是字符串,上面我们获取到@resId
资源的时候,需要截取除了首位之外的字符;注意打包之后,我们获取的资源属性都变成了id匹配的,比如上面的可能变成@1234567
的资源id,只需要截取就可以转成真正的映射id了
那么这时候,我们已经保存了相关的View,相关的属性名,已经当前属性匹配的资源id了,这里是原应用内的。
根据上篇的新建的Resources
资源配置
fun findResIntValue(originId: Int): Int {
val resName = appResource!!.getResourceEntryName(originId)
val resType = appResource!!.getResourceTypeName(originId)
if (useOrigin) return getResourceIntValue(resType, originId, appResource!!)
if (skinResouce != null && skinPkgName != null) {
val skinId = skinResouce!!.getIdentifier(resName, resType, skinPkgName)
val colorValue = getResourceIntValue(resType, skinId, skinResouce!!)
if (colorValue != Int.MAX_VALUE) return colorValue
}
return Int.MAX_VALUE
}
fun getResourceIntValue(name: String, colorId: Int, resources: Resources): Int {
try {
when (name) {
"color" -> {
val color = resources.getColor(colorId)
return color
}
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return -1
}
我们可以通过刚刚保存的id
使用getResourceEntryName
查找出对应的资源声明字符串,比如aaa.png(这个查找出的.png后缀是没有的,这里只是提示完整资源)
。
也通过getResourceTypeName
获取到对应的资源类型,比如color
,drawable
。
根据这些属性我们就可以从自定义的Resources
去查找到指定的资源了;
注意如果是应用内自带的不需要进行进行额外查找,如果是下载的需要更新的主题包,那么需要通过getIdentifier
查找到该主题资源包中对应的资源id,然后重复上面的操作即可;而查找资源库中对应名称的资源id使用需要传递指定的资源名和资源类型,比如上面的aaa
和drawable
。
比如上面例子通过color
这个type
,然后根据传入的resource
去查找对应的id
对应的属性
3.总结
那么换肤的原理步骤可以简单分为下面的(这里以资源包apk为例)
1.
新建一个资源工程, 准备好需要替换的资源,名称声明和应用内需要替换的名称和类型保持一致,只是资源内容不一样,直接打包成apk就可以
2.
自定义Factory2
去接管View
的创建过程,可以获取到页面内所有的View
的属性,记录下所需要替换资源的View
以及所对应的属性,比如src
,background
,可以根据tag
区分需要更新的View
3.
通过反射的方式创建指定路径的AssetManager
(就是上面生成的apk的路径),并以此生成对应的主题资源包Resources
4.
依据保存的资源属性,去查找主题包Resources
中对应的资源id
,并根据资源类型刷新所保存的所有需要替换资源的View
即可
5.
上述方法只适用于布局中生成的View
,对于通过new XView(...)
方法生成的则需要额外进行存储,原理其实差不多,这里就不多描述了