每一个View/ViewGroup的显示都会经过三个过程:
1、measure过程(测量View显示的大小,位置);
2、layout过程(布局view的位置);
3、draw过程(上一篇文章说到的通过canvas绘制到界面上显示,形成了各色的View)
下面分析一下各个过程:
measure过程:
因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌View或ViewGroup形成一个View树。所以measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局大小(“窗口”大小)。实际对应属性就是View中的mMeasuredHeight(高)和mMeasureWidth(宽)。
View在Measure过程中,最主要的三个方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)。(此方法在onMesare方法里面调用,如果没有调用需要手动调用)
前面两个函数都是final类型的,不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数,实现measure的原理是:假如View还有子View,则measure子View,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChild或measureChildWithMargins就是实现这个功能的。各个View/ViewGroup最终调用setMeasuredDimension方法,设置View的大小,如果没有调用它,测量是不起作用的。
在具体介绍测量原理之前还是先了解些基础知识,即measure函数的参数由类measureSpec的makeMeasureSpec函数方法生成的一个32位整数,该整数的高两位表示模式(Mode),低30位则是具体的尺寸大小(specSize)。
MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLY和AT_MOST,各表示的意义如下:
如果是AT_MOST,specSize代表的是最大可获得的尺寸;
如果是EXACTLY,specSize代表的是精确的尺寸;
如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。
那么对于一个View的上述Mode和specSize值默认是怎么获取的呢,他们是根据View的LayoutParams参数来获取的:
参数为fill_parent/match_parent时,Mode为EXACTLY,specSize为剩余的所有空间;
参数为具体的数值,比如像素值(px或dp),Mode为EXACTLY,specSize为传入的值;
参数为wrap_content,Mode为AT_MOST,specSize运行时决定
具体测量原理
上面提供的Mode和specSize只是程序对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得。
根据源码View的OnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeight和android:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。
layout过程:
上述measure过程达到的结果是设定了视图的高和宽,layout过程的作用就是设定视图在父视图中的四个点(分别对应View四个成员变量mLeft,mTop,mRight,mBottom)。同样layout也是被fianl修饰符限定为不能重载,不过在ViewGroup中onLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有子视图的布局设定。
我们来看任意一个Layout布局,此就用LinearLayout为例,onLayout(layoutVertical方法)方法部分源码:
void layoutVertical() { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // Where right end of child should go final int width = mRight - mLeft; int childRight = width - mPaddingRight; // Space available for child int childSpace = width - paddingLeft - mPaddingRight; final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; switch (majorGravity) { case Gravity.BOTTOM: // mTotalLength contains the padding already childTop = mPaddingTop + mBottom - mTop - mTotalLength; break; // mTotalLength contains the padding already case Gravity.CENTER_VERTICAL: childTop = mPaddingTop + (mBottom - mTop - mTotalLength) / 2; break; case Gravity.TOP: default: childTop = mPaddingTop; break; } for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); int gravity = lp.gravity; if (gravity < 0) { gravity = minorGravity; } final int layoutDirection = getResolvedLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
从代码显然可知具体layout布局时,就是根据measure过程设置的高和宽,结合视图在父视图中的起始位置,再外加视图的layoutgravity属性来设置四个点的具体位置(在LinearLayout中还会增加对layoutweight属性的考虑)。这个过程相对没有measure那么复杂。我们在写控件的时候,我们可以根据自己的需要利用measure计算的结果来控制各个控件的位置,实现各种绚丽的动画交互。
这是我在项目中写的一个利用measure+layout写的一个LineLayout,实现显示一行,左边显示文字,右边显示Icon;当文字照过一屏的时候,自动截取打省略号,右边Icon需要一直显示;当然还有诸多缺陷与待改进的地方,期望指出与改进。