Android View的测量模式
onMeasure讲解
View绘制出来需要知道自己的宽高是多少,所以要先进行测量尺寸。
从门缝里面看世界,那就从View的内部类MeasureSpec测量类去学:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
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);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
测量模式:
UNSPECIFIED
EXACTLY
AT_MOST
为了认准测量模式的对应方式,我写了一个简单测试类:
public class CustomView extends View{
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
switch (widthMode){
case MeasureSpec.UNSPECIFIED:
Log.e("TAG","widthMode " + "UNSPECIFIED");
break;
case MeasureSpec.AT_MOST:
Log.e("TAG","widthMode " + "AT_MOST");
break;
case MeasureSpec.EXACTLY:
Log.e("TAG","widthMode " + "EXACTLY");
break;
}
Log.e("TAG","widthSize " + MeasureSpec.getSize(widthMeasureSpec));
switch (heightMode){
case MeasureSpec.UNSPECIFIED:
Log.e("TAG","heightMode " + "UNSPECIFIED");
break;
case MeasureSpec.AT_MOST:
Log.e("TAG","heightMode " + "AT_MOST");
break;
case MeasureSpec.EXACTLY:
Log.e("TAG","heightMode " + "EXACTLY");
break;
}
Log.e("TAG","heightSize " + MeasureSpec.getSize(heightMeasureSpec));
}
}
测试结果:
布局中宽高均为match_parent: 测量模式为EXACTLY
布局中宽高均为wrap_content: 测量模式为AT_MOST
布局中宽高均为200dp(固定数值): 测量模式为EXACTLY
-
UNSPECIFIED 表示子布局想要多大就多大,父view不限制子view大小。一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。
-
EXACTLY 父容器测量的值是多少,那么这个view的大小就是这个specSize,毫不讨价还价
-
AT_MOST 父容器给定一个子view的最大尺寸,大小在这个值范围以内,具体是多少看子view的表现
测量完成:
测量完成回调onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。
那么这两个名字长长的变量是什么呢?就是测量出的宽和高的信息。
回到MeasureSpec类分析,一个Int有32位,用前2位表示SpecMode ,2位数有四种表示方法了,00,01,11分别表示上面的模式顺序。后30位表示SpecSize。那我们是不是获取测量模式和尺寸都要自己使用位移计算呢?不用的,MeasureSpec类已经有了,自带了拆分和打包方法。
image.pngpublic static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
获取测量模式和测量大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
}
现在,我们要写一个正方形ImageView,使用setMeasuredDimension()自己重设测量值,让高度值也等于宽度值:
public class SquareImageView extends AppCompatImageView{
public SquareImageView(Context context) {
super(context);
}
public SquareImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(widthMeasureSpec,widthMeasureSpec);
}
}
getChildMeasureSpec()代码里的生成规则:
1.当子View的宽高设置的是具体数值时,显然我们可以直接拿到子View的宽高,则子View宽高就确定了,不用再去考虑父容器的SpecMode了,此时子View的SpecMode为EXACTLY,SpecSize就是设置的宽高。
2.当子View的宽高设置的是match_parent, 则不管父容器的SpecMode是什么模式,子View的SpecSize就等于父容器的宽高,而子View的SpecMode随父容器的SpecMode。
3.当子View的宽高设置的是wrap_content,因为这种情况父容器实在不知道子View应该多宽多高,所以子View的SpecSize给的是父容器的宽高,也就是说只是给子View限制了一个最大宽高,而子View的SpecMode是AT_MOST模式。
如果你设置的是wrap_content,则默认显示出来是其父容器的大小,如果你想要它正常的显示为wrap_content,则你就要自己重写onMeasure()来自己计算它的宽高度并设置。
示例:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
父view的大小是200dp,子view的宽高是wrap_content,父view的背景是紫色,子view背景是黑色。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/purple_200">
<com.example.performanceopt.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black"/>
</RelativeLayout>
</RelativeLayout>
结果是子view的显示出来是其父容器的200dp大小。