本文原创, 转载请注明出处:http://blog.csdn.net/qinjuning
在之前一篇博文中<<Android中View绘制流程以及invalidate()等相关方法分析>>,简单的阐述 了Android View
绘制流程的三个步骤,即:
1、 measure过程 --- 测量过程
2、 layout 过程 --- 布局过程
3、 draw 过程 --- 绘制过程
要想对Android 中View这块深入理解,对这三个步骤地学习是必不可少的 。
今天,我着重讲解下如下三个内容:
1、measure过程
2、WRAP_CONTENT、MATCH_PARENT/FILL_PARENT属性的原理说明
3、xml布局文件解析成View树的流程分析。
希望对大家能有帮助。- -分析版本基于Android 2.3 。
初入Android殿堂的同学们,对这三个属性一定又爱又恨。爱的是使用起来挺爽地---照葫芦画瓢即可,恨的
却是时常混淆这几个属性地意义,需要三思而后行。在带着大家重温下这几个属性的用法吧(希望我没有啰嗦)。
这三个属性都用来适应视图的水平或垂直大小,一个以视图的内容或尺寸为基础的布局比精确地指定视图范围
更加方便。
① fill_parent
设置一个视图的布局为fill_parent将强制性地使视图扩展至父元素大小。
② match_parent
Android 中match_parent和fill_parent意思一样,但match_parent更贴切,于是从2.2开始两个词都可以
用,但2.3版本后建议使用match_parent。
③ wrap_content
自适应大小,强制性地使视图扩展以便显示其全部内容。以TextView和ImageView控件为例,设置为
wrap_content将完整显示其内部的文本和图像。布局元素将根据内容更改大小。
可不要重复造轮子,以上摘自<<Androidfill_parent、wrap_content和match_parent的区别>>。
当然,我们可以设置View的确切宽高,而不是由以上属性指定。
android:layout_weight="wrap_content" //自适应大小 android:layout_weight="match_parent" //与父视图等高 android:layout_weight="fill_parent" //与父视图等高 android:layout_weight="100dip" //精确设置高度值为 100dip
接下来,我们需要转换下视角,看看ViewGroup.LayoutParams类及其派生类。
Android API中如下介绍:
LayoutParams are used by views to tell their parents how they want to be laid out.
意思大概是说:View通过LayoutParams类告诉其父视图它想要地大小(即,长度和宽度)。
因此,每个View都包含一个ViewGroup.LayoutParams类或者其派生类,View类依赖于ViewGroup.LayoutParams。
路径:frameworks\base\core\java\android\view\View.java
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ... /** * The layout parameters associated with this view and used by the parent * {@link android.view.ViewGroup} to determine how this view should be * laid out. * {@hide} */ //该View拥有的 LayoutParams属性,父试图添加该View时,会为其赋值,特别注意,其类型为ViewGroup.LayoutParams。 protected ViewGroup.LayoutParams mLayoutParams; ... }
路径位于:frameworks\base\core\java\android\view\ViewGroup.java
public abstract class ViewGroup extends View implements ViewParent, ViewManager { ... public static class LayoutParams { /** * Special value for the height or width requested by a View. * FILL_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. This value is deprecated * starting in API Level 8 and replaced by {@link #MATCH_PARENT}. */ @Deprecated public static final int FILL_PARENT = -1; // 注意值为-1,Android2.2版本不建议使用 /** * Special value for the height or width requested by a View. * MATCH_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. Introduced in API Level 8. */ public static final int MATCH_PARENT = -1; // 注意值为-1 /** * Special value for the height or width requested by a View. * WRAP_CONTENT means that the view wants to be just large enough to fit * its own internal content, taking its own padding into account. */ public static final int WRAP_CONTENT = -2; // 注意值为-2 /** * Information about how wide the view wants to be. Can be one of the * constants FILL_PARENT (replaced by MATCH_PARENT , * in API Level 8) or WRAP_CONTENT. or an exact size. */ public int width; //该View的宽度,可以为WRAP_CONTENT/MATCH_PARENT 或者一个具体值 /** * Information about how tall the view wants to be. Can be one of the * constants FILL_PARENT (replaced by MATCH_PARENT , * in API Level 8) or WRAP_CONTENT. or an exact size. */ public int height; //该View的高度,可以为WRAP_CONTENT/MATCH_PARENT 或者一个具体值 /** * Used to animate layouts. */ public LayoutAnimationController.AnimationParameters layoutAnimationParameters; /** * Creates a new set of layout parameters. The values are extracted from * the supplied attributes set and context. The XML attributes mapped * to this set of layout parameters are:、 */ public LayoutParams(Context c, AttributeSet attrs) { TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); a.recycle(); } /** * Creates a new set of layout parameters with the specified width * and height. */ public LayoutParams(int width, int height) { this.width = width; this.height = height; } /** * Copy constructor. Clones the width and height values of the source. * * @param source The layout params to copy from. */ public LayoutParams(LayoutParams source) { this.width = source.width; this.height = source.height; } /** * Used internally by MarginLayoutParams. * @hide */ LayoutParams() { } /** * Extracts the layout parameters from the supplied attributes. * * @param a the style attributes to extract the parameters from * @param widthAttr the identifier of the width attribute * @param heightAttr the identifier of the height attribute */ protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { width = a.getLayoutDimension(widthAttr, "layout_width"); height = a.getLayoutDimension(heightAttr, "layout_height"); } }
我们发现FILL_PARENT/MATCH_PARENT值为 -1 ,WRAP_CONETENT值为-2,是不是有点诧异? 将值
设置为负值的目的是为了区别View的具体值(an exact size) 总是大于0的。
ViewGroup子类可以实现自定义LayoutParams,自定义LayoutParams提供了更好地扩展性,例如LinearLayout
就有LinearLayout.LayoutParams自定义类(见下文)。整个LayoutParams类家族还是挺复杂的。
ViewGroup.LayoutParams及其常用派生类的类图(部分类图)如下:
该类图是在太庞大了,大家有兴趣的去看看Android API吧。
前面我们说过,每个View都包含一个ViewGroup.LayoutParams类或者其派生类,下面我们的疑问是Android框架
中时如何为View设置其LayoutParams属性的。
有两种方法会设置View的LayoutParams属性:
1、 直接添加子View时,常见于如下几种方法:ViewGroup.java
//Adds a child view. void addView(View child, int index) //Adds a child view with this ViewGroup's default layout parameters //and the specified width and height. void addView(View child, int width, int height) //Adds a child view with the specified layout parameters. void addView(View child, ViewGroup.LayoutParams params)
三个重载方法的区别只是添加View时构造LayoutParams对象的方式不同而已,稍后我们探寻一下它们的源码。
2、 通过xml布局文件指定某个View的属性为:android:layout_heigth=””以及android:layout_weight=”” 时。
总的来说,这两种方式都会设定View的LayoutParams属性值----指定的或者Default值。
直接添加子View时,比较容易理解,我们先来看看这种方式设置LayoutParams的过程:
路径:\frameworks\base\core\java\android\view\ViewGroup.java
public abstract class ViewGroup extends View implements ViewParent, ViewManager { ... /** * Adds a child view. If no layout parameters are already set on the child, the * default parameters for this ViewGroup are set on the child. * * @param child the child view to add * * @see #generateDefaultLayoutParams() */ public void addView(View child) { addView(child, -1); } /** * Adds a child view. If no layout parameters are already set on the child, the * default parameters for this ViewGroup are set on the child. * * @param child the child view to add * @param index the position at which to add the child * * @see #generateDefaultLayoutParams() */ public void addView(View child, int index) { LayoutParams params = child.getLayoutParams(); if (params == null) { params = generateDefaultLayoutParams(); //返回默认地LayoutParams类,作为该View的属性值 if (params == null) {//如果不能获取到LayoutParams对象,则抛出异常。 throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); } } addView(child, index, params); } /** * Adds a child view with this ViewGroup's default layout parameters and the * specified width and height. * * @param child the child view to add */ public void addView(View child, int width, int height) { //返回默认地LayoutParams类,作为该View的属性值 final LayoutParams params = generateDefaultLayoutParams(); params.width = width; //重新设置width值 params.height = height; //重新设置height值 addView(child, -1, params); //这儿,我们有指定width、height的大小了。 } /** * Adds a child view with the specified layout parameters. * * @param child the child view to add * @param params the layout parameters to set on the child */ public void addView(View child, LayoutParams params) { addView(child, -1, params); } /** * Adds a child view with the specified layout parameters. * * @param child the child view to add * @param index the position at which to add the child * @param params the layout parameters to set on the child */ public void addView(View child, int index, LayoutParams params) { ... // addViewInner() will call child.requestLayout() when setting the new LayoutParams // therefore, we call requestLayout() on ourselves before, so that the child's request // will be blocked at our level requestLayout(); invalidate(); addViewInner(child, index, params, false); } /** * Returns a set of default layout parameters. These parameters are requested * when the View passed to {@link #addView(View)} has no layout parameters * already set. If null is returned, an exception is thrown from addView. * * @return a set of default layout parameters or null */ protected LayoutParams generateDefaultLayoutParams() { //width 为 WRAP_CONTENT大小 , height 为WRAP_CONTENT //ViewGroup的子类可以重写该方法,达到其特定要求。稍后会以LinearLayout类为例说明。 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { if (!checkLayoutParams(params)) { //params对象是否为null params = generateLayoutParams(params); //如果params对象是为null,重新构造个LayoutParams对象 } //preventRequestLayout值为false if (preventRequestLayout) { child.mLayoutParams = params; //为View的mLayoutParams属性赋值 } else { child.setLayoutParams(params);//为View的mLayoutParams属性赋值,但会调用requestLayout()请求重新布局 } //if else 语句会设置View为mLayoutParams属性赋值 ... } ... }
主要功能就是在添加子View时为其构建了一个LayoutParams对象。但更重要的是,ViewGroup的子类可以重载
上面的几个方法,返回特定的LayoutParams对象,例如:对于LinearLayout而言,则是LinearLayout.LayoutParams
对象。这么做地目的是,能在其他需要它的地方,可以将其强制转换成LinearLayout.LayoutParams对象。
LinearLayout重写函数地实现为:
public class LinearLayout extends ViewGroup { ... @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LinearLayout.LayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { //该LinearLayout是水平方向还是垂直方向 if (mOrientation == HORIZONTAL) { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } else if (mOrientation == VERTICAL) { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } return null; } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } /** * Per-child layout information associated with ViewLinearLayout. * * @attr ref android.R.styleable#LinearLayout_Layout_layout_weight * @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity */ //自定义的LayoutParams类 public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * Indicates how much of the extra space in the LinearLayout will be * allocated to the view associated with these LayoutParams. Specify * 0 if the view should not be stretched. Otherwise the extra pixels * will be pro-rated among all views whose weight is greater than 0. */ @ViewDebug.ExportedProperty(category = "layout") public float weight; // 见于属性,android:layout_weight="" ; /** * Gravity for the view associated with these LayoutParams. * * @see android.view.Gravity */ public int gravity = -1; // 见于属性, android:layout_gravity="" ; /** * {@inheritDoc} */ public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a =c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout); weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0); gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1); a.recycle(); } /** * {@inheritDoc} */ public LayoutParams(int width, int height) { super(width, height); weight = 0; } /** * Creates a new set of layout parameters with the specified width, height * and weight. * * @param width the width, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels * @param height the height, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels * @param weight the weight */ public LayoutParams(int width, int height, float weight) { super(width, height); this.weight = weight; } public LayoutParams(ViewGroup.LayoutParams p) { super(p); } public LayoutParams(MarginLayoutParams source) { super(source); } } ... }
LinearLayout.LayoutParams类继承至ViewGroup.MarginLayoutParams类,添加了对android:layout_weight以及
android:layout_gravity这两个属性的获取和保存。而且它的重写函数返回的都是LinearLayout.LayoutParams
类型。这样,我们可以再对子View进行其他操作时,可以将将其强制转换成LinearLayout.LayoutParams对象进行
使用。
例如,LinearLayout进行measure过程,使用了LinearLayout.LayoutParam对象,有如下代码:
public class LinearLayout extends ViewGroup { ... @Override //onMeasure方法。 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //判断是垂直方向还是水平方向,这儿我们假设是VERTICAL垂直方向, if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } /** * Measures the children when the orientation of this LinearLayout is set * to {@link #VERTICAL}. * * @param widthMeasureSpec Horizontal space requirements as imposed by the parent. * @param heightMeasureSpec Vertical space requirements as imposed by the parent. * * @see #getOrientation() * @see #setOrientation(int) * @see #onMeasure(int, int) */ void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; ... // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); //获得索引处为i的子VIew ... //注意,我们将类型为 ViewGroup.LayoutParams的实例对象强制转换为了LinearLayout.LayoutParams, //即父对象转换为了子对象,能这样做的原因就是LinearLayout的所有子View的LayoutParams类型都为 //LinearLayout.LayoutParams LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); ... } ... }
超类ViewGroup.LayoutParams强制转换为了子类LinearLayout.LayoutParams,因为LinearLayout的每个
”直接“子View的LayoutParams属性都是LinearLayout.LayoutParams类型,因此可以安全转换。
PS : Android 2.3源码Launcher2中也实现了自定义的LayoutParams类,在IDLE界面的每个View至少包含如下
信息:所在X方向的单元格索引和高度、所在Y方向的单元格索引和高度等。
路径:packages\apps\Launcher2\src\com\android\launcher2\CellLayout.java
public class CellLayout extends ViewGroup { ... public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * Horizontal location of the item in the grid. */ public int cellX; //X方向的单元格索引 /** * Vertical location of the item in the grid. */ public int cellY; //Y方向的单元格索引 /** * Number of cells spanned horizontally by the item. */ public int cellHSpan; //水平方向所占高度 /** * Number of cells spanned vertically by the item. */ public int cellVSpan; //垂直方向所占高度 ... public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); cellHSpan = 1; //默认为高度 1 cellVSpan = 1; } public LayoutParams(ViewGroup.LayoutParams source) { super(source); //默认为高度 1 cellHSpan = 1; cellVSpan = 1; } public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); this.cellX = cellX; this.cellY = cellY; this.cellHSpan = cellHSpan; this.cellVSpan = cellVSpan; } ... } ... }
对该自定义CellLayout.LayoutParams类的使用可以参考LinearLayout.LayoutParams类,我也不再赘述了。
使用属性android:layout_heigth=””以及android:layout_weight=”” 时,为某个View设置LayoutParams值。
其实这种赋值方法其实也如同前面那种,只不过它需要一个前期孵化过程---需要利用XML解析将布局文件
解析成一个完整的View树,可别小看它了,所有Xxx.xml的布局文件都需要解析成一个完整的View树。下面,
我们就来仔细走这个过程,重点关注如下两个方面
①、xml布局是如何解析成View树的 ;
②、android:layout_heigth=””和android:layout_weight=””的解析。
PS: 一直以来,我都想当然android:layout_heigth以及android:layout_weight这两个属性的解析过程是在
View.java内部完成的,但当我真正去找寻时,却一直没有在View.java类或者ViewGroup.java类找到。直到一位
网友的一次提问,才发现它们的藏身之地。
解析布局文件时,使用的类为LayoutInflater。 关于该类的使用请参考如下博客:
主要有如下API方法:
publicViewinflate(XmlPullParserparser,ViewGrouproot, boolean attachToRoot)
publicViewinflate(int resource,ViewGrouproot)
publicViewinflate(int resource,ViewGrouproot, boolean attachToRoot)
这三个类主要迷惑之处在于地三个参数attachToRoot,即是否将该View树添加到root中去。具体可看这篇博客:
<<关于inflate的第3个参数>>
当然还有LayoutInflater的inflate()的其他重载方法,大家可以自行了解下。
我利用下面的例子给大家走走这个流程 :
public class MainActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //1、该方法最终也会调用到 LayoutInflater的inflate()方法中去解析。 setContentView(R.layout.main); //2、使用常见的API方法去解析xml布局文件, LayoutInflater layoutInflater = (LayoutInflater)getSystemService(); View root = layoutInflater.inflate(R.layout.main, null); } }
Step 1、获得LayoutInflater的引用。
路径:\frameworks\base\core\java\android\app\ContextImpl.java
/** * Common implementation of Context API, which provides the base * context object for Activity and other application components. */ class ContextImpl extends Context { if (WINDOW_SERVICE.equals(name)) { return WindowManagerImpl.getDefault(); } else if (LAYOUT_INFLATER_SERVICE.equals(name)) { synchronized (mSync) { LayoutInflater inflater = mLayoutInflater; //是否已经赋值,如果是,直接返回引用 if (inflater != null) { return inflater; } //返回一个LayoutInflater对象,getOuterContext()指的是我们的Activity、Service或者Application引用 mLayoutInflater = inflater = PolicyManager.makeNewLayoutInflater(getOuterContext()); return inflater; } } else if (ACTIVITY_SERVICE.equals(name)) { return getActivityManager(); }... }
继续去PolicyManager查询对应函数,看看内部实现。
路径:frameworks\base\core\java\com\android\internal\policy\PolicyManager.java
public final class PolicyManager { private static final String POLICY_IMPL_CLASS_NAME = "com.android.internal.policy.impl.Policy"; private static final IPolicy sPolicy; // 这可不是Binder机制额,这只是是一个接口,别想多啦 static { // Pull in the actual implementation of the policy at run-time try { Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME); sPolicy = (IPolicy)policyClass.newInstance(); } ... } ... public static LayoutInflater makeNewLayoutInflater(Context context) { return sPolicy.makeNewLayoutInflater(context); //继续去实现类中去查找 } }
IPolicy接口的实现对为Policy类。路径:/frameworks/base/policy/src/com/android/internal/policy/impl/Policy.java
//Simple implementation of the policy interface that spawns the right //set of objects public class Policy implements IPolicy{ ... public PhoneLayoutInflater makeNewLayoutInflater(Context context) { //实际上返回的是PhoneLayoutInflater类。 return new PhoneLayoutInflater(context); } } //PhoneLayoutInflater继承至LayoutInflater类 public class PhoneLayoutInflater extends LayoutInflater { ... /** * Instead of instantiating directly, you should retrieve an instance * through {@link Context#getSystemService} * * @param context The Context in which in which to find resources and other * application-specific things. * * @see Context#getSystemService */ public PhoneLayoutInflater(Context context) { super(context); } ... }
LayoutInflater是个抽象类,实际上我们返回的是PhoneLayoutInflater类,但解析过程的操作基本上是在
LayoutInflater中完成地。
Step 2、调用inflate()方法去解析布局文件。
public abstract class LayoutInflater { ... public View inflate(int resource, ViewGroup root) { //继续看下个函数,注意root为null return inflate(resource, root, root != null); } public View inflate(int resource, ViewGroup root, boolean attachToRoot) { //获取一个XmlResourceParser来解析XML文件---布局文件。 //XmlResourceParser类以及xml是如何解析的,大家自己有兴趣找找。 XmlResourceParser parser = getContext().getResources().getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } } /** * The XML parsing interface returned for an XML resource. This is a standard * XmlPullParser interface, as well as an extended AttributeSet interface and * an additional close() method on this interface for the client to indicate * when it is done reading the resource. */ public interface XmlResourceParser extends XmlPullParser, AttributeSet { /** * Close this interface to the resource. Calls on the interface are no * longer value after this call. */ public void close(); }
我们获得了一个当前应用程序环境的XmlResourceParser对象,该对象的主要作用就是来解析xml布局文件的。
XmlResourceParser类是个接口类,更多关于XML解析的,大家可以参考下面博客:
<<android之XmlResourceParser类使用实例>>
Step 3 、真正地开始解析工作 。
public abstract class LayoutInflater { ... /** * Inflate a new view hierarchy from the specified XML node. Throws * {@link InflateException} if there is an error. */ //我们传递过来的参数如下: root 为null , attachToRoot为false 。 public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; mConstructorArgs[0] = mContext; //该mConstructorArgs属性最后会作为参数传递给View的构造函数 View result = root; //根View try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } ... final String name = parser.getName(); //节点名,即API中的控件或者自定义View完整限定名。 if (TAG_MERGE.equals(name)) { // 处理<merge />标签 if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } //将<merge />标签的View树添加至root中,该函数稍后讲到。 rInflate(parser, root, attrs); } else { // Temp is the root view that was found in the xml //创建该xml布局文件所对应的根View。 View temp = createViewFromTag(name, attrs); ViewGroup.LayoutParams params = null; if (root != null) { // Create layout params that match root, if supplied //根据AttributeSet属性获得一个LayoutParams实例,记住调用者为root。 params = root.generateLayoutParams(attrs); if (!attachToRoot) { //重新设置temp的LayoutParams // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } // Inflate all children under temp //添加所有其子节点,即添加所有字View rInflate(parser, temp, attrs); // We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { 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; } } } ... return result; } } /* * default visibility so the BridgeInflater can override it. */ View createViewFromTag(String name, AttributeSet attrs) { //节点是否为View,如果是将其重新赋值,形如 <View class="com.qin.xxxView"></View> if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } try { View view = (mFactory == null) ? null : mFactory.onCreateView(name, mContext, attrs); //没有设置工厂方法 if (view == null) { //通过这个判断是Android API的View,还是自定义View if (-1 == name.indexOf('.')) { view = onCreateView(name, attrs); //创建Android API的View实例 } else { view = createView(name, null, attrs);//创建一个自定义View实例 } } return view; } ... } //获得具体视图的实例对象 public final View createView(String name, String prefix, AttributeSet attrs) { Constructor constructor = sConstructorMap.get(name); Class clazz = null; //以下功能主要是获取如下三个类对象: //1、类加载器 ClassLoader //2、Class对象 //3、类的构造方法句柄 Constructor try { if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name); ... constructor = clazz.getConstructor(mConstructorSignature); sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { ... } } //传递参数获得该View实例对象 Object[] args = mConstructorArgs; args[1] = attrs; return (View) constructor.newInstance(args); } ... } }
这段代码的作用是获取xml布局文件的root View,做了如下两件事情
1、获取xml布局的View实例,通过createViewFromTag()方法获取,该方法会判断节点名是API 控件
还是自定义控件,继而调用合适的方法去实例化View。
2、判断root以及attachToRoot参数,重新设置root View值以及temp变量的LayoutParams值。
如果仔细看着段代码,不知大家心里有没有疑惑:当root为null时,我们的temp变量的LayoutParams值是为
null的,即它不会被赋值?有个View的LayoutParams值为空,那么,在系统中不会报异常吗?见下面部分
代码:
//我们传递过来的参数如下: root 为null , attachToRoot为false 。 public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { ... try { ... if (TAG_MERGE.equals(name)) { // 处理<merge />标签 ... } else { // Temp is the root view that was found in the xml //创建该xml布局文件所对应的根View。 View temp = createViewFromTag(name, attrs); ViewGroup.LayoutParams params = null; //注意!!! root为null时,temp变量的LayoutParams属性不会被赋值的。 if (root != null) { // Create layout params that match root, if supplied //根据AttributeSet属性获得一个LayoutParams实例,记住调用者为root。 params = root.generateLayoutParams(attrs); if (!attachToRoot) { //重新设置temp的LayoutParams // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } ... } } ... } }
关于这个问题的详细答案,我会在后面讲到。这儿我简单说下,任何View树的顶层View被添加至窗口时,
一般调用WindowManager.addView()添加至窗口时,在这个方法中去做进一步处理。即使,LayoutParams
值为空,UI框架每次measure()时都忽略该View的LayoutParams值,而是直接传递MeasureSpec值至View树。
接下来,我们关注另外一个函数,rInflate(),该方法会递归调用每个View下的子节点,以当前View作为根View
形成一个View树。
/** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). */ //递归调用每个字节点 private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) 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)) { //处理<requestFocus />标签 parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { //处理<include />标签 if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, parent, attrs);//解析<include />节点 } else if (TAG_MERGE.equals(name)) { //处理<merge />标签 throw new InflateException("<merge /> must be the root element"); } else { //根据节点名构建一个View实例对象 final View view = createViewFromTag(name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; //调用generateLayoutParams()方法返回一个LayoutParams实例对象, final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs); //继续递归调用 viewGroup.addView(view, params); //OK,将该View以特定LayoutParams值添加至父View中 } } parent.onFinishInflate(); //完成了解析过程,通知.... }
值得注意的是,每次addView前都调用了viewGroup.generateLayoutParams(attrs)去构建一个LayoutParams
实例,然后在addView()方法中为其赋值。参见如下代码:ViewGroup.java
public abstract class ViewGroup extends View implements ViewParent, ViewManager { ... public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } public static class LayoutParams { ... //会调用这个构造函数 public LayoutParams(Context c, AttributeSet attrs) { TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); a.recycle(); } protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { width = a.getLayoutDimension(widthAttr, "layout_width"); height = a.getLayoutDimension(heightAttr, "layout_height"); } }
好吧 ~~ 我们还是探寻根底,去TypeArray类的getLayoutDimension()看看。
路径:/frameworks/base/core/java/android/content/res/TypedArray.java
public class TypedArray { ... /** * Special version of {@link #getDimensionPixelSize} for retrieving * {@link android.view.ViewGroup}'s layout_width and layout_height * attributes. This is only here for performance reasons; applications * should use {@link #getDimensionPixelSize}. * * @param index Index of the attribute to retrieve. * @param name Textual name of attribute for error reporting. * * @return Attribute dimension value multiplied by the appropriate * metric and truncated to integer pixels. */ public int getLayoutDimension(int index, String name) { index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; //获得属性对应的标识符 , Identifies,目前还没有仔细研究相关类。 final int type = data[index+AssetManager.STYLE_TYPE]; if (type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { //类型为dimension类型 return TypedValue.complexToDimensionPixelSize( data[index+AssetManager.STYLE_DATA], mResources.mMetrics); } //没有提供layout_weight和layout_height会来到此处 ,这儿会报异常! //因此布局文件中的View包括自定义View必须加上属性layout_weight和layout_height。 throw new RuntimeException(getPositionDescription() + ": You must supply a " + name + " attribute."); } ... }
从上面得知, 我们将View的AttributeSet属性传递给generateLayoutParams()方法,让其构建合适地
LayoutParams对象,并且初始化属性值weight和height。同时我们也得知布局文件中的View包括自定义View
必须加上属性layout_weight和layout_height,否则会报异常。
Step 3 主要做了如下事情:
首先,获得了了布局文件地root View,即布局文件中最顶层的View。
其次,通过递归调用,我们形成了整个View树以及设置了每个View的LayoutParams对象。
总结:通过对布局文件的解析流程的学习,也就是转换为View树的过程,我们明白了解析过程的个中奥妙,以及
设置ViewLayoutParams对象的过程。但是,我们这儿只是简单的浮光掠影,更深层次的内容希望大家能深入学习。
本来是准备接下去往下写的,但无奈贴出来的代码太多,文章有点长而且自己也有点凌乱了,因此决定做两篇
博客发表吧。下篇内容包括如下方面:
1、MeasureSpec类说明 ;
2、measure过程中如何正确设置每个View的长宽 ;
3、UI框架正确设置顶层View的LayoutParams对象,对Activity而言,顶层View则是DecorView,
其他的皆是普通View了。