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>

android 自定义的View预设高度_sed


设置为具体的大小

<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>

android 自定义的View预设高度_android_02


设置为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>

android 自定义的View预设高度_xml_03


可以看到很奇怪的一点,宽高设置为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);

    }
}

android 自定义的View预设高度_xml_04

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>

android 自定义的View预设高度_sed_05


可以很明显的看到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的内容进行测量。