一.背景
公司業(yè)務上需要用到換膚.為了不重復造輪子,并且快速實現(xiàn)需求,并且求穩(wěn),,于是到Github上找了一個star數(shù)比較多的換膚框架-
Android-skin-support(一款用心去做的Android 換膚框架, 極低的學習成本, 極好的用戶體驗. 一行代碼就可以實現(xiàn)換膚, 你值得擁有!!!). 簡單了解之后,可以快速上手,并且侵入性很低.作為一名合格的程序員,當然需要了解其背后的原理才能算是真正的靈活運用.并且有bug的話,也能很快定位是哪里的問題,這對于公司的項目后期維護是非常有用的. 這里只講原理,具體的使用方式還是去看官方的文檔吧,源碼地址:https://github.com/ximsfei/Android-skin-support
二.AppCompatActivity實現(xiàn)
在開始之前,先來點預備知識吧,看看AppCompatActivity的實現(xiàn),這對于之后的理解框架原理非常有用.
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
@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);
}
......
}
我們看到有一個AppCompatDelegate,這玩意兒有什么用呢?查閱資料得知,它是Activity的委托,AppCompatActivity將大部分生命周期都委托給了AppCompatDelegate,這點可從上面的源碼中可以看出.接著我們查看AppCompatDelegate的源碼,發(fā)現(xiàn)其類注釋也是這么寫的.
接下來,我們看看AppCompatDelegate的創(chuàng)建
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
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
不同的API版本號使用的AppCompatDelegate是不一樣的,下面是類的繼承關(guān)系圖

因為上面的delegate.installViewFactory();其實是在AppCompatDelegateImplV9里面實現(xiàn)的.看一下源碼.
AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat.setFactory2(layoutInflater, this);最終是調(diào)用的LayoutInflater的setFactory2()方法,看看實現(xiàn)
/**
* 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);
}
}
這里有個小細節(jié),Factory2只能被設(shè)置一次,設(shè)置完成后mFactorySet屬性就為true,下一次設(shè)置時被直接拋異常.
那么Factory2有什么用呢?看看其實現(xiàn)
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);
}
它是一個接口,只有一個方法,看起來是用來創(chuàng)建View的.到達是不是呢?答案稍后揭曉.
AppCompatActivity設(shè)置了一個委托,并給LayoutInflater設(shè)置了一個mFactory2.現(xiàn)在知道這個就夠了.
三.Android創(chuàng)建View全過程解析
下面先看看Android是如何根據(jù)xml布局創(chuàng)建一個View的
平時我們最常使用的Activity中的setContentView()設(shè)置布局ID,看看Activity中的實現(xiàn),
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
調(diào)用的是Window中的setContentView(),而Window只有一個實現(xiàn)類,就是PhoneWindow.看看setContentView()實現(xiàn)
@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的構(gòu)造方法中初始化的.用mLayoutInflater去加載這個布局(layoutResID).點進去看看實現(xiàn)
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) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
可以看到將用布局創(chuàng)建了一個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句關(guān)鍵代碼,就是去根據(jù)xml寫的東西去構(gòu)建View嘛.rInflateChildren()最后還是會去調(diào)用createViewFromTag()方法,這里是為了先創(chuàng)建出rootView,然后將子View添加進rootView.
來看看createViewFromTag()的實現(xiàn)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
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);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
...
return view;
}
可以看到如果mFactory2不為空的話,那么就會調(diào)用mFactory2去創(chuàng)建View(mFactory2.onCreateView(parent, name, context, attrs)) . 這句結(jié)論很重要.前面的答案已揭曉.如果設(shè)置了mFactory2就會用mFactory2去創(chuàng)建View.而mFactory2在上面的AppCompatDelegateImplV9的installViewFactory()中已設(shè)置好了的,其實mFactory2就是AppCompatDelegateImplV9.
來看看createView()的具體實現(xiàn)
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
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 */
);
}
可以看到,最后是調(diào)用的AppCompatViewInflater的對象的createView()去創(chuàng)建View.我感覺AppCompatViewInflater就是專門用來創(chuàng)建View的,面向?qū)ο蟮奈宕笤瓌t之一--單一職責原則.
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 = 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);
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;
}
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;
}
可以看到如果在xml中寫了一個TextView控件,其實是通過我們寫的控件名稱判斷是什么控件,然后去new的方式創(chuàng)建出來的,并且new的不是TextView,而是AppCompatTextView.其他的一些系統(tǒng)控件也是這么new出來的.
但是,有個問題,如果我在xml布局中不是寫的這些控件(比如RecyclerView,自定義控件等),那么怎么創(chuàng)建view呢?注意到代碼中如果執(zhí)行完switch塊之后view為空(說明不是上面列的那些控件),調(diào)用了createViewFromTag()方法.來看看實現(xiàn)
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;
//這里判斷一下name(即在xml中寫的控件名稱)中是否含有'.'
//如果沒有那么肯定就是系統(tǒng)控件(比如ProgressBar,在布局中是不需要加ProgressBar的具體包名的)
//如果有那么就是自定義控件,或者是系統(tǒng)的控件(比如android.support.v7.widget.SwitchCompat)
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;
}
}
這里比較有意思,首先是判斷一下是否是系統(tǒng)控件,怎么判斷呢?通過判斷控件名稱中是否包含'.'來判斷.系統(tǒng)控件在xml布局中聲明時是不需要加具體包名的,比如ProgressBar,所以沒有'.'的肯定是系統(tǒng)控件.那么有'.'的就是自定義控件或者一些特殊的系統(tǒng)控件了(比如android.support.v7.widget.SwitchCompat).
有個小疑問?為什么系統(tǒng)控件可以在布局中聲明時不加包名,而自定義控件必須要加包名呢?
<img src="https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1532670081&di=a57c80745a7af4517f0e30a60564789e&src=http://07.imgmini.eastday.com/mobile/20171017/20171017045451_1d939f2d4f0edad71f85f1afb779ff88_4.jpeg" height=100></img>
其實是系統(tǒng)的控件大多放在sClassPrefixList定義的這些包名下,所以待會兒可以通過拼接的方式將控件的位置找到.隨便舉個例子,我們來看看哪些系統(tǒng)控件在android.widget.包下面

