上一章讲了 View 的测量、布局和绘制,这一章对应的是 ViewGroup 的测量、布局和绘制,重点讲解测量,其次是布局,绘制基本为零。
ViewGroup的Measure过程,如果我们去读它的源码,发现没有 onMeasure() 方法,是不需要吗?答案是否定的,应为 ViewGroup 是个抽象类,同时也是 View 的子类,我们使用容器时,一般都是要实现各种功能的容器,所以这个测量就要根据种类来做相应的定制,比如 自适应布局的情况下, FrameLayout 是帧布局,它的大小就是子类中view最大的宽和高;
LinearLayout 是线性布局,根据垂直或水平,它的宽或高是根据子view的宽或高的相加;RelativeLayout 是相对布局,根据相对位置经历了两次计算才决定了宽和高。所以,我们自定义容器时,就要根据我们的需求,去求取对应的值,直接说的话比较笼统,以最简单的 FrameLayout 为例,分析一下。
上一章我们知道,MeasureSpec一共有三种模式,分别代表着
UPSPECIFIED : 父容器对子容器没有任何大小限制,子容器想要多大就可以由多大
EXACTLY: 父容器已经为子容器设置了尺寸,子容器需要服从这些边界, 具体的值,或者match_parent
AT_MOST:子容器可以是父容器大小内的任意大小, wrap_content
我们看看FrameLayout的源码, 简化版的 onMeasure() 方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
}
我们先看这一段截断版的,意思就是把FrameLayout中没有gone的子view,都遍历一遍,我们注意,理解的重点放到
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
上,我们知道,widthMeasureSpec 是 FrameLayout 的宽,heightMeasureSpec是 FrameLayout 的高,我们点击进去,看看这个方法的代码
ViewGroup 中的源码
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
/根据父View的测量规格和父View自己的Padding、子View的Margin,及widthUsed,算出子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
注意,我们看到了,在这个方法里,获取到了 View child 也就是子view的宽和高,然后调用了 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 而View
的 onMeasure(widthMeasureSpec, heightMeasureSpec) 方法就在 measure(int widthMeasureSpec, int heightMeasureSpec) 中调用,由此测量自己的宽高,看来子view的宽和高也是由上一层传进来的,我们这时就看看获取宽高的方法,看看子view的宽高是如何被父view所限制和决定的,看看 getChildMeasureSpec()方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
方法比较长,截取第一部分,先把一种情况讲清楚,然后再继续。
第一个参数 spec 是 parentWidthMeasureSpec,也就是父View的规格;第二个参数 padding 是 父View的Padding和子View的margin及widthUsed之和;第三个参数 childDimension 是子View 的布局中设计的宽或高,即 MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); lp.width 这个值。 刚进方法,先获取父View的规格,拿到宽和Mode格式,然后会比较一下父View的宽和 第二个参数 padding 的差,如果 padding 大,说明已经没有地方给子View的空间了,size值为0,;如果父View减去padding还有剩余,那么size取这个差值,子View在这个值的范围内可以被绘制。 补充一点,MeasureSpec的模式size值,-1 代表的是EXACTLY,-2 是AT_MOST, 其他大于等于0的值代表具体值。int resultSize = 0; 为子View的宽的size初始值,int resultMode = 0; 为子View的宽的mode初始值。假设父View的mode模式为EXACTLY,我们对照上面的代码继续看,childDimension 是子View的 MeasureSpec的模式size值,大于等于0说明是具体值,此时,我们所求的子View的宽 resultSize = childDimension; 即设置的值不用改变,由于是具体的值,所以mode为resultMode =MeasureSpec.EXACTLY; 标识是具体的值。 childDimension 为 MATCH_PARENT 即为-1时,是铺满父View的剩余的地方,所以值为resultSize = size; int size = Math.max(0, specSize- padding); 由于size是固定的,所以 resultMode = MeasureSpec.EXACTLY; 即mode标识的也是具体的值。 childDimension为 WRAP_CONTENT 即-2时,意思是不超过父View,这时候就把父View剩余的地方都给它,mode值为-2,即 resultSize = size; resultMode = MeasureSpec.AT_MOST; 。
同理,只要子View的值是具体的,都是 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; 父View为 AT_MOST 的情况下,这就表示父View自己也不确定自己的大小,所以不论子View的模式是 MATCH_PARENT 或 WRAP_CONTENT, 子View的mode都是AT_MOST,因为即使子View的mode本身是 MATCH_PARENT 铺满父View,父View根本不知道自己有多大,所以就把子View的mode强制改为了 AT_MOST ,即 resultSize = size; resultMode = MeasureSpec.AT_MOST; 。 父View为 UNSPECIFIED 的情况下,UNSPECIFIED意思是不限制大小,所以不管子View的模式是 MATCH_PARENT 或 WRAP_CONTENT, 子view的值已无意义,只要把子View的mode修改为 UNSPECIFIED即可,即 resultSize = 0;resultMode =MeasureSpec.UNSPECIFIED;
就这样,我们在xml布局中写的view的属性,都需要经过父View的修改,最终确定其大小,我们继续看FrameLayout的onMeasure()方法,里面会比较得到其子View的宽和高,并求出最大值,然后会和FrameLayout设置的最小的宽和高及背景图的宽和高做比较,选出其中大的值,然后重点来了
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,childState << MEASURED_HEIGHT_STATE_SHIFT));
这个是设置FrameLayout的宽和高的方法,我们看看里面 resolveSizeAndState(maxWidth, widthMeasureSpec, childState) 这个方法,这个方法上一章说过,这里再重复一遍
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
第一个参数size是计算的最大值 maxWidth, 第二个参数measureSpec是FrameLayout的宽的值,第三个参数childMeasuredState是子View们的childState,如果不理解,可以认为是0。然后先获取FrameLayout的 specSize 和 specMode,如果FrameLayout的mode是 EXACTLY ,则置为 specSize;如果是UNSPECIFIED 不限制大小,则maxWidth,这个其实意义不大;精彩的来了,如果是AT_MOST,即包裹内容,那么我们会做一个判断,取 maxWidth 和 specSize 其中小的值作为标准,同时 如果是 specSize 小于 maxWidth,我们还会进行为运算,添加一个布局变小的标识。
假设xml根节点是FrameLayout,由于content中根节点也依附于PhoneWindow中的DecorView,DecorView的宽和高都是match_parent,所以我们如果布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/red">
<View
android:background="@color/white"
android:layout_width="wrap_content"
android:layout_height="100dp"/>
</FrameLayout>
此时,上面一部分是白色,剩余部分被红色铺满;
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/main_red_day"
android:paddingBottom="100dp"
>
<View
android:background="@color/cardview_dark_background"
android:layout_width="wrap_content"
android:layout_height="100dp"/>
</FrameLayout>
这时候会发现,上面是灰色,中间是红色,下面是白色。为什么呢?灰色是子View的颜色,红色是FrameLayout的颜色,因为android:paddingBottom="100dp",下面的白色实际上是DecorView它的颜色,由于FrameLayout的格式是wrap_content,再加上子View的高度+FrameLayout的padding值没有超过屏幕,所以就是这样了。这样的计算模式正是印证了resolveSizeAndState()方法中的判断。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/main_red_day"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/food_ratingbar"
/>
</FrameLayout>
food_ratingbar 是个星星的图片,这时候会正常显示,只有星星的背景也就是FrameLayout是红色的,其他都是白色。这时候还算正常,但是,如果背景图换成颜色的背景图,那么
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/main_red_day"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/dark" // 灰色
/>
</FrameLayout>
这时候,ImageView的宽和高都换成了wrap_content, 整体都是白色,根本就没灰色和红色,
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/main_red_day"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/dark" // 灰色
/>
</FrameLayout>
ImageView的宽和高都换成了 match_parent ,还是没有红色和灰色。什么呢?原因在于ImageView 的 onMeasure() 方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
resolveUri();
...
// We are allowed to change the view's width
boolean resizeWidth = false;
boolean resizeHeight = false;
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (mDrawable == null) {
// If no drawable, its intrinsic size is 0.
mDrawableWidth = -1;
mDrawableHeight = -1;
w = h = 0;
} else {
w = mDrawableWidth;
h = mDrawableHeight;
if (w <= 0) w = 1;
if (h <= 0) h = 1;
if (mAdjustViewBounds) {
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
desiredAspect = (float) w / (float) h;
}
}
if (resizeWidth || resizeHeight) {
...
} else {
// 走到这里 *****
w += pleft + pright;
h += ptop + pbottom;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
setMeasuredDimension(widthSize, heightSize);
}
按照我们的写法,先执行resolveUri();方法,最终到 updateDrawable(Drawable d) 方法,获取到 mDrawableWidth = d.getIntrinsicWidth(); mDrawable 为 color 的drawable,ColorDrawable 中没有复写这个方法,父类中值为-1;继续往 下看,mDrawable 不为 null,则第一个if中,走到了else中,此时 w 和 h 均为1,,mAdjustViewBounds默认值为false,所以最后一个else的地方:走到这里*****, 然后 pleft pright ptop pbottom 四个值是 padding,都为0,此时,w 和 h 还是1,然后进入了最后计算阶段,resolveSizeAndState()方法,我们看看它的源码这不就是刚才说的那个方法吗?经过一番对比取值,很明显,最终 resolveSizeAndState(w, widthMeasureSpec, 0) 方法中,w的值为1,widthMeasureSpec 的值为屏幕的宽,假设为1080*1920的手机,那么widthMeasureSpec值为1080,此时 resolveSizeAndState()方法中的 imageView 的 mode 为 AT_MOST, 此时,取最小的值,即 1,也就是说,widthSize = 1,同理 heightSize = 1,然后调用 setMeasuredDimension(widthSize, heightSize); 方法,imageView 的宽高都是1px,差一点的手机根本就显示不出来,由于FrameLayout 也是包裹内容,所以FrameLayout的宽高也都是1px,所以填充color时什么也没显示。 同理,imageView 填充星星图片时,由于Drawable图片有具体的宽和高,所以就显示出来了。
下面看一下 layout 的过程,ViewGroup这个onLayout()方法是抽象的,需要子类去实现,重写实现布局。还是以FrameLayout为例,我们在onMeasure中计算出了子View的宽和高,在onLayout()方法中,我们先减去FrameLayout的本身的padding值,计算出可以布局子View的区域,然后根据子View的宽和高,及 Gravity 属性,来计算出每个view应该所在的位置,计算出每一个View的左上角的坐标,距离父View也就是FrameLayout的左边和上面的距离,有了左上角的坐标,图片本身的宽和高,那么就计算出图片由下角的坐标,有了这四个坐标,直接调用 View 的 layout() 方法,系统就开始布局位置了。
最后一个 onDraw() 方法,ViewGroup 一般用不到,ViewGroup 的子类属于容器级别,一般都是用来装view的,很少自己绘制东西的,如果有特殊需求,可以像在View中使用onDraw() 方法一样,去使用。
不管是View 还是 ViewGroup, onMeasure() 都是重头戏。绘制宽高理解了,剩下两个就好理解了。