高级UI<第十篇>:视图的测量(onMeasure)
当自定义一个视图时,基本都会重写
onMeasure
、onLayout
以及onDraw
这三个方法,本文的重点是onMeasure
。
(1)onMeasure的作用
简单的说,onMeasure
的主要负责视图绘制之前的测量工作。
(2)自定义视图时,必须重写onMeasure吗?
不是必须重写onMeasure
;
如果没有重写onMeasure
,则默认测量当前视图;
如果重写了onMeasure
方法,也必须测量当前视图,否则会抛出异常,如下:
java.lang.IllegalStateException: View with id -1: com.zyc.hezuo.myapplication.ViewGroupDemo#onMeasure() did not set the measured dimension by calling setMeasuredDimension()
为了解决这个异常,我们必须测量当前视图,那么怎么去测量当前视图呢?
(3)怎么去测量当前视图?
- 方法一
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
- 方法二
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);//测量当前视图
}
那么,widthMeasureSpec
和heightMeasureSpec
又是什么呢?为什么测量当前视图时必须传递这两个参数?
(4)测量规格
widthMeasureSpec
和heightMeasureSpec
作为onMeasure
方法的形参,为视图的测量提供了重要的帮助,我们必须了解这两个参数的意义和作用,这两个参数是由父视图
通过一定的计算方式计算出来的,至于父视图是怎么计算出这两个参数的值,我们不需要去关注。(这里需要再次提醒,这里的两个参数是由父视图
通过某个计算方式计算出来的,不排除嵌套自定义view的情况,也就是说,某自定义view的父视图是自定义viewGroup,它的这两个参数就是父视图计算出来的)
那么,widthMeasureSpec
和heightMeasureSpec
到底是什么呢?
widthMeasureSpec可以分成三个英文单词,分别是:width
、Measure
、Spec
,根据英文翻译组合起来的意思是:宽度测量规格
,对应着heightMeasureSpec的意思就是:高度测量规格
。
我翻阅了源码,发现这两个参数是这个方法计算的来的,如下:
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
方法MeasureSpec.makeMeasureSpec
有两个参数,第一个参数windowSize
是父视图的实际大小,第二个参数是测量模式
。
为了了解widthMeasureSpec
和heightMeasureSpec
的计算方式,查看makeMeasureSpec
的源码,源码如下:
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
大致的意思如下,如果targetSdkVersion小于等于17,那么
measureSpec = size + mode;
否则
measureSpec = (size & ~MODE_MASK) | (mode & MODE_MASK);
这里我们假设targetSdkVersion > 17(就目前而言,为了解决有些兼容问题,targetSdkVersion肯定>17了),所以最终的算法如下:
measureSpec = (size & ~MODE_MASK) | (mode & MODE_MASK);
size是某视图的实际大小,暂定为100,mode是某视图的测量模式
,测量模式
有三种,分别是:
MeasureSpec.UNSPECIFIED
:不准确的大小,不对View大小做限制,如:ListView,ScrollView
MeasureSpec.EXACTLY
:确切的大小,如:100dp或者march_parent
MeasureSpec.AT_MOST
:大小不可超过某数值,如:wrap_content
这三种模式的取值可从源码获取,如下:
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
MODE_SHIFT
的取值为30,所以UNSPECIFIED
值为0向左移动30位,即0 * 230;
EXACTLY
的取值为1向左移动30位,,即1 * 230;
AT_MOST
的取值为2向左移动30位,,即2 * 230;
从源码中找到这样一句代码:
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
所以MODE_MASK
取值为 3 * 230;
万事俱备,只欠东风,现在开始计算measureSpec的取值,按照如下步骤计算:
【size的二进制表示】
size的值为100,二进制表示为
00000000000000000000000001100100
【MODE_MASK的二进制表示】
11000000000000000000000000000000(30个0,加上前面两位共32位)
MeasureSpec
由两部分组成,测量模式
和大小
,
【~MODE_MASK的二进制表示】
00111111111111111111111111111111(30个1,加上前面两位共32位)
【size & ~MODE_MASK的二进制表示】
00000000000000000000000001100100
& 00111111111111111111111111111111
--------------------------------------------------
00000000000000000000000001100100
【mode的二进制表示】
UNSPECIFIED:
00000000000000000000000000000000(32个0)
EXACTLY:
01000000000000000000000000000000(31个0)
AT_MOST:
11000000000000000000000000000000(30个0)
【mode & MODE_MASK的二进制表示】
UNSPECIFIED:
00000000000000000000000000000000
& 11000000000000000000000000000000
--------------------------------------------------
00000000000000000000000000000000
EXACTLY:
01000000000000000000000000000000
& 11000000000000000000000000000000
--------------------------------------------------
01000000000000000000000000000000
AT_MOST:
10000000000000000000000000000000
& 11000000000000000000000000000000
--------------------------------------------------
10000000000000000000000000000000
【(size & ~MODE_MASK) | (mode & MODE_MASK)的二进制表示】
UNSPECIFIED:
00000000000000000000000001100100
| 00000000000000000000000000000000
--------------------------------------------------
00000000000000000000000001100100
计算结果为100。
EXACTLY:
00000000000000000000000001100100
| 01000000000000000000000000000000
--------------------------------------------------
01000000000000000000000001100100
计算结果为: 2 * 30 + 100 = 1073741924
AT_MOST:
00000000000000000000000001100100
| 10000000000000000000000000000000
--------------------------------------------------
10000000000000000000000001100100
计算结果为: -2 * 2 * 30 + 100 = -2147483548
所以measureSpec
的取值受到size和mode的影响。
(5)怎么去获取测量规格?
int measureSpec = MeasureSpec.makeMeasureSpec(当前视图的大小, 测量模式);
measureSpec主要用于子视图的测量,因为onMeasure(int widthMeasureSpec, int heightMeasureSpec)
中的两个形参是由父视图中计算的,所以这两个参数应当作为当前视图的测量参数。
(6)根据测量规格获取测量模式和大小?
//获取测量模式
int mode = MeasureSpec.getMode(measureSpec);
//获取测量大小
int size = MeasureSpec.getSize(measureSpec);
(7)怎么去测量子视图?
如果您想要自定义一个类似于线性布局
或相对布局
的ViewGroup,就必须测量子视图(如果是自定义View,非ViewGroup,不需要重写onMeasure方法,因为自定义View不可能存在子视图)。
测量子视图有以下几种方法:
-
方法一
measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有的子布局
-
方法二
for (int index=0;index<getChildCount();index++){//测量所有的子布局 View subview = getChildAt(index); measureChild(subview, widthMeasureSpec, heightMeasureSpec); }
-
方法三
for (int index=0;index<getChildCount();index++){//测量所有的子布局 View subview = getChildAt(index); MarginLayoutParams lp = (MarginLayoutParams) subview.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,0, lp.width); int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,0, lp.height); subview.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
需要特别注意的是:获取subview的宽和高不能直接使用subview.getWidth()
、subview.getMeasuredWidth()
和subview.getHeight()
、subview.getMeasuredHeight()
,因为获取到的数据有时候为0导致测量不准确,这里使用getLayoutParams
来获取宽和高,当getLayoutParams
获取到数字是-1时,代表MATCH_PARENT,当getLayoutParams
获取到的数字是-2时,代表WRAP_CONTENT,当getLayoutParams
获取到的数据是固定值时,说明subview的宽或高也是固定值。
-
方法四
for (int index=0;index<getChildCount();index++){//测量所有的子布局 View subview = getChildAt(index); measureChildWithMargins(subview, widthMeasureSpec, 0, heightMeasureSpec, 0); }
需要注意的是:如果用到measureChildWithMargins
这个方法,就必须重写generateLayoutParams
进行类型转换,否则会报错:
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
以下是需要重写方法的代码:
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet params) {
return new MarginLayoutParams(getContext(), params);
}
(8)onMeasure为什么会被执行两次?
在源码中有这样的方法,源码如下:
private void performTraversals() {
//...(省略)
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(mTag,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//...(省略)
}
其中performMeasure
就是执行onMeasure
方法,这里performMeasure
被执行了两次,原因是lp.horizontalWeight
或者lp.verticalWeight
的取值大于0;
那么为什么onMeasure
要执行两次呢?
这里我给出的答案是:系统为了测量的准确性,在某种条件下进行多次测量工作。
最后,有关onMeasure
被执行多次的补充:
本文的重点是onMeasure
方法,从performMeasure
源码角度分析,onMeasure
可能只被执行一次,也有可能被执行两次,也就是说,最多被执行两次;
也许你们会说,我的自定义ViewGroup的onMeasure
方法怎么被执行了3次?4次?
我只想确切的说,在执行onLayout
方法之前,最多执行2次,如果一旦执行了onLayout
方法方法,onMeasure
可能会再次执行,因为从源码中可以得到答案:
ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
//...(省略)
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//...(省略)
}
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//...(省略)
onLayout(changed, l, t, r, b);
//...(省略)
}
在layout
方法中,可以找到onMeasure
方法,当然,只有当(mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0
满足条件的时候onMeasure
才能被执行。
除此之外,以下三个方法也会执行onMeasure
方法,在代码中执行以下方法:
- addView:添加一个控件到当前ViewGroup中
linearLayout.addView(imageView);
- setVisibility:从隐藏状态切换到显示状态
linearLayout.setVisibility(View.VISIBLE);
注意:setVisibility(View.GONE)不会触发重新测量。
- setText:id为text的控件是当前ViewGroup下的子控件
((TextView)findViewById(R.id.text)).setText("11111");
最后,兵来将挡,水来土掩,不管onMeasure
执行多少次,都不会影响onMeasure
下的代码逻辑,在后面篇章中,会详细讲解瀑布流布局,到时候一起来见识一下onMeasure
的艺术吧。
[本章完...]