学习过自定义 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 属性,具体细节可以在此不再赘述,我们进入该方法继续看
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("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("cannot be the root element"); } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException(" 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(...)
讲起吧!