主目錄見(jiàn):Android高級(jí)進(jìn)階知識(shí)(這是總目錄索引)
一.目標(biāo)
秉承一貫的原則,不以目的為出發(fā)點(diǎn)的源碼解析都是耍流氓。所以我們來(lái)說(shuō)明下今天的目的:
1)復(fù)習(xí)《setContentView源碼分析》的知識(shí)點(diǎn)。
2)為下一篇的換膚框架打一個(gè)基礎(chǔ).
3)當(dāng)然也是為《小紅書的效果》做一個(gè)鋪墊.
二.support v7庫(kù)解析
1.回顧setContentView的一個(gè)知識(shí)點(diǎn)
為了省去大家去查找前面文章的麻煩(當(dāng)然我建議有時(shí)間還是可以去看下《setContentView源碼分析》這一定不會(huì)浪費(fèi)你時(shí)間),我這個(gè)地方重新貼一遍這一段關(guān)鍵代碼,這也是原則鲸伴,重要的知識(shí)點(diǎ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;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
..........
}
}
這個(gè)地方我們看到下面這句代碼 :
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
這句代碼非常關(guān)鍵,也是我們接下來(lái)?yè)Q膚框架要用到的知識(shí)點(diǎn)夫嗓,我們看到這個(gè)地方創(chuàng)建view的時(shí)候會(huì)先用factory來(lái)創(chuàng)建即這邊的mFactory2迟螺,mFactory,這兩個(gè)東西是什么呢舍咖?這兩個(gè)其實(shí)是Factory的子類矩父,LayoutInflater其中有個(gè)方法:
public void setFactory(Factory factory) {
}
這個(gè)方法可以設(shè)置factory,也就是說(shuō)只要我們?cè)O(shè)置進(jìn)來(lái)我們自己的factory排霉,那么系統(tǒng)就會(huì)用我們的factory來(lái)創(chuàng)建view窍株,這也就達(dá)到攔截view創(chuàng)建過(guò)程的作用。Fantastic Resource Code9ツ<欣选!
2.AppCompatActivity onCreate
我們知道我們現(xiàn)在創(chuàng)建Activity的時(shí)候會(huì)繼承兼容包里面的AppCompatActivity來(lái)達(dá)到使用高版本控件的能力辙诞。那我們的源碼之旅就從onCreate開(kāi)始辙售。
protected void onCreate(@Nullable Bundle savedInstanceState) {
//我們看到接下來(lái)這三句,主要就是得到delegate飞涂,然后執(zhí)行delegate里面的onCreate方法
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);
}
看到這個(gè)方法旦部,我們看到第一句getDelegate()方法,后面的操作都是在這個(gè)delegate對(duì)象里面较店,那么我們看看這個(gè)delegate到底是哪里的孫猴子=》列車直通花果山:
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
so easy士八!有沒(méi)有,就是調(diào)用的AppCompatDelegate的create方法梁呈,順其代碼直接進(jìn)入AppCompatDelegate的這個(gè)方法:
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
我去婚度。。蝗茁。。童話里都是騙人的寻咒,剛說(shuō)的so easy呢哮翘?這里竟然不同版本返回的還是不一樣,但是這有個(gè)有趣的地方毛秘,待我給大家揭露:
class AppCompatDelegateImplN extends AppCompatDelegateImplV23 {
}
class AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 {
}
class AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 {
}
class AppCompatDelegateImplV11 extends AppCompatDelegateImplV9 {
}
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflaterFactory {
}
這里看到了沒(méi)有饭寺,各個(gè)的類其實(shí)最終都是從AppCompatDelegateImplV9 出來(lái)的阻课,那為什么要弄出幾個(gè)呢?其實(shí)只是為了兼容后面版本一些夜間模式功能而已艰匙,所以這里返回的對(duì)象我們就當(dāng)做AppCompatDelegateImplV9 限煞。
3.delegate installViewFactory
我們到這里已經(jīng)找到我們delegate(AppCompatDelegateImplV9 )了,所以我們直接進(jìn)入AppCompatDelegateImplV9 的installViewFactory()方法:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
其實(shí)這句代碼也很簡(jiǎn)單有沒(méi)有员凝,就是得到layoutInflater對(duì)象晰骑,然后判斷factory存不存在,不存在則將factory設(shè)置成AppCompatDelegateImplV9绊序。這個(gè)地方是不是和開(kāi)頭我們說(shuō)的想吻合了硕舆。我們通過(guò)設(shè)置factory來(lái)攔截創(chuàng)建view的過(guò)程。
4.delegate onCreateView
既然已經(jīng)將我們的factory設(shè)置進(jìn)去了骤公,那我們知道創(chuàng)建view的時(shí)候會(huì)調(diào)用onCreateView抚官,這也是我們開(kāi)篇說(shuō)過(guò)的,所以我們這里進(jìn)入AppCompatDelegateImplV9這個(gè)方法看有什么不同:
@Override
public final View onCreateView(View parent, String name,
Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
其實(shí)從英文注釋也可以看出阶捆,首先會(huì)調(diào)用Activity的onCreateView進(jìn)行嘗試創(chuàng)建View凌节,如果沒(méi)有創(chuàng)建成功則調(diào)用我們的createView(),我們知道,如果用到高級(jí)控件的話洒试,有些屬性是低版本沒(méi)有的所以如果用到低版本沒(méi)有的屬性的話那么肯定會(huì)創(chuàng)建失敗即這里會(huì)調(diào)用到我們的createView()方法倍奢。
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mAppCompatViewInflater.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 */
);
}
這個(gè)方法其實(shí)沒(méi)有什么內(nèi)容就是創(chuàng)建一個(gè)mAppCompatViewInflater 對(duì)象,然后調(diào)用他的createView()方法垒棋,所以我們直接跳到這個(gè)方法:
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 = null;
//關(guān)鍵代碼看這里卒煞,這下面的控件就是我們要兼容的控件,也是support V7要攔截的view
// 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;
}
//當(dāng)然如果不是上面的控件則會(huì)嘗試用其他方法創(chuàng)建view
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 it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
這個(gè)方法很長(zhǎng)叼架,但是其實(shí)很簡(jiǎn)單畔裕,就是根據(jù)name(這個(gè)name就是我們控件的名字),如果發(fā)現(xiàn)name對(duì)應(yīng)的控件則new出對(duì)應(yīng)的控件乖订。我們這個(gè)地方挑一個(gè)控件來(lái)看扮饶,那就挑AppCompatTextView吧。
5.AppCompatTextView
到這里我們的講解就快完成了哈乍构,大家堅(jiān)持一下甜无,馬上就見(jiàn)到曙光了。
我們直接打開(kāi)AppCompatTextView類哥遮,然后看到構(gòu)造方法岂丘,我們都知道,自定義有好幾個(gè)構(gòu)造函數(shù)昔善,但是一個(gè)參數(shù)的構(gòu)造函數(shù)會(huì)調(diào)用兩個(gè)參數(shù)的構(gòu)造函數(shù)元潘,兩個(gè)參數(shù)的構(gòu)造函數(shù)會(huì)調(diào)用三個(gè)參數(shù)的構(gòu)造函數(shù)畔乙,所以我們看三個(gè)參數(shù)的構(gòu)造函數(shù)君仆。
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();
}
這個(gè)方法其實(shí)也很簡(jiǎn)單,我們可以看到主要有AppCompatBackgroundHelper和AppCompatTextHelper,那這兩個(gè)是干什么的呢返咱?我們先來(lái)看AppCompatBackgroundHelper:
5.1.AppCompatBackgroundHelper loadFromAttributes
我們看到這個(gè)方法里面做了啥:
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)) {
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)) {
ViewCompat.setBackgroundTintList(mView,
a.getColorStateList(R.styleable.ViewBackgroundHelper_backgroundTint));
}
if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTintMode)) {
ViewCompat.setBackgroundTintMode(mView,
DrawableUtils.parseTintMode(
a.getInt(R.styleable.ViewBackgroundHelper_backgroundTintMode, -1),
null));
}
} finally {
a.recycle();
}
}
看到這個(gè)是不是很熟悉钥庇,其實(shí)就是得到背景,背景著色器等等屬性咖摹,然后進(jìn)行設(shè)置進(jìn)TextView评姨。
5.1.AppCompatTextHelper loadFromAttributes
這是方法其實(shí)也是獲取對(duì)應(yīng)屬性的值,然后分別進(jìn)行設(shè)置萤晴,由于這個(gè)地方屬性太多了吐句,代碼也會(huì)比較多所以我就貼出部分代碼,因?yàn)檫@不是重點(diǎn)店读,主要給大家一個(gè)思路嗦枢,可以通過(guò)這種方式來(lái)自定義屬性:
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();
//底下省略部分代碼,跟上面這段類似
...........
}
所以我們知道其實(shí)AppCompatTextView這個(gè)控件也好屯断,還是其他控件也好文虏,這個(gè)地方就是獲取我們自定義的屬性然后進(jìn)行設(shè)置。這樣我們的TextView就有了一些高級(jí)屬性殖演,達(dá)到了自定義的作用.到這里我們的源碼分析就已經(jīng)完成了氧秘,希望大家有enjoy這段旅程。
總結(jié):我們的support v7庫(kù)就是設(shè)置LayoutInflater的Factory然后攔截onCreateView方法來(lái)創(chuàng)建View趴久,原理特別簡(jiǎn)單丸相,但是這是一個(gè)很有用的技能。彼棍。已添。