一.背景
目前换肤实现大致有以下几种方式:
1.主题(Theme)切换,或后台下发color,image,icon(传统电商发方案);(代码侵入性高,开发量大,需要重启,不推荐)
2.依赖原生的UIModeManager,通过系统的uimode属性来应用不同的资源;(代码侵入性高,需要重启,不推荐)
3.热修复,插件化技术,创建独立Module作为换肤插件,利用ClassLoad,反射加载资源,插入新资源(代码侵入性高,开发难度大,需要重启,不推荐)
4.RRO技术,使用系统OverlayManager管理资源覆盖,支持换肤和定制化功能(需要系统权限,Activity重启生效)
5.Android-skin-support换肤框架(方案全面,代码侵入性小,Activity不重启,Compose无法支持,高版本需要改代码)
源码地址:https://github.com/ximsfei/Android-skin-support
6.Compose (较XML实现容易点,Activity不重启,学习成本高,代码侵入性高,跟XML兼容性差)
针对方案5进行原理解析,一句话概述:
通过LayoutInflater调用inflate方法加载XML布局,在inflate方法中有一个createViewFromTag,再根据LayoutInflater当中Factory的接口类型(Factory or Factory2)调用CreateView方法加载,其中通过“name”可以得到加载的控件Tag,替换相关的控件,再通过AttributeSet得到控件的全部属性最后再切换背景颜色
本文基于androidx.appcompat:1.6.1版本 Android API 34 (Android14) skin-support:4.0.5 适配android14改进版分析
二.AppCompatActivity实现
预备知识,看看AppCompatActivity的实现,这对于之后的理解框架原理非常有用.
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
private AppCompatDelegate mDelegate;
@Override
public AppCompatActivity() {
super();
initDelegate();//从这里开始
}
private void initDelegate() {
// TODO: Directly connect AppCompatDelegate to SavedStateRegistry
getSavedStateRegistry().registerSavedStateProvider(DELEGATE_TAG,
new SavedStateRegistry.SavedStateProvider() {
@NonNull
@Override
public Bundle saveState() {
Bundle outState = new Bundle();
getDelegate().onSaveInstanceState(outState);
return outState;
}
});
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(@NonNull Context context) {
//ComponentActivity在onCreate方法中会调用 mContextAwareHelper.dispatchOnContextAvailable(this); 的方法 从而调用到这个Listener的方法
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();//重点
delegate.onCreate(getSavedStateRegistry()
.consumeRestoredStateForKey(DELEGATE_TAG));
}
});
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onStart() {
super.onStart();
getDelegate().onStart();
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
......
}
androidx.activity:1.6.1 的 ComponentActivity 是AppCompatActivity的父类的父类
public interface OnContextAvailableListener {
/**
* Called when the {@link ContextAware} object this listener was added to is associated to a
* {@link Context}.
*
* @param context The {@link Context} the {@link ContextAware} object is now associated with.
*/
void onContextAvailable(@NonNull Context context);
}
ComponentActivity的 addOnContextAvailableListener 方法添加了 OnContextAvailableListener 在onCreate() 方法中调用了ContextAwareHelper.dispatchOnContextAvailable()方法
public class ComponentActivity extends androidx.core.app.ComponentActivity implements //省略...
{
@OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// Restore the Saved State first so that it is available to
// OnContextAvailableListener instances
mSavedStateRegistryController.performRestore(savedInstanceState);
//重点 这里做了调用
mContextAwareHelper.dispatchOnContextAvailable(this);
super.onCreate(savedInstanceState);
ReportFragment.injectIfNeededIn(this);
if (BuildCompat.isAtLeastT()) {
mOnBackPressedDispatcher.setOnBackInvokedDispatcher(
Api33Impl.getOnBackInvokedDispatcher(this)
);
}
if (mContentLayoutId != 0) {
setContentView(mContentLayoutId);
}
}
@Override
public final void addOnContextAvailableListener(@NonNull OnContextAvailableListener listener) {
mContextAwareHelper.addOnContextAvailableListener(listener);
}
}
ContextAwareHelper 的dispatchOnContextAvailable方法调用了OnContextAvailableListener的onContextAvailable()方法
public final class ContextAwareHelper {
public void dispatchOnContextAvailable(@NonNull Context context) {
mContext = context;
for (OnContextAvailableListener listener : mListeners) {
listener.onContextAvailable(context);
}
}
}
我们看到有一个AppCompatDelegate,这玩意儿有什么用呢?查阅资料得知,它是Activity的委托,AppCompatActivity将大部分生命周期都委托给了AppCompatDelegate,这点可从上面的源码中可以看出.接着我们查看AppCompatDelegate的源码,发现其类注释也是这么写的.
接下来,我们看看AppCompatDelegate的创建
AppCompatActivity.java 抽象类
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
AppCompatDelegate.java 的实现类 AppCompatDelegateImpl.java
//AppCompatDelegate.java
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return new AppCompatDelegateImpl(context, window, callback);
}
//AppCompatDelegateImpl.java
private AppCompatDelegateImpl(Context context, Window window, AppCompatCallback callback,
Object host) {
//不重要,省略
....
}
因为上面的AppCompatActivity.java 中的 initDelegate()的监听当中调用了AppCompatDelegate 的delegate.installViewFactory();
AppCompatDelegate是抽象类,方法在子类AppCompatDelegateImpl里面实现的.看一下源码.
AppCompatDelegateImpl.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
//layoutInflater只能加载一次
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat.setFactory2(layoutInflater, this);
最终是调用的LayoutInflater的setFactory2()
方法,看看实现
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
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);
}
}
这里有个小细节,Factory2只能被设置一次,设置完成后mFactorySet属性就为true,下一次设置时被直接抛异常.
那么Factory2有什么用呢?看看其实现
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
它是一个接口,只有一个方法,看起来是用来创建View的.到底是不是呢?答案稍后揭晓.
AppCompatActivity设置了一个委托,并给LayoutInflater设置了一个mFactory2.现在知道这个就够了.
三.Android创建View全过程解析
下面先看看Android是如何根据xml布局创建一个View的
平时我们最常使用的Activity中的setContentView()设置布局ID,看看Activity中的实现,
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
调用的是Window中的setContentView(),而Window只有一个实现类,就是PhoneWindow.看看setContentView()实现
@Override
public void setContentView(int layoutResID) {
...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
看到了今天的主角mLayoutInflater,mLayoutInflater是在PhoneWindow的构造方法中初始化的.用mLayoutInflater去加载这个布局(layoutResID).点进去看看实现
LayoutInflater.java
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) + ")");
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
可以看到将用布局创建了一个Xml解析器,然后进行解析
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
}
其实里面我觉得就只有2句关键代码,就是去根据xml写的东西去构建View嘛.rInflateChildren()
最后还是会去调用createViewFromTag()
方法,这里是为了先创建出rootView,然后将子View添加进rootView.
来看看createViewFromTag()的实现
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
View view = tryCreateView(parent, name, context, attrs);
...
//调用tryCreateView获得View后返回
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, 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;
}
...
return view;
}
可以看到如果mFactory2不为空的话,那么就会调用mFactory2去创建View(mFactory2.onCreateView(parent, name, context, attrs)) . 这句结论很重要.前面的答案已揭晓.如果设置了mFactory2就会用mFactory2去创建View.而mFactory2在上面的AppCompatDelegateImpl
的installViewFactory()
中已设置好了的,其实mFactory2就是AppCompatDelegateImpl,其实现了Factory2的接口并且被设置进LayoutInflater
来看看AppCompatDelegateImpl的createView()的具体实现
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if (viewInflaterClassName == null) {
// Set to null (the default in all AppCompat themes). Create the base inflater
// (no reflection)
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class<?> viewInflaterClass =
mContext.getClassLoader().loadClass(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new AppCompatViewInflater();
}
}
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
if (mLayoutIncludeDetector == null) {
mLayoutIncludeDetector = new LayoutIncludeDetector();
}
if (mLayoutIncludeDetector.detect(attrs)) {
// The view being inflated is the root of an <include>d view, so make sure
// we carry over any themed context.
inheritContext = true;
} else {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
可以看到,最后是调用的AppCompatViewInflater的对象的createView()
去创建View.我感觉AppCompatViewInflater就是专门用来创建View的,面向对象的五大原则之一–单一职责原则.
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;
// 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 = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
case "ToggleButton":
view = new AppCompatToggleButton(context, attrs);
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);
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
可以看到如果在xml中写了一个TextView
控件,其实是通过我们写的控件名称判断是什么控件,然后去new的方式创建出来的,并且new的不是TextView,而是AppCompatTextView
.其他的一些系统控件也是这么new出来的.
但是,有个问题,如果我在xml布局中不是写的这些控件(比如RecyclerView,自定义控件等),那么怎么创建view呢?注意到代码中如果执行完switch块之后view为空(说明不是上面列的那些控件),调用了createViewFromTag()
方法.来看看实现
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createViewByPrefix(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
首先是判断一下是否是系统控件,怎么判断呢?通过判断控件名称中是否包含’.‘来判断.系统控件在xml布局中声明时是不需要加具体包名的,比如ProgressBar,所以没有’.‘的肯定是系统控件.那么有’.'的就是自定义控件或者一些特殊的系统控件了(比如android.support.v7.widget.SwitchCompat).
有个小疑问?为什么系统控件可以在布局中声明时不加包名,而自定义控件必须要加包名呢?
其实是系统的控件大多放在sClassPrefixList定义的这些包名下,所以待会儿可以通过拼接的方式将控件的位置找到
源码中创建系统控件和非系统控件分开去创建.其实方法都是同一个,只是一个传了前缀,一个没有传前缀.来看看创建方法实现
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();
private View createViewByPrefix(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
//这里的sConstructorMap是用来做缓存的,如果之前已经创建,则会将构造方法缓存起来,下次直接用
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//通过classLoader去寻找该class,这里的classLoader其实是PathClassLoader
//看到没? (prefix + name)这种直接将前缀与名称拼接的方式就可以将View的位置拼接出来
//然后其他的全类名的View就不需要拼接前缀
Class<? extends View> clazz = Class.forName(
prefix != null ? (prefix + name) : name,
false,
context.getClassLoader()).asSubclass(View.class);
//获取构造方法
constructor = clazz.getConstructor(sConstructorSignature);
//缓存构造方法
sConstructorMap.put(name, constructor);
}
//设置构造方法可访问
constructor.setAccessible(true);
//通过构造方法new一个View对象出来
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
其实这个创建View就是利用ClassLoader去寻找这个类的class,然后获取其{ Context.class, AttributeSet.class}
这个构造方法,然后通过反射将View创建出来.具体逻辑在代码中已标明注释.
至此,Android的控件加载方式已全部剖析完毕.
其中,有一个小细节,刚刚为了流程顺畅没有在上面说到,上面有一段构建View(根据控件名称创建AppCompatXX控件)的代码如下:
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;
}
我们来随便看一下控件的源码,比如AppCompatTextView
,其他的AppCompatXX控件实现都是差不多的.
public class AppCompatTextView extends TextView implements TintableBackgroundView,
AutoSizeableTextView {
//这3个是关键类
private final AppCompatBackgroundHelper mBackgroundTintHelper;
private final AppCompatTextHelper mTextHelper;
private final AppCompatTextClassifierHelper mTextClassifierHelper;
public AppCompatTextView(Context context) {
this(context, null);
}
public AppCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = AppCompatTextHelper.create(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();
}
......
class AppCompatBackgroundHelper {
......
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
R.styleable.ViewBackgroundHelper, defStyleAttr, 0);
try {
if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {
//获取android:background 背景的资源id
mBackgroundResId = a.getResourceId(
R.styleable.ViewBackgroundHelper_android_background, -1);
ColorStateList tint = mDrawableManager
.getTintList(mView.getContext(), mBackgroundResId);
if (tint != null) {
setInternalBackgroundTint(tint);
}
}
if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTint)) {
//获取android:backgroundTint
ViewCompat.setBackgroundTintList(mView,
a.getColorStateList(R.styleable.ViewBackgroundHelper_backgroundTint));
}
if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTintMode)) {
//获取android:backgroundTintMode
ViewCompat.setBackgroundTintMode(mView,
DrawableUtils.parseTintMode(
a.getInt(R.styleable.ViewBackgroundHelper_backgroundTintMode, -1),
null));
}
} finally {
a.recycle();
}
}
}
class AppCompatTextHelper {
......
@SuppressLint("NewApi")
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
final Context context = mView.getContext();
final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
//...省略获取属性
if (textColor != null) {
mView.setTextColor(textColor);
}
if (textColorHint != null) {
mView.setHintTextColor(textColorHint);
}
if (textColorLink != null) {
mView.setLinkTextColor(textColorLink);
}
if (!hasPwdTm && allCapsSet) {
setAllCaps(allCaps);
}
if (mFontTypeface != null) {
mView.setTypeface(mFontTypeface, mStyle);
}
mAutoSizeTextHelper.loadFromAttributes(attrs, defStyleAttr);
if (PLATFORM_SUPPORTS_AUTOSIZE) {
// Delegate auto-size functionality to the framework implementation.
if (mAutoSizeTextHelper.getAutoSizeTextType()
!= TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) {
final int[] autoSizeTextSizesInPx =
mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
if (autoSizeTextSizesInPx.length > 0) {
if (mView.getAutoSizeStepGranularity() != AppCompatTextViewAutoSizeHelper
.UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
// Configured with granularity, preserve details.
mView.setAutoSizeTextTypeUniformWithConfiguration(
mAutoSizeTextHelper.getAutoSizeMinTextSize(),
mAutoSizeTextHelper.getAutoSizeMaxTextSize(),
mAutoSizeTextHelper.getAutoSizeStepGranularity(),
TypedValue.COMPLEX_UNIT_PX);
} else {
mView.setAutoSizeTextTypeUniformWithPresetSizes(
autoSizeTextSizesInPx, TypedValue.COMPLEX_UNIT_PX);
}
}
}
}
}
这里又一次体现了单一职责原则,系统将背景相关的交给AppCompatBackgroundHelper去处理,将文字相关的交给AppCompatTextHelper处理.
AppCompatBackgroundHelper和AppCompatTextHelper拿到了xml中定义的属性的值之后,将其值赋值给控件.就是这么简单.
四.换肤原理详细解析
1.上文预备知识与换肤的关系
源码中可以通过拦截View创建过程, 替换一些基础的组件(比如TextView -> AppCompatTextView
), 然后对一些特殊的属性(比如:background, textColor) 做处理, 那我们将这种思想拿到换肤框架中来使用
2.源码一,创建控件全过程
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);
//在SkinActivityLifecycle初始化创建的时候,直接将Factory设置成三方库里面的
installLayoutFactory(application);
SkinCompatManager.getInstance().addObserver(getObserver(application));
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (isContextSkinEnable(activity)) {
installLayoutFactory(activity);
updateWindowBackground(activity);
if (activity instanceof SkinCompatSupportable) {
((SkinCompatSupportable) activity).applySkin();
}
}
}
可以看到,初始化时在SkinActivityLifecycle中其实就注册了ActivityLifecycleCallbacks,现在我们可以监听app所有Activity的生命周期.application的onCreate()一开始就给LayoutInflater设置了Factory2
//SkinActivityLifecycle.java
/**
* 设置Factory(创建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");
}
}
在我们的Activity创建的时候,首先判断一下是否需要换肤,需要换肤才去搞.
下面我们来看看setFactory()
的第二个参数创建过程,第二个参数其实是一个创建View的工厂.
//SkinActivityLifecycle.java
private SkinCompatDelegate getSkinDelegate(Context context) {
if (mSkinDelegateMap == null) {
mSkinDelegateMap = new WeakHashMap<>();
}
SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
if (mSkinDelegate == null) {
mSkinDelegate = SkinCompatDelegate.create(context);
mSkinDelegateMap.put(context, mSkinDelegate);
}
return mSkinDelegate;
}
//SkinCompatDelegate.java
public class SkinCompatDelegate implements LayoutInflater.Factory2 {
private final Context mContext;
//主角 在这里 在这里!!!
private SkinCompatViewInflater mSkinCompatViewInflater;
private List<WeakReference<SkinCompatSupportable>> mSkinHelpers = new ArrayList<>();
private SkinCompatDelegate(Context context) {
mContext = context;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createView(parent, name, context, attrs);
if (view == null) {
return null;
}
if (view instanceof SkinCompatSupportable) {
mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
}
return view;
}
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);
}
public static SkinCompatDelegate create(Context context) {
return new SkinCompatDelegate(context);
}
public void applySkin() {
if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
for (WeakReference ref : mSkinHelpers) {
if (ref != null && ref.get() != null) {
((SkinCompatSupportable) ref.get()).applySkin();
}
}
}
}
}
可以看到SkinCompatDelegate是一个SkinCompatViewInflater的委托.这里其实和系统的AppCompatDelegateImpl很类似.
当系统需要创建View的时候,就会回调SkinCompatDelegate的@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
方法,因为前面设置了LayoutInflater的Factory为SkinCompatDelegate. 然后SkinCompatDelegate将创建View的工作交给SkinCompatViewInflater去处理(也是和系统一模一样).
来看看SkinCompatViewInflater是如何创建View的
public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
View view = createViewFromHackInflater(context, name, attrs);
if (view == null) {
view = createViewFromInflater(context, name, attrs);
}
if (view == null) {
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
View view = null;
//这里的SkinLayoutInflater(我理解为控件创建器)就是我们之前在初始化时设置的SkinAppCompatViewInflater
//当然,SkinLayoutInflater可以有多个
for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
view = inflater.createView(context, name, attrs);
if (view == null) {
continue;
} else {
break;
}
}
return view;
}
//这个方法和系统的完全一模一样
public View createViewFromTag(Context context, String name, AttributeSet attrs) {
if ("view".equals(name)) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
//自定义控件
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createView(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
可以看到,这些实现其实是和系统的实现是差不多的.原理已在上面的预备知识中给出.
这里也有不同的地方,createViewFromInflater()
方法中利用了我们在初始化库时设置的SkinLayoutInflater(我觉得是控件创造器)去创建view.
为什么要在SkinCompatViewInflater还要细化,还需要交由更细的SkinLayoutInflater来处理呢?我觉得是因为方便扩展,库中给出了几个SkinLayoutInflater,有SkinAppCompatViewInflater(基础控件构建器)、SkinMaterialViewInflater(material design控件构造器)、SkinConstraintViewInflater(ConstraintLayout构建器)、SkinCardViewInflater(CardView v7构建器)。
由于初始化时我们设置的是SkinAppCompatViewInflater,其他的构建器都是类似的原理.我们就来看看
//SkinAppCompatViewInflater.java
@Override
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;
......
default:
break;
}
return view;
}
private View createViewFromV7(Context context, String name, AttributeSet attrs) {
View view = null;
switch (name) {
case "android.support.v7.widget.Toolbar":
view = new SkinCompatToolbar(context, attrs);
break;
default:
break;
}
return view;
}
我们在这里将View的创建拦截,然后创建自己的控件。既然是我们自己创建的控件,想干啥还不容易么?
我们看一下SkinCompatTextView
的源码
//SkinCompatTextView.java
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();
}
}
}
还是那套经典的操作,将background相关的属性交给SkinCompatBackgroundHelper去处理,将textColor相关的操作交给SkinCompatTextHelper去处理。与源码中一模一样。
3. 源码二,从皮肤包加载皮肤
其实皮肤包就是一个apk,只不过里面没有任何代码,只有一些需要换肤的资源或者颜色什么的.而且这些资源的名称必须和当前app中的资源名称是一致的,才能替换. 需要什么皮肤资源,直接去皮肤包里面去拿就好了.
使用方式
SkinCompatManager.getInstance().loadSkin("night.skin", null, CustomSDCardLoader.SKIN_LOADER_STRATEGY_SDCARD);
来吧,我们进入loadSkin()方法看一下:
/**
* 加载皮肤包.
* @param skinName 皮肤包名称.
* @param listener 皮肤包加载监听.
* @param strategy 皮肤包加载策略.
*/
public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
//加载策略 分为好几种:从SD卡中加载皮肤,从assets文件中加载皮肤等等
SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);
if (loaderStrategy == null) {
return null;
}
return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);
}
可以看到SkinLoadTask应该是一个AsyncTask,然后在后台去解析这个皮肤包.既然是AsyncTask,那肯定看doInBackground()
方法咯
我们来看看SkinLoadTask的doInBackground()
//SkinLoadTask.java
@Override
protected String doInBackground(String... params) {
......
try {
if (params.length == 1) {
//根据加载策略去后台加载皮肤
String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
if (TextUtils.isEmpty(skinName)) {
SkinCompatResources.getInstance().reset(mStrategy);
}
return params[0];
}
} catch (Exception e) {
e.printStackTrace();
}
SkinCompatResources.getInstance().reset();
return null;
}
//加载策略 随便挑一个吧 SkinSDCardLoader.java 从SD卡加载皮肤
@Override
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 resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
if (resources != null && !TextUtils.isEmpty(pkgName)) {
SkinCompatResources.getInstance().setupSkin(
resources,
pkgName,
skinName,
this);
return skinName;
}
}
return null;
}
//SkinCompatManager.java
//获取皮肤包包名.
public String getSkinPackageName(String skinPkgPath) {
PackageManager mPm = mAppContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
return info.packageName;
}
//获取皮肤包资源{@link Resources}.
@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;
}
大概就是去子线程获取皮肤包的包名和Resources(后面我们需要获取皮肤包中的颜色或者资源时需要通过这个进行获取).
SkinCompatResources.getInstance().setupSkin()
方法中就是将这些从皮肤包中加载的Resources,包名,皮肤名,加载策略全部存下来.有了这些东西,待会儿就能取皮肤包里面的资源了.
库中定义的控件都是实现了SkinCompatSupportable接口的,方便控制换肤。比如SkinCompatTextView的applySkin()方法中调用了BackgroundTintHelper和TextHelper的applySkin()
方法,就是说换肤时会去动态的更换背景或文字颜色什么的。我们来看看 mBackgroundTintHelper.applySkin()
的实现
//SkinCompatBackgroundHelper.java
@Override
public void applySkin() {
//该控件是否有背景 检测
mBackgroundResId = checkResourceId(mBackgroundResId);
if (mBackgroundResId == INVALID_ID) {
return;
}
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);
}
}
就是获取drawable然后给view设置背景嘛.关键在于这里的获取drawable是怎么实现的.来看看具体实现
//SkinCompatVectorResources.java
private Drawable getSkinDrawableCompat(Context context, int resId) {
//当前是非默认皮肤
if (!SkinCompatResources.getInstance().isDefaultSkin()) {
try {
return SkinCompatDrawableManager.get().getDrawable(context, resId);
} catch (Exception e) {
e.printStackTrace();
}
}
......
return AppCompatResources.getDrawable(context, resId);
}
//SkinCompatDrawableManager.java
public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
return getDrawable(context, resId, false);
}
Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
boolean failIfNotKnown) {
//检查Drawable是否能被正确解码
checkVectorDrawableSetup(context);
Drawable drawable = loadDrawableFromDelegates(context, resId);
if (drawable == null) {
drawable = createDrawableIfNeeded(context, resId);
}
if (drawable == null) {
//这里是关键
drawable = SkinCompatResources.getDrawable(context, resId);
}
if (drawable != null) {
// Tint it if needed
drawable = tintDrawable(context, resId, failIfNotKnown, drawable);
}
if (drawable != null) {
// See if we need to 'fix' the drawable
SkinCompatDrawableUtils.fixDrawable(drawable);
}
return drawable;
}
最后是调用的SkinCompatDrawableManager去获取drawable,我发现这个SkinCompatDrawableManager和系统的AppCompatDrawableManager一模一样.
唯一不同点是上面的31行处drawable = SkinCompatResources.getDrawable(context, resId);
,在这里我们去创建drawable时就使用SkinCompatResources去获取.
还记得SkinCompatResources么?就是上面我们获取了皮肤包的信息后,将信息全部保存到了这个类里面.
//SkinCompatResources.java
//皮肤的Resources可以通过它来获取皮肤里面的资源
private Resources mResources;
//皮肤包名
private String mSkinPkgName = "";
//皮肤名
private String mSkinName = "";
//加载策略
private SkinCompatManager.SkinLoaderStrategy mStrategy;
//是默认皮肤?
private boolean isDefaultSkin = true;
public static Drawable getDrawable(Context context, int resId) {
return getInstance().getSkinDrawable(context, resId);
}
/**
* 通过id获取皮肤中的drawable资源
* @param context Context
* @param resId 资源id
*/
private Drawable getSkinDrawable(Context context, int resId) {
//是否有皮肤颜色缓存
if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
if (colorStateList != null) {
return new ColorDrawable(colorStateList.getDefaultColor());
}
}
//是否有皮肤drawable缓存
if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
if (drawable != null) {
return drawable;
}
}
//加载策略非空 可以通过加载策略去加载drawable,开发者可自定义
if (mStrategy != null) {
Drawable drawable = mStrategy.getDrawable(context, mSkinName, resId);
if (drawable != null) {
return drawable;
}
}
//非默认皮肤 去皮肤中加载资源
if (!isDefaultSkin) {
//皮肤资源id 这是我们的目标
int targetResId = getTargetResId(context, resId);
if (targetResId != 0) {
//根据id通过皮肤的Resources去获取drawable
return mResources.getDrawable(targetResId);
}
}
return context.getResources().getDrawable(resId);
}
大概意思就是有缓存资源(之前在皮肤包中取过这个resId的资源)则取缓存资源,没有缓存则根据resId通过皮肤的Resources去获取drawable.
到此,已经获取到皮肤包中的drawable,也就是实现了动态的加载皮肤包中的图片,shape等等的资源,加载皮肤中的颜色的过程也是类似的,这里就不多介绍了.
4.简单总结一下原理(本文精髓)
- 监听APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
- 在每个Activity的onCreate()方法调用时setFactory(),设置创建View的工厂.将创建View的琐事交给SkinCompatViewInflater去处理.
- 库中自己重写了系统的控件(比如View对应于库中的SkinCompatView),实现换肤接口(接口里面只有一个applySkin()方法),表示该控件是支持换肤的.并且将这些控件在创建之后收集起来,方便随时换肤.
- 在库中自己写的控件里面去解析出一些特殊的属性(比如:background, textColor),并将其保存起来
- 在切换皮肤的时候,遍历一次之前缓存的View,调用其实现的接口方法applySkin(),在applySkin()中从皮肤资源(可以是从网络或者本地获取皮肤包)中获取资源.获取资源后设置其控件的background或textColor等,就可实现换肤.