再开始之前,我们有必要先看这么一个问题,同样一个TextView,如果我们的MainActivity 分别继承Activity,AppCompatActivity,我们打印一下我们的TextView。
<!--继承自Activity:-->
android.widget.TextView id/text_view
<!--继承自AppCompatActivity: -->
android.support.v7.widget.AppCompatTextView id/text_view
发现一个很有趣的现象,明明是TextView,为什么继承AppCompatActivity 就变成AppCompatTextView了?到底发生了什么?
可以看到,setContentView所做的事情,就是通过PhoneWindow实例化一个DecorView,同时解析一系列系统自带的资源文件,把它添加到DecorView中,这里面有一个id为 android.R.id.content的FrameLayout,这个正是我们的activity的直接父容器,而setContentView把自己的资源文件添加进去。
也可以看到继承AppComatActivity的setContentView方法其实跟继承Activity的实现没什么太大的区别。
到这里,我们还是没发现是什么导致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);
}
在调用super.onCreate(savedInstanceState)之前,其实调用了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,则可以跳过这个调用,调用自己工厂的createView(View view,String name, Context context, AttributeSet attrs)}来返回任何兼容的小部件。
看来原因就在这里面。
我们找到他的实现
//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>. 创建视图的父节点将被放置在其中 可能为空
* @param name Tag name to be inflated.被创建的标签名称。比如TextView,livesun.io.MyTextView等。
* @param context The context the view is being created in. 上下文
* @param attrs Inflation attributes as specified in XML file.
*在XML文件中指定的详细属性:如textColor width 等
* @return View Newly created view. Return null for the default
* behavior.视图新创建的视图
*/
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);
}
接着看这两个回掉方法是如何实现的
/**
* 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 其实是Window.Callback。
// mAppCompatWindowCallback = wrapWindowCallback(mOriginalWindowCallback);
//mWindow.setCallback(mAppCompatWindowCallback);
//通过这两句代码 重新设置了一个新的Callback
//但是并没用看到跟LayoutInflater.Factory有什么关系
//难道我们自己设置callBack的时候 同时让其实现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)方法是个关键的方法。
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
//否则我们就得使用古老的启发式
: 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 生效。
//这里调用的createViewFromTag 其实跟Inflate的createViewFromTag是差不多一样的,只是少了三个Factory的判断
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
// 如果我们创建了一个视图,检查它的android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
到这里,明白了为什么会出现这样类似变魔术的情况了吧。
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,那么视图的创建就不会走系统的,而按我们自己的来,至于其中原因,我们看下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,就会调用factort2的onCreateView方法。
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
//如果factort 不为null,就会调用factort的onCreateView方法。
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 如果factort2和factortweinull 而私有的不为null,就会调用mPrivateFactory的onCreateView方法。
这个mPrivateFactory是在Activity中赋值,算是默认的
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
//如果还为null,才会调用默认的onCreateView
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
//如系统控件 TextView ImagView
view = onCreateView(parent, name, attrs);
} else {
//自定义控件,如livesun.io.MyTextView
//其实是通过放射来实现初始化的
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设了值。
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);
....
看他的实现方法,如果name!="fragment",就调用自身的onCreateView 否则,调用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的创建的本质,其实就是在LayoutInfalte中,通过pull解析器解析整个XML视图文件,通过createViewFromTag方法,创建一个个View时,在之前通过Factory是否为null的判断,结合view的值,来创建view。这时,如果我们改变前期的判断,整个view在创建之前,我们就可以做很多事情,其实就是layoutInflate留好位置,改变它就行了。
来那我们变个魔术
重写Activity的onCreateView方法,因为我们知道Activity已经实现了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>
运行结果
当然 还可以这么写,自己设置Factory2、Factory 他们优先级更高
记得要在setContentView之前,应为setContentView就会调用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);
}