学习过自定义 View 的都知道,ViewGroup 的事件分发或者绘制都涉及到子 View 的遍历,在看ViewGroup 的源码的过程中发现了这个我们一直见到的 ViewGroup 的子 View 的集合,在这里是以数组的形式存储的:View[] mChildren
。我就想,这个 mChildren 是在哪里赋值的呢?或者说是怎么被赋值的呢?这时候我想到,在平时我们想往一个 ViewGroup 中添加子 View 的时候,往往是调用的这个 ViewGroup 的 addView(View child)
方法,那么我们只要找到调用 addView(View child)
的地方,自然也就找到了。但是直接找这个方法的调用,可不是一件容易的事,看来我们要换种思路了。不急,这里我们先深入 addView(View child)
方法里看看具体是怎么赋值的,也算是给自己记下笔记吧。
ViewGroup 的 addView(View child)
过程,即 mChildren
的赋值
用一个图来表示吧:
在 addView(View child)
中调用了 addViewInner(child, index, params, false)
,在addViewInner(child, index, params, false)
中又调用了 addInArray(child, index)
,在该方法中才是具体的实现了对 mChildren
的赋值(通过引用直接修改),下面贴下该方法的源码:
private void addInArray(View child, int index) {
View[] children = mChildren;
final int count = mChildrenCount;
final int size = children.length;
if (index == count) {
if (size == count) {
mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
System.arraycopy(children, 0, mChildren, 0, size);
children = mChildren;
}
// 注意这里,这里就是对 mChildren 进行赋值的地方
children[mChildrenCount++] = child;
} else if (index < count) {
if (size == count) {
mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
System.arraycopy(children, 0, mChildren, 0, index);
System.arraycopy(children, index, mChildren, index + 1, count - index);
children = mChildren;
} else {
System.arraycopy(children, index, children, index + 1, count - index);
}
children[index] = child;
mChildrenCount++;
if (mLastTouchDownIndex >= index) {
mLastTouchDownIndex++;
}
} else {
throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
}
}
代码很简单,可以看到这里也是搞了一个不断增长的数组来更新 mChildren,每次增长的长度为:ARRAY_CAPACITY_INCREMENT
,为常量 12,这里对该方法我们不过多讲解,毕竟,我们的重点不在这。
布局文件 xxx.xml 是怎么生成 View 树,并显示出来的呢?
我们已经习惯了直接在一个 activity
中的 onCreate(...)
方法中通过 setContentView(R.layout.main)
这种方式来给我们的 activity 指定布局,然后我们就可以通过修改布局文件,来决定activity
的显示内容。显然,在 setContentView(...)
这个方法内部肯定有我们想要的答案,在其内部,一定涉及到 xml
布局文件与具体的 View
之间的转换,也就是 xml
文件的解析和 View
树的生成。那么就让我们深入到该方法里一探究竟。
注意:这里我们创建的activity
直接继承自 Activity.Java
而非 AppCompatActivity.java
, 至于为什么强调这个,在后面的文章里我们会具体讲解(涉及到LayoutInflate.Factory 以及兼容性的处理策略)。
按着 Ctrl
键,鼠标左键
点击我们创建的 activity
的 setContentView(...)
方法,将会进入到 Activity.java
的 setContentView(@LayoutRes int layoutResID)
方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可以看到,这里又调用了 Window 的 setContentView(layoutResID)
方法,而 Window
是一个抽象类,因为调用的实际上是它的唯一实现类 PhoneWindow.java 里的该方法:
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
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;
}
别看这么多代码,关键的只有一句: mLayoutInflater.inflate(layoutResID, mContentParent)
,这句才是解析 xml 文件的核心代码,在此, LayoutInflate现身了,经常在代码里使用 LayoutInflate
,这次终于找到系统中使用该方法的地方了吧。那么接下来我们就真正看看 LayoutInflate
的使用以及 LayoutInflate.inflate(...)
的具体过程吧。
LayoutInflate 的具体使用
一、如何获取 LayoutInflate
获取 InflateInflate 有三种方式:
LayoutInflater layoutInflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater layoutInflater = LayoutInflater.from(context);
在
activity
中直接调用LayoutInflater layoutInflater = getLayoutInflater();
获得
其实,前面两种方式是完全一样的,只不过第二种对第一种进行了封装,使用起来更加方便。
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
二、LayoutInflate.inflate(...) 的过程
知道怎么获取 LayoutInflate 之后,我们继续上面的讲,前面我们通过 深入 setContentView(R.layout.xxx)
发现真正解析 xml 布局文件,并且生成 View 树的其实就是 mLayoutInflater.inflate(layoutResID, mContentParent)
这行代码。那么我们就来看看
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
在该方法里又调用了含有三个参数的重载方法 : inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
,这就是我们经常使用 LayoutInfalte 时候传递三个参数或者传递两个参数的两种情况,关于这两种情况的区别,其实就是是否保留最外层View的 layout_xxx 属性,具体细节可以在此不再赘述,我们进入该方法继续看
` 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) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
// 注意这里,这里调用的是另一个重载方法,注意第一个参数的类型为 XmlResourceParser
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}`
需要注意,这里又调用了inflate(...)
的另一个重载方法:public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
,仍然是三个参数,但要注意,第一个参数类型变为了 XmlResourceParser
,这里就设计 xml 文档的 pull 解析了:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 关键代码1,调用含有四个参数的 inflate(...)
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 关键代码2,根据传进来的各种参数,生成对应的 View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
// 关键代码3,递归的解析出以 temp 为父节点的所有子 View 或者子 ViewGroup
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
// 关键代码4,将temp添加到根View:root中
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
看着代码那么多,是不是傻眼了,哈哈,不过不用着急,只需要看上面我汉语注释了的那几句代码,下面我们结合图来具体说明,相信你也会更加明白。
方法 rInflateChildren(...)
顾名思义就是递归的解析出所有的子View,我们看他的代码,里面是直接的调用了 rInflate(...)
方法的
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
我们也直接在这里粘贴出来的源码,一起进行比较。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// 重点注意以下几行
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
// 此处是进行加载完成之后的回调,目前为止传进来的参数 finishInflate 均为 true ,因而当所有的子Viewz都加载完毕的时候,就会回调父容器的 onFinishInflate 方法,在 ViewGroup 中,该方法为空实现
if (finishInflate) {
parent.onFinishInflate();
}
}
上面这么多代码看起来很乱,但是我们只要重点抓住几条,就可以让思路变得更加清晰,上面那么多代码,核心只有下面几句:
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
然后就只是递归的问题了。可以总结为下面的几句:
createViewFromTag(...)
负责根据指定的 name 生成具体的 ViewrInflateChildren(...)
负责递归调用rInflate(...)
,使得该过程反复被执行,直到最后遍历完所有的View,不再进入While循环,直接调用parent.onFinishInflate()
,表明遍历完结,然后就会在栈中一步步的向上调用parent.onFinishInflate()
方法来通知父容器,本View已经解析完成.viewGroup.addView(view, params)
,该句负责把每次解析出来的 View 都添加进本次的根 ViewGroup 中,这样最终会将其下所有的子 View 都添加进来。
自此,我们就将 LayoutInflate
解析 xml
,从而生成 View树
的过程讲解完了,当然,具体的内容还有很多,接下来我会一步步的去讲解,在本篇中的一些迷惑,你也会一一解开,那么下一篇,就让我们从 createViewFromTag(...)
讲起吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。