源碼中創(chuàng)建系統(tǒng)控件和非系統(tǒng)控件分開去創(chuàng)建.其實方法都是同一個,只是一個傳了前綴,一個沒有傳前綴.來看看創(chuàng)建方法實現(xiàn)
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();
private View createView(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
//這里的sConstructorMap是用來做緩存的,如果之前已經(jīng)創(chuàng)建,則會將構(gòu)造方法緩存起來,下次直接用
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
//看到?jīng)]? (prefix + name)這種直接將前綴與名稱拼接的方式就可以將View的位置拼接出來
//然后其他的全類名的View就不需要拼接前綴
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//獲取構(gòu)造方法
constructor = clazz.getConstructor(sConstructorSignature);
//緩存構(gòu)造方法
sConstructorMap.put(name, constructor);
}
//設(shè)置構(gòu)造方法可訪問
constructor.setAccessible(true);
//通過構(gòu)造方法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;
}
}
其實這個創(chuàng)建View就是利用ClassLoader去尋找這個類的class,然后獲取其{ Context.class, AttributeSet.class}這個構(gòu)造方法,然后通過反射將View創(chuàng)建出來.具體邏輯在代碼中已標明注釋.
至此,Android的控件加載方式已全部剖析完畢.
其中,有一個小細節(jié),剛剛為了流程順暢沒有在上面說到,上面有一段構(gòu)建View(根據(jù)控件名稱創(chuàng)建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控件實現(xiàn)都是差不多的.
public class AppCompatTextView extends TextView implements TintableBackgroundView,
AutoSizeableTextView {
//這2個是關(guān)鍵類
private final AppCompatBackgroundHelper mBackgroundTintHelper;
private final AppCompatTextHelper mTextHelper;
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();
// First read the TextAppearance style id
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.AppCompatTextHelper, defStyleAttr, 0);
final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1);
// Now read the compound drawable and grab any tints
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
mDrawableLeftTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0));
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
mDrawableTopTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0));
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
mDrawableRightTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0));
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
mDrawableBottomTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0));
}
a.recycle();
// PasswordTransformationMethod wipes out all other TransformationMethod instances
// in TextView's constructor, so we should only set a new transformation method
// if we don't have a PasswordTransformationMethod currently...
final boolean hasPwdTm =
mView.getTransformationMethod() instanceof PasswordTransformationMethod;
boolean allCaps = false;
boolean allCapsSet = false;
ColorStateList textColor = null;
ColorStateList textColorHint = null;
ColorStateList textColorLink = null;
// First check TextAppearance's textAllCaps value
if (ap != -1) {
a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.TextAppearance);
if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
allCapsSet = true;
allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
}
updateTypefaceAndStyle(context, a);
if (Build.VERSION.SDK_INT < 23) {
// If we're running on < API 23, the text color may contain theme references
// so let's re-set using our own inflater
if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
textColorHint = a.getColorStateList(
R.styleable.TextAppearance_android_textColorHint);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
textColorLink = a.getColorStateList(
R.styleable.TextAppearance_android_textColorLink);
}
}
a.recycle();
}
// Now read the style's values
a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TextAppearance,
defStyleAttr, 0);
if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
allCapsSet = true;
allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
}
if (Build.VERSION.SDK_INT < 23) {
// If we're running on < API 23, the text color may contain theme references
// so let's re-set using our own inflater
if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
textColorHint = a.getColorStateList(
R.styleable.TextAppearance_android_textColorHint);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
textColorLink = a.getColorStateList(
R.styleable.TextAppearance_android_textColorLink);
}
}
updateTypefaceAndStyle(context, a);
a.recycle();
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);
}
}
}
}
}
這里的這里,不得不說Android源碼真是讓在下佩服的五體投地,又一次體現(xiàn)了單一職責原則.你問我在哪里? 系統(tǒng)將背景相關(guān)的交給AppCompatBackgroundHelper去處理,將文字相關(guān)的交給AppCompatTextHelper處理.
AppCompatBackgroundHelper和AppCompatTextHelper拿到了xml中定義的屬性的值之后,將其值賦值給控件.就是這么簡單.
看到了這里,預備知識就介紹得差不多了,看了半天,你說的這些亂七八糟的東西與我的換膚有個毛關(guān)系啊?
<img src="http://olg7c0d2n.bkt.clouddn.com/18-7-27/77497119.jpg" width=100px height=100px></img>
請各位看官放下手中的磚頭,且聽貧道細細道來.
四.換膚原理詳細解析
1.上文預備知識與換膚的關(guān)系
源碼中可以通過攔截View創(chuàng)建過程, 替換一些基礎(chǔ)的組件(比如TextView -> AppCompatTextView), 然后對一些特殊的屬性(比如:background, textColor) 做處理, 那我們?yōu)槭裁床荒軐⑦@種思想拿到換膚框架中來使用呢?我擦,一語驚醒夢中人啊,老哥.我們也可以搞一個委托啊,我們也可以搞一個類似于AppCompatViewInflater的控件加載器啊,我們也可以設(shè)置mFactory2啊,相當于創(chuàng)建View的過程由我們接手.既然我們接手了,那豈不是對所有控件都可以為所欲為????那是當然啦. 既然都可以為所欲為了,那換個膚算什么,so easy.
2.源碼一,創(chuàng)建控件全過程
SkinCompatManager.withoutActivity(application)
.addInflater(new SkinAppCompatViewInflater());
首先我們從庫的初始化處著手,這里將Application傳入,又添加了一個SkinAppCompatViewInflater,SkinAppCompatViewInflater其實就是用來創(chuàng)建View的,和系統(tǒng)的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,可以監(jiān)聽所有Activity的生命周期
application.registerActivityLifecycleCallbacks(this);
//這個方法稍后看
installLayoutFactory(application);
SkinCompatManager.getInstance().addObserver(getObserver(application));
}
可以看到,初始化時在SkinActivityLifecycle中其實就注冊了ActivityLifecycleCallbacks,現(xiàn)在我們可以監(jiān)聽app所有Activity的生命周期.
來看看SkinActivityLifecycle中監(jiān)聽到Activity的onCreate()方法時干了什么
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//判斷是否需要換膚 這個可以外部初始化時控制
if (isContextSkinEnable(activity)) {
//在Activity創(chuàng)建的時候,直接將Factory設(shè)置成三方庫里面的
installLayoutFactory(activity);
//更新狀態(tài)欄顏色
updateStatusBarColor(activity);
//更新window背景顏色
updateWindowBackground(activity);
if (activity instanceof SkinCompatSupportable) {
((SkinCompatSupportable) activity).applySkin();
}
}
}
/**
* 設(shè)置Factory(創(chuàng)建View的工廠)
*/
private void installLayoutFactory(Context context) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
try {
//setFactory只能調(diào)用一次,用于設(shè)置Factory(創(chuàng)建View), 設(shè)置了Factory了mFactorySet就會是true
//如果需要重新設(shè)置Factory,則需要先將mFactorySet設(shè)置為false,不然系統(tǒng)判斷到mFactorySet是true則會拋異常.
//這里使用自己構(gòu)建的Factory去創(chuàng)建View,在創(chuàng)建View時當然也就可以控制它的背景或者文字顏色.
//(在這里之前需要知道哪些控件需要換膚,其中一部分是繼承自三方庫的控件,這些控件是實現(xiàn)了SkinCompatSupportable接口的,可以很方便的控制.
// 還有一部分是系統(tǒng)的控件,在創(chuàng)建時直接創(chuàng)建三方庫中的控件(比如View就創(chuàng)建SkinCompatView).
// 在設(shè)置系統(tǒng)控件的背景顏色和文字顏色時,直接從三方庫緩存顏色中取值,然后進行設(shè)置.)
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
在我們的Activity創(chuàng)建的時候,首先判斷一下是否需要換膚,需要換膚才去搞.
我們重點看看installLayoutFactory()方法,在上面的預備知識中說了mFactory只能設(shè)置一次,不然就要拋異常,所以需要先利用發(fā)射將mFactorySet的值設(shè)置為false才不會拋異常.然后才能setFactory().
下面我們來看看setFactory()的第二個參數(shù)創(chuàng)建過程,第二個參數(shù)其實是一個創(chuàng)建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 LayoutInflaterFactory {
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的委托.這里其實和系統(tǒng)的AppCompatDelegateImplV9很類似.
當系統(tǒng)需要創(chuàng)建View的時候,就會回調(diào)SkinCompatDelegate的@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,因為前面設(shè)置了LayoutInflater的Factory為SkinCompatDelegate. 然后SkinCompatDelegate將創(chuàng)建View的工作交給SkinCompatViewInflater去處理(也是和系統(tǒng)一模一樣).
來看看SkinCompatViewInflater是如何創(chuàng)建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(我理解為控件創(chuàng)建器)就是我們之前在初始化時設(shè)置的SkinAppCompatViewInflater
//當然,SkinLayoutInflater可以有多個
for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
view = inflater.createView(context, name, attrs);
if (view == null) {
continue;
} else {
break;
}
}
return view;
}
//這個方法和系統(tǒng)的完全一模一樣嘛,so easy
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;
}
}
可以看到,這些實現(xiàn)其實是和系統(tǒng)的實現(xiàn)是差不多的.原理已在上面的預備知識中給出.
這里也有不同的地方,createViewFromInflater()方法中利用了我們在初始化庫時設(shè)置的SkinLayoutInflater(我覺得是控件創(chuàng)造器)去創(chuàng)建view.
為什么要在SkinCompatViewInflater還要細化,還需要交由更細的SkinLayoutInflater來處理呢?我覺得是因為方便擴展,庫中給出了幾個SkinLayoutInflater,有SkinAppCompatViewInflater(基礎(chǔ)控件構(gòu)建器)、SkinMaterialViewInflater(material design控件構(gòu)造器)、SkinConstraintViewInflater(ConstraintLayout構(gòu)建器)、SkinCardViewInflater(CardView v7構(gòu)建器)。
由于初始化時我們設(shè)置的是SkinAppCompatViewInflater,其他的構(gòu)建器都是類似的原理.我們就來看看
//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;
}
柳暗花明又一村?這不就是之前我們在Android源碼中看過的代碼嗎?幾乎是一模一樣。我們在這里將View的創(chuàng)建攔截,然后創(chuàng)建自己的控件。既然是我們自己創(chuàng)建的控件,想干啥還不容易么?
我們看一下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();
}
}
}
還是那套經(jīng)典的操作,將background相關(guān)的屬性交給SkinCompatBackgroundHelper去處理,將textColor相關(guān)的操作交給SkinCompatTextHelper去處理。與源碼中一模一樣。
3. 源碼二,從皮膚包加載皮膚
其實皮膚包就是一個apk,只不過里面沒有任何代碼,只有一些需要換膚的資源或者顏色什么的.而且這些資源的名稱必須和當前app中的資源名稱是一致的,才能替換. 需要什么皮膚資源,直接去皮膚包里面去拿就好了.
使用方式
SkinCompatManager.getInstance().loadSkin("night.skin", null, CustomSDCardLoader.SKIN_LOADER_STRATEGY_SDCARD);
來吧,我們進入loadSkin()方法看一下:
/**
* 加載皮膚包.
* @param skinName 皮膚包名稱.
* @param listener 皮膚包加載監(jiān)聽.
* @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) {
//根據(jù)加載策略去后臺加載皮膚
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 {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = mAppContext.getResources();
return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
大概就是去子線程獲取皮膚包的包名和Resources(要這個干啥?后面我們需要獲取皮膚包中的顏色或者資源時需要通過這個進行獲取).
SkinCompatResources.getInstance().setupSkin()方法中就是將這些從皮膚包中加載的Resources,包名,皮膚名,加載策略全部存下來.有了這些東西,待會兒就能取皮膚包里面的資源了.
庫中定義的控件都是實現(xiàn)了SkinCompatSupportable接口的,方便控制換膚。比如SkinCompatTextView的applySkin()方法中調(diào)用了BackgroundTintHelper和TextHelper的applySkin()方法,就是說換膚時會去動態(tài)的更換背景或文字顏色什么的。我們來看看mBackgroundTintHelper.applySkin()的實現(xiàn)
//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設(shè)置背景嘛.關(guān)鍵在于這里的獲取drawable是怎么實現(xiàn)的.來看看具體實現(xiàn)
//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) {
//這里是關(guān)鍵
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;
}
最后是調(diào)用的SkinCompatDrawableManager去獲取drawable,我發(fā)現(xiàn)這個SkinCompatDrawableManager和系統(tǒng)的AppCompatDrawableManager一模一樣.
唯一不同點是上面的31行處drawable = SkinCompatResources.getDrawable(context, resId);,在這里我們?nèi)?chuàng)建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,開發(fā)者可自定義
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) {
//根據(jù)id通過皮膚的Resources去獲取drawable
return mResources.getDrawable(targetResId);
}
}
return context.getResources().getDrawable(resId);
}
大概意思就是有緩存資源(之前在皮膚包中取過這個resId的資源)則取緩存資源,沒有緩存則根據(jù)resId通過皮膚的Resources去獲取drawable.
到此,已經(jīng)獲取到皮膚包中的drawable,也就是實現(xiàn)了動態(tài)的加載皮膚包中的圖片,shape等等的資源,加載皮膚中的顏色的過程也是類似的,這里就不多介紹了.終于,我們完成了換膚大業(yè).
4.簡單總結(jié)一下原理(本文精髓)
- 監(jiān)聽APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
- 在每個Activity的onCreate()方法調(diào)用時setFactory(),設(shè)置創(chuàng)建View的工廠.將創(chuàng)建View的瑣事交給SkinCompatViewInflater去處理.
- 庫中自己重寫了系統(tǒng)的控件(比如View對應于庫中的SkinCompatView),實現(xiàn)換膚接口(接口里面只有一個applySkin()方法),表示該控件是支持換膚的.并且將這些控件在創(chuàng)建之后收集起來,方便隨時換膚.
- 在庫中自己寫的控件里面去解析出一些特殊的屬性(比如:background, textColor),并將其保存起來
- 在切換皮膚的時候,遍歷一次之前緩存的View,調(diào)用其實現(xiàn)的接口方法applySkin(),在applySkin()中從皮膚資源(可以是從網(wǎng)絡(luò)或者本地獲取皮膚包)中獲取資源.獲取資源后設(shè)置其控件的background或textColor等,就可實現(xiàn)換膚.
感謝開源,感謝作者.項目地址:https://github.com/ximsfei/Android-skin-support