再開始之前,我們有必要先看這么一個問題,同樣一個TextView,如果我們的MainActivity 分別繼承Activity袋励,AppCompatActivity,我們打印一下我們的TextView末购。
<!--繼承自Activity:-->
android.widget.TextView id/text_view
<!--繼承自AppCompatActivity: -->
android.support.v7.widget.AppCompatTextView id/text_view
發(fā)現(xiàn)一個很有趣的現(xiàn)象,明明是TextView虎谢,為什么繼承AppCompatActivity 就變成AppCompatTextView了盟榴?到底發(fā)生了什么?
可以看到婴噩,setContentView所做的事情擎场,就是通過PhoneWindow實(shí)例化一個DecorView羽德,同時解析一系列系統(tǒng)自帶的資源文件,把它添加到DecorView中顶籽,這里面有一個id為 android.R.id.content的FrameLayout,這個正是我們的activity的直接父容器玩般,而setContentView把自己的資源文件添加進(jìn)去银觅。
也可以看到繼承AppComatActivity的setContentView方法其實(shí)跟繼承Activity的實(shí)現(xiàn)沒什么太大的區(qū)別礼饱。
到這里,我們還是沒發(fā)現(xiàn)是什么導(dǎo)致TextView變成AppCompatTextView的究驴。
我們看AppCompatActivity onCreate方法()
@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);
}
在調(diào)用super.onCreate(savedInstanceState)之前镊绪,其實(shí)調(diào)用了delegate.installViewFactory()和 delegate.onCreate(savedInstanceState)這兩個方法。
看注釋
/**
* Installs AppCompat's {@link android.view.LayoutInflater} Factory so that it can replace
* the framework widgets with compatible tinted versions. This should be called before
* {@code super.onCreate()} as so:
* <pre class="prettyprint">
* protected void onCreate(Bundle savedInstanceState) {
* getDelegate().installViewFactory();
* getDelegate().onCreate(savedInstanceState);
* super.onCreate(savedInstanceState);
*
* // ...
* }
* </pre>
* If you are using your own {@link android.view.LayoutInflater.Factory Factory} or
* {@link android.view.LayoutInflater.Factory2 Factory2} then you can omit this call, and instead call
* {@link #createView(android.view.View, String, android.content.Context, android.util.AttributeSet)}
* from your factory to return any compatible widgets.
*/
public abstract void installViewFactory();
安裝AppCompat的( android.view洒忧。LayoutInflater)工廠蝴韭,以便它可以用兼容版本b并替換框架小部件。(比如TextView 替換成AppCompatTextView)榄鉴。
如果你使用自己的Factory蛉抓,或者Factory2,則可以跳過這個調(diào)用,調(diào)用自己工廠的createView(View view巷送,String name, Context context, AttributeSet attrs)}來返回任何兼容的小部件驶忌。
看來原因就在這里面笑跛。
我們找到他的實(shí)現(xiàn)
//AppCompatDelegateImplV9中
AppCompatDelegateImplV9 implements LayoutInflater.Factory2
@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");
}
}
}
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>. 創(chuàng)建視圖的父節(jié)點(diǎn)將被放置在其中 可能為空
* @param name Tag name to be inflated.被創(chuàng)建的標(biāo)簽名稱飞蹂。比如TextView,livesun.io.MyTextView等几苍。
* @param context The context the view is being created in. 上下文
* @param attrs Inflation attributes as specified in XML file.
*在XML文件中指定的詳細(xì)屬性:如textColor width 等
* @return View Newly created view. Return null for the default
* behavior.視圖新創(chuàng)建的視圖
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @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(String name, Context context, AttributeSet attrs);
}
接著看這兩個回掉方法是如何實(shí)現(xiàn)的
/**
* From {@link LayoutInflater.Factory2}.
*/
@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);
}
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
//這里看了半天妻坝,只能看出mOriginalWindowCallback 其實(shí)是Window.Callback芥颈。
// mAppCompatWindowCallback = wrapWindowCallback(mOriginalWindowCallback);
//mWindow.setCallback(mAppCompatWindowCallback);
//通過這兩句代碼 重新設(shè)置了一個新的Callback
//但是并沒用看到跟LayoutInflater.Factory有什么關(guān)系
//難道我們自己設(shè)置callBack的時候 同時讓其實(shí)現(xiàn)LayoutInflater.Factory才行爬坑??
//但是不管怎么說售担,目前這里是走不了。我們接著看return的createView(parent, name, context, attrs);方法岩四。
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// Let the Activity's LayoutInflater.Factory try and handle it
if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
}
return null;
}
createView(parent, name, context, attrs)方法是個關(guān)鍵的方法哥攘。
private static final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
@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逝淹,我們就可以檢測布局中的位置
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
//否則我們就得使用古老的啟發(fā)式
: 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 */
);
}
先判斷mAppCompatViewInflater為空栅葡,然后獲取其對象,如果小于5.0规脸,然后再判斷inheritContext為true還是false莫鸭。如果為true則使用父容器的context网棍。
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;
//一系列的判斷到底使用誰的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;
// 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.
//如果原始上下文不等于我們的主題上下文氏身,
//那么我們需要用這個名稱手動infalte蛋欣,以便android:theme 生效如贷。
//這里調(diào)用的createViewFromTag 其實(shí)跟Inflate的createViewFromTag是差不多一樣的,只是少了三個Factory的判斷
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
// 如果我們創(chuàng)建了一個視圖尚猿,檢查它的android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
到這里凿掂,明白了為什么會出現(xiàn)這樣類似變魔術(shù)的情況了吧。
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('.')) {
// try the android.widget prefix first...
//這里是走的widget包 適配 inflate 則是android.view包
return createView(context, name, "android.widget.");
} 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;
}
}
LayoutInflate的createViewFromTag方法
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) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
也就是說只要我們再onCreat方法的時候,setFactory援奢,那么視圖的創(chuàng)建就不會走系統(tǒng)的集漾,而按我們自己的來锉罐,至于其中原因,我們看下inflate的源碼。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
//如果factort2 不為null,就會調(diào)用factort2的onCreateView方法侨舆。
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
//如果factort 不為null,就會調(diào)用factort的onCreateView方法挨下。
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 如果factort2和factortweinull 而私有的不為null,就會調(diào)用mPrivateFactory的onCreateView方法脐湾。
這個mPrivateFactory是在Activity中賦值秤掌,算是默認(rèn)的
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
//如果還為null,才會調(diào)用默認(rèn)的onCreateView
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
//如系統(tǒng)控件 TextView ImagView
view = onCreateView(parent, name, attrs);
} else {
//自定義控件,如livesun.io.MyTextView
//其實(shí)是通過放射來實(shí)現(xiàn)初始化的
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
Activity的源碼中可以看到,在attach方法中孟岛,為mPrivateFactory設(shè)了值渠羞。
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
attachBaseContext(context);
....
//這里的this ,是Factory2
mWindow.getLayoutInflater().setPrivateFactory(this);
....
看他的實(shí)現(xiàn)方法,如果name!="fragment",就調(diào)用自身的onCreateView 否則,調(diào)用fragment的onCreateView方法荧恍。這下清楚了Fragment的onCreateView的由來渗蟹。
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
* Standard implementation of
* {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)}
* used when inflating with the LayoutInflater returned by {@link #getSystemService}.
* This implementation handles <fragment> tags to embed fragments inside
* of the activity.
*
* @see android.view.LayoutInflater#createView
* @see android.view.Window#getLayoutInflater
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return onCreateView(name, context, attrs);
}
return mFragments.onCreateView(parent, name, context, attrs);
}
所以攔截View的創(chuàng)建的本質(zhì),其實(shí)就是在LayoutInfalte中辨嗽,通過pull解析器解析整個XML視圖文件糟需,通過createViewFromTag方法谷朝,創(chuàng)建一個個View時,在之前通過Factory是否為null的判斷,結(jié)合view的值杈帐,來創(chuàng)建view专钉。這時跃须,如果我們改變前期的判斷,整個view在創(chuàng)建之前尽楔,我們就可以做很多事情第练,其實(shí)就是layoutInflate留好位置,改變它就行了垦缅。
來那我們變個魔術(shù)
重寫Activity的onCreateView方法驹碍,因?yàn)槲覀冎繟ctivity已經(jīng)實(shí)現(xiàn)了Factory2 (mWindow.getLayoutInflater().setPrivateFactory(this);)
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (name.equals("TextView"))
{
EditText editText=new EditText(this);
editText.setHint("大變TextView");
return editText;
}
return null;
}
這是我們的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/root"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ddddd"
/>
</RelativeLayout>
運(yùn)行結(jié)果
當(dāng)然 還可以這么寫怔球,自己設(shè)置Factory2浮还、Factory 他們優(yōu)先級更高
記得要在setContentView之前,應(yīng)為setContentView就會調(diào)用inflat方法担汤。
@Override
protected void onCreate(Bundle savedInstanceState) {
mLayoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(mLayoutInflater, new LayoutInflater.Factory2() {
@Override
public View onCreateView(String s, Context context, AttributeSet attributeSet) {
return null;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attributeSet) {
if (name.equals("TextView"))
{
EditText editText=new EditText(MainActivity.this);
editText.setHint("大變TextView2.0");
return editText;
}
return null;
}
});
setContentView(R.layout.scroll_demo);
super.onCreate(savedInstanceState);
}