文章可能篇幅过长,但会一个步骤一个步骤带大家来分析。
实现页面的几种方式
- 继承 AppCompatActivity
- 继承 Activity
Activity
public class Activity {
...
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
...
}
这里的 mWindow 其实是 PhoneWindow ,在 Activity 的 attach 方法中初始化
PhoneWindow
public class PhoneWindow {
...
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
...
}
第一次 mContentParent 肯定为 null ,所以会执行 installDecor 方法
private void installDecor() {
...
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
...
}
protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
generateDecor 方法的意思就是 new DecorView 并返回
接着 installDecor 方法往下看
private void installDecor() {
...
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
...
}
protected ViewGroup generateLayout(DecorView decor) {
...
int layoutResource;
int features = getLocalFeatures();
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
...
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
...
}
...
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
generateLayout 方法有 3 点
1 加载系统的布局
2 将布局加载到创建的 DecorView 中
3 通过 DecorView 找到 id 为 com.android.internal.R.id.content 的 ViewGroup 返回,也就是 mContentParent
界面的组成
AppCompatActivity
public class AppCompatActivity {
...
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
...
}
class AppCompatDelegateImpl {
...
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
private void ensureSubDecor() {
...
mSubDecor = createSubDecor();
...
}
private ViewGroup createSubDecor() {
...
mWindow.setContentView(subDecor);
...
return subDecor;
}
...
}
ensureSubDecor 方法和上面的 generateLayout 类似,只不过 contentParent 是在 setContentView 中处理的
LayoutInflater 分析
经过上面的分析,不论是 Activity 还是 AppCompatActivity 最终都会执行 LayoutInflater.inflate 方法
public abstract class LayoutInflater {
...
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) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
}
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
View view = tryCreateView(parent, name, context, attrs);
...
}
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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);
}
return view;
}
...
}
LayoutInflater. inflate -> createViewFromTag -> tryCreateView
public class AppCompatActivity {
...
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
...
}
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
AppCompatActivity 在 onCreate 中设置了Factory,所以会执行 mFactory2.onCreateView 的方法
class AppCompatDelegateImpl {
...
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
...
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 */
);
}
...
}
public class AppCompatViewInflater {
...
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
...
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);
}
}
...
}
可以看到当 API < 21时 View 被转换成为其他的 View,所以我们可以理解为 AppCompatActivity 会兼容低版本。
有兴趣的可以去做个小实验,分别继承 Activity 和 AppCompatActivity,将 TextView 打印出来。继承 Activity TextView 还是 TextView,而继承 AppCompatActivity 时 TextView 打印为 AppCompatTextView。
public abstract class LayoutInflater {
...
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
经过刚才的分析,继承 AppCompatActivity View 会被创建成对应的 View,而Activity 是没有设置 Factory 的,所以 View 为空,将执行下面的判断。
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
如果没有'.',也就是说如果是系统控件会执行 onCreateView 方法
如果有'.',就是我们的自定义控件,如'com.xxx',将会执行 createView 方法
系统控件
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
就是将系统的控件拼接上 android.view ,然后创建出来。
setContentView 源码大概就介绍到这里了,最后给大家做一个总结。
- Activity 和 AppCompatActivity 加载布局前都会创建一个 DecorView,并将系统布局加载到 DecorView 中,通过 DecorView 找到 id 为 android.id.content 的FrameLayout,最后通过 LayoutInflater 加载我们的 xml 布局。
- Activity 没有设置Factory ,AppCompatActivity 设置了 Factory。
- Activity 不会拦截 View,而 AppCompatActivity 会拦截 View,并将部分 View 转换成对应的 AppCompatView。
拦截小例子
public class V8BaseActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
inflateTest();
super.onCreate(savedInstanceState);
}
private void inflateTest() {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
Log.d("View:", name);
if (name.equals("Button")) {
TextView textView = new TextView(context);
textView.setText("test");
textView.setTextColor(Color.BLACK);
return textView;
}
return null;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
});
}
}
通过拦截方法将 Button 换成了 TextView
文章就介绍到这里了,如果有什么写得不对的,可以在下方评论留言,我会第一时间改正。
Github 源码链接