本篇你将了解到:
-
LayoutParams
和MeasureSpec
的作用和使用场景 - 父 View 如何使用
MeasureSpec
影响子 View 的测量 - 重写
onMeasure
的作用
View 的绘制过程,主要体现在 onMeasure()
、onLayout()
和 onDraw()
这三个方法,这三部对应着一个 View 的测量,布局,绘制三个步骤。
在讲 onMeasure()
方法之前,我们先讲一下 Android 中,测绘中常用的参数类 -- LayoutParams
和 MeasureSpec
。
LayoutParams
LayoutParams
类只简单的描述了宽高,他们往往被设置成以下这三种值:
- 一个确定的值;
- FILL_PARENT,即填满父容器;
- WRAP_CONTENT,刚刚好包裹组件;
常见的使用方式如下:
// FrameLayout 的子 View
FrameLayout.LayoutParams lytp = new FrameLayout.LayoutParams(80, LayoutParams.WRAP_CONTENT);
lytp .gravity = Gravity.CENTER;
btn.setLayoutParams(lytp);
// RelativeLayout 的子 View
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
btn1.setLayoutParams(lp);
MeasureSpec
MeasureSpec
是父控件提供给子 View 的一个参数,作为设定自身大小参考,而子 View 也可以完全不参照这个值,设定自己的大小。
MeasureSpec
由 size 和 mode 构成,size 代表着子 View 当前所在父布局的具体尺寸,其中 mode 包括以下三种:
- EXACTLY:父布局为子 View 指定确切大小,希望子 View 完全按照自己给定尺寸来处理。
- AT_MOST:父布局为子元素指定最大参考尺寸,希望子 View 的尺寸不要超过这个尺寸。
-
UNSPECIFIED:父布局对子控件不加任何束缚,这种情况一般出现在
ScrollView
这种不限制大小的特殊父控件。
ViewGroup 如何测量子 View
看源码:
// 测量子 View
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 获取 child 的 LayoutParams
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/* 获取子 View 的 MeasureSpec
* @param spec 子 View MeasureSpec
* @param padding 当前 View 的 padding 大小,需要被减去
* @param childDimension 子 View LayoutParams 的height,width
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 父 View 推荐的 Size 需要留有一部给 padding
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;
...
// 省略了 MeasureSpec.AT_MOST 和 MeasureSpec.UNSPECIFIED
}
}
看完源码后,大致能了解到测量是由父 View 的 MeasureSpec
和子 View 的 LayoutParams
一起决定的。更具体的对应关系如下:
-
子 View 指定大小值时:
- Mode = EXACTLY
- Size = 指定的大小
-
子View指定为 MATCH_PARENT 时:
- Mode = 父View此时的模式
- Size = 父View的大小 – padding
-
子View指定为 WRAP_CONTENT 时:
- Mode = AT_MOST
- Size = 父View的大小 – padding
根据以上的规律,父 View 一层一层的向下设置子 View 的 MeasureSpec
。那么子 View 是如何根据父 View 给的 MeasureSpec
,确定自身的大小呢?答案就在下面马上要讲的 onMeasure()
方法里。
为什么要重写 onMeasure ?
调用 setMeasuredDimension
后,正式的确定了当前 View 的大小,那默认的onMeasure
是怎么去调用的呢?
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// AT_MOST 和 EXACTLY 使用同一种处理方式
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
View 的默认 onMeasure
方法中,可以看到 MeasureSpec.AT_MOST
和 MeasureSpec.EXACTLY
方法处理的方式是一样的。这样导致,自定义的 View 使用 WRAP_CONTENT 和 MATCH_PARENT 的效果是一样的,都会撑满整个父容器。
在某些情况下,我们并不想要默认的那种效果,这时候我们就可以根据自己的需求,去重写 onMeasure
方法。
这里我们简单的重写一下,实现功能如下:
- 如果指定高度和宽度的大小,那么结果就是这个值;
- 如果设置为 MATCH_PARENT ,就填满父容器;
- 如果设置为 WRAP_CONTENT ,就在父容器建议值和本 View 设置的建议值取最小值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(doLocalMeasure(60, widthMeasureSpec),
doLocalMeasure(30, heightMeasureSpec));
}
int doLocalMeasure(int defaultSize, int measureSpec) {
int result = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.EXACTLY:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(result, size);
break;
}
return result;
}
小结
这一章简单的讲了 View 的测量流程,以及重写 onMeasure
方法的作用。
下一篇:Android View 框架(3)-- layout
网友评论