主目录见:Android高级进阶知识(这是总目录索引)
终于迎来我们的换肤框架最终章了,前面我们也学了support v7的源码了,那么今天我们这里就轻松很多,废话不多说,直接开始讲解。想要源码的直接[点击下载],这老哥稳!!!大家向他学习。
一.目标
看过前面几篇的问题都知道,文章前面会有目标说明,今天不例外。
1.复习《Support v7库》的知识点。
2.说下framework怎么加载资源。
3.同时说明下Application的registerActivityLifecycleCallbacks方法。
二.源码分析
1.基础使用
在Application的onCreate中初始化:
SkinCompatManager.withoutActivity(this) // 基础控件换肤初始化
.addStrategy(new CustomSDCardLoader()) // 自定义加载策略,指定SDCard路径[可选]
.addHookInflater(new SkinHookAutoLayoutViewInflater()) // hongyangAndroid/AndroidAutoLayout[可选]
.addInflater(new SkinMaterialViewInflater()) // material design 控件换肤初始化[可选]
.addInflater(new SkinConstraintViewInflater()) // ConstraintLayout 控件换肤初始化[可选]
.addInflater(new SkinCardViewInflater()) // CardView v7 控件换肤初始化[可选]
.addInflater(new SkinCircleImageViewInflater()) // hdodenhof/CircleImageView[可选]
.addInflater(new SkinFlycoTabLayoutInflater()) // H07000223/FlycoTabLayout[可选]
.setSkinStatusBarColorEnable(false) // 关闭状态栏换肤,默认打开[可选]
.setSkinWindowBackgroundEnable(false) // 关闭windowBackground换肤,默认打开[可选]
.loadSkin();
加载插件皮肤库:
// 指定皮肤插件
SkinCompatManager.getInstance().loadSkin("new.skin"[, SkinLoaderListener], int strategy);
// 恢复应用默认皮肤
SkinCompatManager.getInstance().restoreDefaultTheme();
本来以前库是要继承SkinCompatActivity的,但是现在用了registerActivityLifecycleCallbacks监听了Activity的生命周期,这样可以燥起来,就不用继承了,等会会说明。
2.SkinCompatManager withoutActivity
这个地方遵循看源码的一贯步骤,我们来看下withoutActivity方法到底是干了啥?方法如下:
public static SkinCompatManager withoutActivity(Application application) {
init(application);
SkinActivityLifecycle.init(application);
return sInstance;
}
这个方法总共就两个方法,我们顺序来看init方法干了啥:
public static SkinCompatManager init(Context context) {
if (sInstance == null) {
synchronized (SkinCompatManager.class) {
if (sInstance == null) {
sInstance = new SkinCompatManager(context);
}
}
}
return sInstance;
}
这个方法其实就是创建一个SkinCompatManager这个单例对象,那我们就看这个构造函数里面做了些啥:
private SkinCompatManager(Context context) {
mAppContext = context.getApplicationContext();
SkinPreference.init(mAppContext);
SkinCompatResources.init(mAppContext);
initLoaderStrategy();
}
这个方法主要是做一些初始化工作,第一个类SkinPreference其实就是对SharePreference的封装,SkinCompatResources其实就是存放包名,皮肤名,加载策略(就是从什么途径加载)等等,同时有一些加载资源的方法。最后initLoaderStrategy()方法是加载默认的皮肤包加载策略。
看完第一个方法,我们现在看第二个方法SkinActivityLifecycle.init(application),这个方法非常关键,我们先看看init做了啥?
public static SkinActivityLifecycle init(Application application) {
if (sInstance == null) {
synchronized (SkinActivityLifecycle.class) {
if (sInstance == null) {
sInstance = new SkinActivityLifecycle(application);
}
}
}
return sInstance;
}
首先也是初始化一个SkinActivityLifecycle单例类,我们也直接跟进构造函数里面:
private SkinActivityLifecycle(Application application) {
application.registerActivityLifecycleCallbacks(this);
}
这个地方就是一句话,但是非常关键,记得在LeakCanary源码里面也有用到这个方法。这个方法是拦截Acitivity的生命周期方法来进行统一处理,这样的话我们不用让我们的Acitivity继承SkinCompatActivity(所以现在这个类被标记为废弃),就可以在每个Acitivity创建的时候做点动作了,非常管用。具体的Activity的生命周期里面做了哪些动作,我们下面会重点讲解。
3.SkinCompatManager loadSkin
这个方法是用来加载皮肤包的,我们这里先看loadSkin做了些什么东西。
public AsyncTask loadSkin() {
String skin = SkinPreference.getInstance().getSkinName();
int strategy = SkinPreference.getInstance().getSkinStrategy();
if (TextUtils.isEmpty(skin) || strategy == SKIN_LOADER_STRATEGY_NONE) {
return null;
}
return loadSkin(skin, null, strategy);
}
我们看到这个地方,首先获取本地是否有皮肤包名称和皮肤包策略的记录,如果有则取出来进行加载。
public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
return new SkinLoadTask(listener, mStrategyMap.get(strategy)).execute(skinName);
}
这个地方用到了SkinLoadTask来进行加载,SkinLoadTask是一个AsyncTask对象,所以我们先看onPreExecute()方法:
protected void onPreExecute() {
if (mListener != null) {
mListener.onStart();
}
}
这个方法没有做啥,就是调用接口的onStart方法,但是我们前面创建来的Listener是null,所以这个地方没有调用。接下来我们看到doInBackground()方法:
@Override
protected String doInBackground(String... params) {
synchronized (mLock) {
while (mLoading) {
try {
mLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mLoading = true;
}
try {
if (params.length == 1) {
if (TextUtils.isEmpty(params[0])) {
SkinCompatResources.getInstance().reset();
return params[0];
}
if (!TextUtils.isEmpty(
mStrategy.loadSkinInBackground(mAppContext, params[0]))) {
return params[0];
}
}
} catch (Exception e) {
e.printStackTrace();
}
SkinCompatResources.getInstance().reset();
return null;
}
这个方法前面加了一个同步代码块进行同步操作,然后我们看到调用了SkinCompatResources.getInstance().reset();将这个对象之前存进去的皮肤包名称,策略等信息重置。接着调用mStrategy.loadSkinInBackground(mAppContext, params[0])方法,这个地方mStrategy就是加载策略,内置了三个策略分别为:SkinAssetsLoader,SkinBuildInLoader,SkinSDCardLoader,第一个就是从Assets中加载皮肤包,第二个就是从本应用中加载皮肤包,第三个是从SD卡中加载皮肤包。这个地方我们用从SD卡中加载皮肤包为例子,因为这个场景用到还比较多。
4.SkinSDCardLoader loadSkinInBackground
这个类是个抽象类,为什么设置为抽象类,是因为SD卡的目录作者希望留给用户自己设置。我们直接进入到这个类的loadSkinInBackground方法:
public String loadSkinInBackground(Context context, String 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;
}
我们看到第一句就是获取皮肤包的路径,这个方法由用户自己指定,我们可以集成这个类进行重写。然后程序判断目录是否存在,如果存在则获取皮肤包的包名:
public String getSkinPackageName(String skinPkgPath) {
PackageManager mPm = mAppContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
return info.packageName;
}
然后获取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;
}
这个地方有个知识点:就是系统是怎么加载资源的。要详细可以看Android应用程序资源管理器(Asset Manager)的创建过程分析 和Android应用程序资源的查找过程分析 。这个地方我们简单归纳就是会调用AssetManager里面的final方法addAssetPath(),所以我们传进皮肤包的路径,反射调用这个方法进行添加即可。这样我们的外部皮肤包就被加进去了。
5.SkinLoadTask onPostExecute
看完onPreExecute()方法和doInBackground()方法我们就来看onPostExecute方法了:
protected void onPostExecute(String skinName) {
SkinLog.e("skinName = " + skinName);
synchronized (mLock) {
// skinName 为""时,恢复默认皮肤
if (skinName != null) {
SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor();
notifyUpdateSkin();
if (mListener != null) mListener.onSuccess();
} else {
SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor();
if (mListener != null) mListener.onFailed("皮肤资源获取失败");
}
mLoading = false;
mLock.notifyAll();
}
}
这里首先是将我们的策略类型保存起来,然后调用notifyUpdateSkin(),这个方法是做什么呢?因为这边加载完皮肤会通知Acitivity里面的视图控件跟着皮肤进行变化,这里用到了观察者设计模式,在每个Activity onResume的时候会将Activity添加为观察者,所以这个地方notifyUpdateSkin就是调用到
observer = new SkinObserver() {
@Override
public void updateSkin(SkinObservable observable, Object o) {
updateStatusBarColor(activity);
updateWindowBackground(activity);
getSkinDelegate((AppCompatActivity) activity).applySkin();
}
};
刷新状态栏,刷新背景,然后通知每个控件进行重新设置皮肤。跟之前套路不一样,我们将要进入主要知识讲解了。为了蹭iphone发布会热点,决定轻松一刻:
6.SkinActivityLifecycle onActivityCreated
我们知道之前有一句话:
private SkinActivityLifecycle(Application application) {
application.registerActivityLifecycleCallbacks(this);
}
这句话已经将SkinActivityLifecycle 设置为生命周期拦截器了,这样我们就可以拦截到Acitivity的每个生命周期,我们先来看我们Activity创建的生命周期即onActivityCreated:
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (activity instanceof AppCompatActivity) {
LayoutInflater layoutInflater = activity.getLayoutInflater();
try {
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
LayoutInflaterCompat.setFactory(activity.getLayoutInflater(),
getSkinDelegate((AppCompatActivity) activity));
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
updateStatusBarColor(activity);
updateWindowBackground(activity);
}
}
看到这个方法我们应该有种似曾相识的感觉,没错!这里其实跟support V7的源码是一样的。重点强调:LayoutInflaterCompat.setFactory(activity.getLayoutInflater(),getSkinDelegate((AppCompatActivity) activity));这个地方就是自定义Factory拦截View的创建过程。那我们还是那个套路看下getSkinDelegate()这个方法做了啥:
private SkinCompatDelegate getSkinDelegate(AppCompatActivity activity) {
if (mSkinDelegateMap == null) {
mSkinDelegateMap = new WeakHashMap<>();
}
SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(activity);
if (mSkinDelegate == null) {
mSkinDelegate = SkinCompatDelegate.create(activity);
}
mSkinDelegateMap.put(activity, mSkinDelegate);
return mSkinDelegate;
}
这个地方还有缓存,作者不错,首先看HashMap(这里要用WeakHashMap主要是因为持有了Activity的实例,为了防止内存泄漏所以用了弱引用的HashMap)里面有没有SkinCompatDelegate对象,没有则创建。SkinCompatDelegate是个LayoutInflaterFactory即Factory对象,所以我们xml里面View创建的时候会调用Factory里面的onCreateView()方法。
7.SkinCompatDelegate onCreateView
因为这个方法拦截了view的创建过程,所以我们就可以看到这个方法做了啥,其实我们已经很熟悉这个方法了:
@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内,到时调用notifyUpdateSkin的时候会调用刷新
mSkinHelpers.add(new WeakReference<SkinCompatSupportable>((SkinCompatSupportable) view));
}
return view;
}
我们知道我们调用createView方法,那么创建view的任务就是这个方法了:
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mSkinCompatViewInflater == null) {
mSkinCompatViewInflater = new SkinCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mSkinCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* 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 */
);
}
前面初始化了mSkinCompatViewInflater对象然后调用它的createView方法,我们继续跟进去:
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = createViewFromHackInflater(context, name, attrs);
// We need to 'inject' our tint aware Views in place of the standard framework versions
if (view == null) {
view = createViewFromFV(context, name, attrs);
}
if (view == null) {
view = createViewFromV7(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;
}
前面跟我们support v7源码是一样的,我们直接看到createViewFromHackInflater方法,这个方法我们看到尝试创建了view对象:
private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
View view = null;
for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {
view = inflater.createView(context, name, attrs);
if (view == null) {
continue;
} else {
break;
}
}
return view;
}
我们看到代码应该很熟悉呀,这个方法其实就是让用户可以设置SkinLayoutInflater,然后来得到优先拦截创建view的能力。我们假装用户没有设置,那么返回的view就会为null,那么就会进入下一个createViewFromFV()方法:
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;
}
return view;
}
我们可以看到这里拦截好多控件的创建过程。为了不使我们的文章冗长,这边就挑一个控件来说明,这里我们挑讲解support v7时候同样的控件TextView控件来讲解。
8.SkinCompatTextView
这是个自定义的TextView(如果自己要往这个库加入什么新的控件,套路也是一样的),又因为到时通知更新要更新控件,所以每个控件必须实现SkinCompatSupportable接口:
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
}
我们接下来看到构造函数:
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);
}
这里主要有两个类SkinCompatBackgroundHelper和SkinCompatTextHelper,很明显第一个是用来设置背景的,第二个类是设置Text相关外形的。我们先来看SkinCompatBackgroundHelper的loadFromAttributes方法:
public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.SkinBackgroundHelper, defStyleAttr, 0);
try {
if (a.hasValue(R.styleable.SkinBackgroundHelper_android_background)) {
mBackgroundResId = a.getResourceId(
R.styleable.SkinBackgroundHelper_android_background, INVALID_ID);
}
} finally {
a.recycle();
}
applySkin();
}
这里清晰明了,对老司机来说这都不是事,获取自定义属性backgroud,但是这里有个小技巧我们来看下SkinBackgroundHelper_android_background对应的属性是啥:
<declare-styleable name="SkinBackgroundHelper">
<attr name="android:background" />
</declare-styleable>
看到没有!!!看到没有!!!其实他对应的就是系统的background,为什么这么做呢?就是为了我们在写background的时候不需要麻烦再去自定义,只要写上background即可,是不是处处有干货,这干货很干。
接下来我们看下SkinCompatTextHelper的loadFromAttributes:
public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
final Context context = mView.getContext();
// First read the TextAppearance style id
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SkinCompatTextHelper, defStyleAttr, 0);
final int ap = a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);
SkinLog.d(TAG, "ap = " + ap);
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
mDrawableLeftResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, INVALID_ID);
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
mDrawableTopResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, INVALID_ID);
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
mDrawableRightResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, INVALID_ID);
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
mDrawableBottomResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, INVALID_ID);
}
a.recycle();
if (ap != INVALID_ID) {
a = context.obtainStyledAttributes(ap, R.styleable.SkinTextAppearance);
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
}
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
mTextColorHintResId = a.getResourceId(
R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
}
a.recycle();
}
// Now read the style's values
a = context.obtainStyledAttributes(attrs, R.styleable.SkinTextAppearance, defStyleAttr, 0);
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
}
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
mTextColorHintResId = a.getResourceId(
R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
}
a.recycle();
applySkin();
}
看着这么多代码不要被吓到,其实非常简单,我们看下style:
<declare-styleable name="SkinCompatTextHelper">
<attr name="android:drawableLeft" />
<attr name="android:drawableTop" />
<attr name="android:drawableRight" />
<attr name="android:drawableBottom" />
<attr name="android:drawableStart" />
<attr name="android:drawableEnd" />
<attr name="android:textAppearance" />
</declare-styleable>
其实就是获取这些属性值。我去。。。。上面代码白贴了。最后会调用applySkin()方法,就是去设置这些属性:
public void applySkin() {
applyTextColorResource();
applyTextColorHintResource();
applyCompoundDrawablesResource();
}
从方法名就可以很清晰地看出来。就不过多纠结了,这个方法applySkin是SkinCompatSupportable 接口里面的,在观察者模式提示更新的时候也会调用到这个方法,所以我们自定义的换肤控件都必须实现这个接口,这是个约定。
到这里我们的讲解就完成,这篇真的是干货和技能MAX,妈妈再也不用担心我求干货了。
总结:这个换肤框架是比较综合的一个support v7知识应用,同时包含了许多的小技巧,都是自定义控件或者其他地方能用到的,是一个解决方案。希望大家有所收获,谢谢坚持看完,说明大哥你是闲人中的战斗机!!!