Android 插件化换肤原理
一、什么是换肤?
换肤功能是指:我们预先准备好几套皮肤资源包,然后用户可以随意选择一套皮肤进行更换,更换后界面上的 View 相关资源(颜色、样式、图片、背景等)相应发生改变;
知道换肤表现,我们就要想想如何实现这个过程:
- 我们要如何加载皮肤资源包
- 我们要如何对view进行换肤
- 我们要如何通知所有view进行换肤
二、View如何换肤?
首先我们来解决下最重要的问题?下如何对View进行换肤?
在这之前,我们看下Activity是如何解析xml,创建view的。AppCompatActivity setContentView()
AppCompatActivity.java
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
getDelegate().setContentView(layoutResID);
}
....
....
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
AppCompatDelegate是AppCompatActivity的代理处理类
AppCompatDelegate.java
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();
}
LayoutInflater我们都很熟悉的布局填充类,解析xml转化成view
LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
...
...
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
//解析xml,根据标签创建对应的view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
return result;
}
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//根据标签,创建系统view,
View view = tryCreateView(parent, name, context, attrs);
...
...
if (view == null) {
try {
//如果为空就创建自定义View
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
....
....
return view;
}
可以看到如果 mFactory2不为空的话,那么就会调用 mFactory2 去创建View(mFactory2.onCreateView(parent, name, context, attrs)) . 这句结论很重要,我们往下看便知道。
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;
}
....
return view;
}
可以看到,最后是调用的 AppCompatViewInflater 的对象的 createView() 去创建 View.我感觉 AppCompatViewInflater 就是专门用来创建 View 的,面向对象的五大原则之一–单一职责原则.
AppCompatViewInflater 类非常重要,先来看看上面提到的 createView() 方法的源码:
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;
// We need to 'inject' our tint aware Views in place of the standard framework versions
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;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
...
...
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
return view;
}
我们随便看下AppCompatTextView ,AppCompatBackgroundHelper 和 AppCompatTextHelper 拿到了xml 中定义的属性的值之后,将其值赋值给控件.就是这么简单.
public AppCompatTextView(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
...
//处理背景工具
mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
//处理文本样式工具
mTextHelper = new AppCompatTextHelper(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();
...
mTextClassifierHelper = new AppCompatTextClassifierHelper(this);
}
至此,Android 的控件加载方式已全部剖析完毕
看了前面这么多乱七八糟的东西,好像跟换肤没啥关系??
不急,我们接着往下看
源码中可以通过拦截 View 创建过程, 替换一些基础的组件(比如 TextView -> AppCompatTextView ), 然后对一些特殊的属性(比如:background, textColor ) 做处理, 那我们是不是也可替换自己换肤SkinCompatTextView? 一语惊醒梦中人啊,我们也可以搞一个类似于 AppCompatViewInflater 的控件加载器啊,我们也可以设置自己mFactory2 ,相当于创建View的过程由我们接手.既然我们接手了,那岂不是对所有控件都可以为所欲为????
Android-skin-support换肤原理详解
明白了原理,我们是不是可以自己写一个换肤框架,网上有现成开源换肤框架Android-skin-support,我们直接看下它们源码
https://github.com/ximsfei/Android-skin-support
初始化
SkinCompatManager.withoutActivity(application)
.addInflater(new SkinAppCompatViewInflater());
首先我们从库的初始化处着手,这里将 Application 传入,又添加了一个 SkinAppCompatViewInflater,SkinAppCompatViewInflater 其实就是用来创建换肤 View 的,和系统的 AppCompatViewInflater 差不多.我们来看看 withoutActivity(application) 做了什么.
//SkinCompatManager.java
public static SkinCompatManager withoutActivity(Application application) {
init(application);
SkinActivityLifecycle.init(application);
return sInstance;
}
//SkinActivityLifecycle.java
public static SkinActivityLifecycle init(Application application) {
if (sInstance == null) {
synchronized (SkinActivityLifecycle.class) {
if (sInstance == null) {
sInstance = new SkinActivityLifecycle(application);
}
}
}
return sInstance;
}
private SkinActivityLifecycle(Application application) {
//就是这里,注册了ActivityLifecycleCallbacks,可以监听所有Activity的生命周期
application.registerActivityLifecycleCallbacks(this);
//这个方法稍后看
installLayoutFactory(application);
SkinCompatManager.getInstance().addObserver(getObserver(application));
}
可以看到,初始化时在 SkinActivityLifecycle 中其实就注册了 ActivityLifecycleCallbacks,来监听 app 所有 Activity 的生命周期.
来看看 SkinActivityLifecycle 中监听到 Activity 的 onCreate() 方法时都干了什么
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//判断是否允许换肤
if (isContextSkinEnable(activity)) {
//初始化自己换肤工厂
installLayoutFactory(activity);
updateWindowBackground(activity);
if (activity instanceof SkinCompatSupportable) {
((SkinCompatSupportable) activity).applySkin();
}
}
}
//设置自己View工厂
private void installLayoutFactory(Context context) {
try {
LayoutInflater layoutInflater = LayoutInflater.from(context);
LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context));
} catch (Throwable e) {
Slog.i("SkinActivity", "A factory has already been set on this LayoutInflater");
}
}
可以看到 SkinCompatDelegate 是一个 SkinCompatViewInflater 的委托.
当系统需要创建 View 的时候,就会回调 SkinCompatDelegate 的 @Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,因为前面设置了 LayoutInflater 的 Factory 为 SkinCompatDelegate. 然后 SkinCompatDelegate 将创建 View 的工作交给 SkinCompatViewInflater去处理(也是和我们上面看的系统一模一样).
public class SkinCompatDelegate implements LayoutInflater.Factory2 {
...
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mSkinCompatViewInflater == null) {
mSkinCompatViewInflater = new SkinCompatViewInflater();
}
List<SkinWrapper> wrapperList = SkinCompatManager.getInstance().getWrappers();
for (SkinWrapper wrapper : wrapperList) {
Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs);
if (wrappedContext != null) {
context = wrappedContext;
}
}
return mSkinCompatViewInflater.createView(parent, name, context, attrs);
}
}
来看看 SkinCompatViewInflater 是如何创建 View 的
SkinCompatViewInflater.java
public View createView(Context context, String name, AttributeSet attrs) {
View view = createViewFromFV(context, name, attrs);
if (view == null) {
view = createViewFromV7(context, name, attrs);
}
return view;
}
private View createViewFromFV(Context context, String name, AttributeSet attrs) {
View view = null;
if (name.contains(".")) {
return null;
}
switch (name) {
case "View":
view = new SkinCompatView(context, attrs);
break;
case "LinearLayout":
view = new SkinCompatLinearLayout(context, attrs);
break;
case "RelativeLayout":
view = new SkinCompatRelativeLayout(context, attrs);
break;
case "FrameLayout":
view = new SkinCompatFrameLayout(context, attrs);
break;
case "TextView":
view = new SkinCompatTextView(context, attrs);
break;
case "ImageView":
view = new SkinCompatImageView(context, attrs);
break;
case "Button":
view = new SkinCompatButton(context, attrs);
break;
case "EditText":
view = new SkinCompatEditText(context, attrs);
break;
case "Spinner":
view = new SkinCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new SkinCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new SkinCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new SkinCompatRadioButton(context, attrs);
break;
case "RadioGroup":
view = new SkinCompatRadioGroup(context, attrs);
break;
case "CheckedTextView":
view = new SkinCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new SkinCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new SkinCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new SkinCompatSeekBar(context, attrs);
break;
case "ProgressBar":
view = new SkinCompatProgressBar(context, attrs);
break;
case "ScrollView":
view = new SkinCompatScrollView(context, attrs);
break;
default:
break;
}
return view;
}
private View createViewFromV7(Context context, String name, AttributeSet attrs) {
View view = null;
switch (name) {
case "androidx.appcompat.widget.Toolbar":
view = new SkinCompatToolbar(context, attrs);
break;
default:
break;
}
return view;
}
??? 这不就是之前我们在 Android 源码中看过的代码吗?几乎是一模一样。我们在这里将 View 的创建拦截,然后创建自己的控件。既然是我们自己创建的控件,那想怎么换肤还不容易?
我们看一下 SkinCompatTextView 的源码
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
private SkinCompatTextHelper mTextHelper;
private SkinCompatBackgroundHelper mBackgroundTintHelper;
public SkinCompatTextView(Context context) {
this(context, null);
}
public SkinCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = SkinCompatTextHelper.create(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
}
@Override
public void applySkin() {
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySkin();
}
if (mTextHelper != null) {
mTextHelper.applySkin();
}
}
执行换肤方法applySkin,里面SkinCompatVectorResources来判断是用换肤资源的SkinCompatResources,还是系统自带AppCompatResources,来达到换肤效果
如何加载皮肤资源包?
其实皮肤包就是一个 apk,只不过里面没有任何代码,只有一些需要换肤的资源或者颜色什么的.而且这些资源的名称必须和当前 app 中的资源名称是一致的,才能替换. 需要什么皮肤资源,直接去皮肤包里面去拿就好了.
使用方式
SkinCompatManager.getInstance().loadSkin("night.skin", null, CustomSDCardLoader.SKIN_LOADER_STRATEGY_SDCARD);
loadSkin() 启动一个异步线程SkinLoadTask加载换肤资源
/**
* 加载皮肤包.
*
* @param skinName 皮肤包名称.
* @param listener 皮肤包加载监听.
* @param strategy 皮肤包加载策略.
* @return
*/
public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);
if (loaderStrategy == null) {
return null;
}
return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);
}
@Override
protected String doInBackground(String... params) {
synchronized (mLock) {
try {
if (params.length == 1) {
//加载资源
String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
return params[0];
}
SkinCompatResources.getInstance().reset();
return null;
}
//策略加载,我们选择SkinSDCardLoader来看下
public String loadSkinInBackground(Context context, String skinName) {
if (TextUtils.isEmpty(skinName)) {
return skinName;
}
String skinPkgPath = getSkinPath(context, skinName);
if (SkinFileUtils.isFileExists(skinPkgPath)) {
String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);
Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
if (resources != null && !TextUtils.isEmpty(pkgName)) {
SkinCompatResources.getInstance().setupSkin(
resources,
pkgName,
skinName,
this);
return skinName;
}
}
return null;
}
获取皮肤资源resources
/**
* 获取皮肤包包名.
*
* @param skinPkgPath sdcard中皮肤包路径.
* @return
*/
public String getSkinPackageName(String skinPkgPath) {
PackageManager mPm = mAppContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
return info.packageName;
}
/**
* 获取皮肤包资源{@link Resources}.
*
* @param skinPkgPath sdcard中皮肤包路径.
* @return
*/
@Nullable
public Resources getSkinResources(String skinPkgPath) {
try {
PackageInfo packageInfo = mAppContext.getPackageManager().getPackageArchiveInfo(skinPkgPath, 0);
packageInfo.applicationInfo.sourceDir = skinPkgPath;
packageInfo.applicationInfo.publicSourceDir = skinPkgPath;
Resources res = mAppContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
Resources superRes = mAppContext.getResources();
return new Resources(res.getAssets(), superRes.getDisplayMetrics(), superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
SkinCompatTextView调用换肤方法applySkin()
@Override
public void applySkin() {
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySkin();
}
if (mTextHelper != null) {
mTextHelper.applySkin();
}
}
来看下SkinCompatBackgroundHelper 执行applySkin方法
@Override
public void applySkin() {
mBackgroundResId = checkResourceId(mBackgroundResId);
if (mBackgroundResId == INVALID_ID) {
return;
}
//获取背景图片,可以是APP本身资源,也可以是换肤资源
Drawable drawable = SkinCompatVectorResources.getDrawableCompat(mView.getContext(), mBackgroundResId);
if (drawable != null) {
int paddingLeft = mView.getPaddingLeft();
int paddingTop = mView.getPaddingTop();
int paddingRight = mView.getPaddingRight();
int paddingBottom = mView.getPaddingBottom();
ViewCompat.setBackground(mView, drawable);
mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
}
SkinCompatVectorResources根据类型区分是用换肤资源SkinCompatResources,还是App本身的资源AppCompatResources
private Drawable getSkinDrawableCompat(Context context, int resId) {
if (AppCompatDelegate.isCompatVectorFromResourcesEnabled()) {
if (!SkinCompatResources.getInstance().isDefaultSkin()) {
try {
return SkinCompatDrawableManager.get().getDrawable(context, resId);
} catch (Exception e) {
e.printStackTrace();
}
}
// SkinCompatDrawableManager.get().getDrawable(context, resId) 中会调用getSkinDrawable等方法。
// 这里只需要拦截使用默认皮肤的情况。
if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
if (colorStateList != null) {
return new ColorDrawable(colorStateList.getDefaultColor());
}
}
if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
if (drawable != null) {
return drawable;
}
}
Drawable drawable = SkinCompatResources.getInstance().getStrategyDrawable(context, resId);
if (drawable != null) {
return drawable;
}
return AppCompatResources.getDrawable(context, resId);
} else {
if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
if (colorStateList != null) {
return new ColorDrawable(colorStateList.getDefaultColor());
}
}
if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
if (drawable != null) {
return drawable;
}
}
Drawable drawable = SkinCompatResources.getInstance().getStrategyDrawable(context, resId);
if (drawable != null) {
return drawable;
}
if (!SkinCompatResources.getInstance().isDefaultSkin()) {
int targetResId = SkinCompatResources.getInstance().getTargetResId(context, resId);
if (targetResId != 0) {
return SkinCompatResources.getInstance().getSkinResources().getDrawable(targetResId);
}
}
return AppCompatResources.getDrawable(context, resId);
}
}
总结
总结流程如下:
1. 监听APP所有 Activity 的生命周期( registerActivityLifecycleCallbacks())
2. 在每个 Activity 的 onCreate()方法调用时setFactory(),设置创建View的工厂.将创建View的琐事交给 SkinCompatViewInflater 去处理.
3. 库中自己重写了系统的控件(比如 View 对应于库中的 SkinCompatView),实现换肤接口(接口里面只有一个 applySkin()方法),表示该控件是支持换肤的.并且将这些控件在创建之后收集起来,方便随时换肤.
4. 在库中自己写的控件里面去解析出一些特殊的属性(比如:background, textColor),并将其保存起来
5. 在切换皮肤的时候,遍历一次之前缓存的 View,调用其实现的接口方法 applySkin(),在applySkin() 中从皮肤资源(可以是从网络或者本地获取皮肤包)中获取资源.获取资源后设置其控件的 background 或 textColor 等,就可实现换肤.