1 View默认的onMeasure实现
自定义View(ViewGroup)重要的三个步骤:测量,布局(只在ViewGroup中),绘制,在Android绘图的专题中已经对绘制进行了讲解,今天主要学习View的测量,View的测量主要对view进行测量,确定view的测量尺寸。
public class MeasureDemo extends View {
Paint paint ;
public MeasureDemo(Context context) {
this(context,null);
}
public MeasureDemo(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MeasureDemo(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStrokeWidth(17);
paint.setTextSize(44);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
String str = "我在中间";
Rect rect = new Rect();
paint.getTextBounds(str,0,str.length(),rect);
//下面的代码没有考虑过多LayoutParams,要注意drawText的绘制开始点不是左上角,而是
Text的基准线,所以下面的代码只能大致绘制在正中,想要完全居中还要考虑很多 canvas.drawText(str,width/2-rect.width()/2,height/2-rect.height(),paint);
}
}
放在LinearLayout就可以正常显示,只是重写了onDraw的方法,测量使用了View的默认行文。
在LinearLayout中使用,并且设置为match_parent
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.ldx.canvasdrawdemo.MeasureDemo
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
设置为具体的大小
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.ldx.canvasdrawdemo.MeasureDemo
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#cdcdcd"/>
</LinearLayout>
设置为wrap_content
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.ldx.canvasdrawdemo.MeasureDemo
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#cdcdcd"/>
</LinearLayout>
可以看到很奇怪的一点,宽高设置为wrap_content竟然也是全屏显示。
查看onMeasure的默认实现:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
内部调用了setMeasuredDimension,由前面的讲解知道调用了setMeasuredDimension就是测量的宽高,设置的宽高为getDefaultSize得到的宽高。
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模式下都是直接返回specSize,也就是父布局推荐的
宽高,只要不大于这个值就是对的
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
看第二节的MeasureSpec的讲解可以知道,wrap_content对应AT_MOST,match_parent和具体尺寸对应EXACTLY,所以上面自定义View设置wrap_content时才和match_parent最终呈现的效果一样。
2 MeasureSpec 讲解
MeasureSpec是父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。通过MeasureSpec可以获取View测量模式和View想要绘制的大小,就可以控制View最后显示的大小。
MeasureSpec是一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode指的是测量模式,而SpecSize指的是某种测量模式下的规格大小。
MeasureSpec是SpecMode和SpecSize的组合SpecMode和SpecSize也是一个int值,MeasureSpec也可以通过解包的形式来得出其原始的SpecMode和SpecSize。
SpecMode有三类:
1.UNSPECIFLED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
2.EXACTLY
对应LayoutParams中的match_parent和具体的数值这两种模式,View的最终大小就是SpecSize所指定的值。
3.AT_MOST
对应的LayoutParams中的wrap_content,父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体值依据View的内部实现。
View默认的onMeasure()方法AT_MOST和EXACTLY模式(也就是说View默认的onMeasure方法时不支持wrap_content的),默认实现都是match_parent的实现,所以自定义控件需要重写onMeasure()方法。想让自定义View支持wrap_content属性,就必须重写onMeasure()方法来指定wrap_content时的大小。
还可以利用SpecMode 和 SpecSize 生成MeasureSpec:
MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth);
3 重写onMeasure自定义View支持wrap_content
public class MeasureDemo extends View {
Paint paint ;
public MeasureDemo(Context context) {
this(context,null);
}
public MeasureDemo(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MeasureDemo(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStrokeWidth(17);
paint.setTextSize(44);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int defaultWidht = 200;
int defaultHeight = 200;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
String str = "我在中间";
Rect rect = new Rect();
paint.getTextBounds(str,0,str.length(),rect);
switch (widthSpecMode) {
case MeasureSpec.UNSPECIFIED:
widthSpecSize = defaultWidht;
heightSpecSize = defaultHeight;
break;
case MeasureSpec.AT_MOST:
widthSpecSize = rect.width() + 20 +getPaddingTop() + getPaddingBottom();
heightSpecSize = rect.height() + 20 + getPaddingLeft() + getPaddingRight();
break;
case MeasureSpec.EXACTLY:
//已经是确定值
break;
}
setMeasuredDimension(widthSpecSize,heightSpecSize);
//或者确定size后,改变MeasureSpec的模式,然后重新super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
String str = "我在中间";
Rect rect = new Rect();
paint.getTextBounds(str,0,str.length(),rect);
canvas.drawText(str,getPaddingLeft(),height - (height - rect.height())/2 + getPaddingTop(),paint);
}
}
4 MeasureSpec.UNSPECIFIED
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="144dp"
android:background="@color/colorPrimary"
android:textColor="#ffffff"
android:text="MeasureSpec.UNSPECIFIED"/>
</ScrollView>
可以很明显的看到TextView没有114dp那么高,好像高度只是包裹内容,为什么会这样。
源码分析:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
调用super.onMeasure(widthMeasureSpec, heightMeasureSpec);
也就是FrameLayout的onMeasure方法,内部调用measureChildWithMargins,measureChildWithMargins又在ScrollView中被复写最终调用ScrollView的measureChildWithMargins方法。
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
最终生成了新的childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
并且模式是MeasureSpec.UNSPECIFIED。
为什么会这样,因为ScrollView等控件可以滑动,它们的高度不能确定。
最终会传到TextView中,看TextView的onMeasure函数:
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
View的测量函数的处理是,如果不是EXACTLY模式,会根据TextView的内容进行宽高的计算,也就会导致我们虽然设置了具体的高度,但是由于模式是UNSPECIFIED,最终还是会按照TextView的内容进行测量。