背景
Android换肤技术已经是很久之前就已经被成熟使用的技术了,公司业务上需要用到换肤.为了不重复造轮子,并且快速实现需求,并且求稳,发现主要有两个框架比较流行,Android-Skin-Loader和Android-skin-support
Android-Skin-Loader
GitHub - fengjundev/Android-Skin-Loader: 一个通过动态加载本地皮肤包进行换肤的皮肤框架
可以看到好几年都没人维护了,出了问题也不好解决(这里没有丝毫贬低该框架的意思)
这里大概说下原理,通过LayoutInflater.setFactory的方式, 在回调的onCreateView中解析每一个View的attrs, 判断是否有已标记需要换肤的属性, 比方说background, textColor, 或者说相应资源是否为skin_开头等等. 然后保存到map中, 对每一个View做for循环去遍历所有的attr, 想要对更多的属性进行换肤, 需要Activity实现接口, 将需要换肤的View, 以及相应的属性收集到一起
Android-skin-support
Github上一个star数比较多的换肤框架-Android-skin-support(一款用心去做的Android 换肤框架, 极低的学习成本, 极好的用户体验. 一行代码就可以实现换肤, 你值得拥有!!!). 简单了解之后,可以快速上手,并且侵入性很低,源码地址: https://github.com/ximsfei/Android-skin-support
介绍
SkinCompatManager.withoutActivity(this).loadSkin();
就这么简单, 你的APK已经拥有了强大的换肤功能, 当然现在是拥有了换肤功能, 别忘了制作皮肤包.
功能
支持布局中用到的资源换肤。
支持代码中设置的资源换肤。
默认支持大部分基础控件,Material Design换肤。
支持动态设置主题颜色值,支持选择sdcard上的图片作为drawable换肤资源。
支持多种加载策略(应用内/插件式/自定义sdcard路径/zip等资源等)。
资源加载优先级: 动态设置资源-加载策略中的资源-插件式换肤/应用内换肤-应用资源。
支持定制化,选择需要的模块加载。
支持矢量图(vector/svg)换肤。
skin-support 4.0.0以上支持AndroidX,4.0.0以下支持support库
更详细的信息可以直接参考官方说明,很详细
那么它是如何实现换肤的呢,下面先来点预备知识
AppCompatActivity实现
吐槽一下,Google为了让开发者升级androidx,support28版本很多库都不提供源码,大家可能也发现了,好了回到正题
public class AppCompatActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
...
}
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
....
}
AppCompatActivity 将大部分生命周期委托给了AppCompatDelegate
源码中主要使用了AppCompateDelegate的子类AppCompatDelegateImpl
class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
}
}
AppCompatDelegateImpl中, 在LayoutInflaterFactory的接口方法onCreateView 中将View的创建交给了AppCompatViewInflater
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (this.mAppCompatViewInflater == null) {
TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);
if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
try {
Class viewInflaterClass = Class.forName(viewInflaterClassName);
this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();
} catch (Throwable var8) {
Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);
this.mAppCompatViewInflater = new AppCompatViewInflater();
}
} else {
this.mAppCompatViewInflater = new AppCompatViewInflater();
}
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = attrs instanceof XmlPullParser ? ((XmlPullParser)attrs).getDepth() > 1 : this.shouldInheritContext((ViewParent)parent);
}
//可以直接看这里
return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}
再来看一下AppCompatViewInflater中createView的实现
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
......
View view = null;
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);