Android自定义View-onMeasure介绍

来源:互联网 发布:黑马程序员薪资 编辑:程序博客网 时间:2024/05/21 01:31

推荐两篇博客:

  • Android View系统解析(下) - 任玉刚 - CSDN博客
  • ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解 - 大苞米的专栏 - CSDN博客

重写onMeasure的作用

实现特定的测量方式,比如实现一个时钟的自定义View要求宽高比例1:1、像TextView一样根据内容自适应。

onMeasure(int, int)介绍

onMeasure(int, int):测量View及其内容以确定测量宽度和测量高度。当重写此方法时,必须调用setMeasuredDimension(int, int)来存储该View的测量宽度和测量高度,否则将触发一个IllegalStateException,或者调用超类的onMeasure(int,int)(超类做了默认的测量方法)。
如果重写了该方法,子类有责任确保测量的宽度和高度至少是View的最小宽度和高度(getSuggestedMinimumWidth()和getSuggestedMinimumHeight())。
参数:

  • widthMeasureSpec:父布局对子View宽度的要求
  • heightMeasureSpec:父布局对子View高度的要求

这两个参数都是由MeasureSpec类打包而成,里面包含测量模式和参考尺寸。widthMeasureSpec和heightMeasureSpec是由父布局确定的(除了DecorView )。

MeasureSpec介绍

一个测量规格封装了从父布局传递给子View的布局要求。每个测量规格代表对宽度和高度的要求,由大小和模式组成。有三种可能的模式:

  • UNSPECIFIED:父布局没有对子View施加任何约束,它可以是它想要的大小
  • EXACTLY:父布局已经确定了子View的确切尺寸,不管子View想要多大它都会得到这些界限
  • AT_MOST:子View可以是它想要的大小一直到指定大小之间

MeasureSpec的实现是为了减少对象分配,这个类提供了打包和解包的方法:

  • 打包:int makeMeasureSpec(int size, int mode)
  • 获取测量模式:int getMode(int measureSpec)
  • 获取参考大小:int getSize(int measureSpec)

步骤

  1. 计算当前View的测量宽度和测量高度(注意确保测量的宽度和高度至少是View的最小宽度和高度getSuggestedMinimumWidth()和getSuggestedMinimumHeight())
  2. 确定所有子View的widthMeasureSpec和heightMeasureSpec,并测量所有子View(自定义ViewGroup才需要这一步)
  3. 调用setMeasuredDimension(int, int)来存储当前View的测量宽度和测量高度

注:源码是23版本的

默认测量方式

View的onMeasure(int,int)代码

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    }

getDefaultSize(int,int)代码:返回默认大小(参考尺寸),如果测量规格没有施加约束(UNSPECIFIED模式),则使用所提供的尺寸(子View建议的最小尺寸)。

    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;        case MeasureSpec.AT_MOST:        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result;    }

getSuggestedMinimumWidth():返回建议的最小宽度

    protected int getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }

getSuggestedMinimumHeight():返回建议的最小高度

    protected int getSuggestedMinimumHeight() {        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());    }

getSuggestedMinimumWidth(),这个方法是从mBackground.getMinimumWidth()和mMinWidth中取最大的值返回,
mBackground是setBackgroundDrawable(Drawable background)设置的背景,
mMinWidth是setMinimumWidth(int minWidth)设置的最小宽度。
getSuggestedMinimumHeight()类似。

ViewGroup 提供的测量子View的方法

measureChildren(int, int) 代码:遍历所有子View,并调用measureChild(View, int, int)测量单个子View

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {        final int size = mChildrenCount;        final View[] children = mChildren;        for (int i = 0; i < size; ++i) {            final View child = children[i];            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {                measureChild(child, widthMeasureSpec, heightMeasureSpec);            }        }    }

measureChild(View, int, int)代码:调用getChildMeasureSpec(int spec, int padding, int childDimension)确定子View的widthMeasureSpec、heightMeasureSpec,并调用子View的measure(int,int)进行测量。

    protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {        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);    }

getChildMeasureSpec(int, int, int)代码:ViewGroup提供确定子View的widthMeasureSpec、heightMeasureSpec的方法(根据父布局对当前ViewGroup的约束、当前ViewGroup已用空间和子View的LayoutParams),但布局不一定或不单单调用这个方法来确定子View的widthMeasureSpec、heightMeasureSpec。

/** @param spec              父布局的measureSpec* @param padding           当前布局已用空间(当前布局的padding、子View的margin)* @param childDimension    子View想要的大小* @return                  子View的measureSpec*/public static int getChildMeasureSpec(int spec, int padding, int childDimension) {        int specMode = MeasureSpec.getMode(spec);        int specSize = MeasureSpec.getSize(spec);        int size = Math.max(0, specSize - padding);//当前ViewGroup可用于布局的空间        int resultSize = 0;        int resultMode = 0;        switch (specMode) {        // 父布局的测量模式是EXACTLY        case MeasureSpec.EXACTLY:            if (childDimension >= 0) {// 子View提供了明确的尺寸                // 子View得到明确的尺寸                resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {// 子View想铺满父布局                // 子View得到父布局的尺寸                resultSize = size;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 子View想要可以包含内容的尺寸                // 子View想自己决定自己的尺寸,这个尺寸不能比父布局的尺寸大                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            }            break;        // 父布局的测量模式是AT_MOST        case MeasureSpec.AT_MOST:            if (childDimension >= 0) {// 子View提供了明确的尺寸                // 子View得到明确的尺寸                resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {// 子View想铺满父布局                // 子View想得到父布局的尺寸,但父布局的尺寸不是固定的                // 强迫子View不能大过父布局                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 子View想要可以包含内容的尺寸                // 子View想自己决定自己的尺寸,这个尺寸不能比父布局的尺寸大                resultSize = size;                resultMode = MeasureSpec.AT_MOST;            }            break;        // 父布局的测量模式是UNSPECIFIED        case MeasureSpec.UNSPECIFIED:            if (childDimension >= 0) {// 子View提供了明确的尺寸                //  让子View得到明确的尺寸                resultSize = childDimension;                resultMode = MeasureSpec.EXACTLY;            } else if (childDimension == LayoutParams.MATCH_PARENT) {                // 子View想得到父布局的尺寸,查找子View想要多大                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;                resultMode = MeasureSpec.UNSPECIFIED;            } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 子View想要可以包含内容的尺寸                // 子View想自己决定自己的尺寸,查找子View想要多大                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;                resultMode = MeasureSpec.UNSPECIFIED;            }            break;        }        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);    }

View.sUseZeroUnspecifiedMeasureSpec:boolean类型,判断当measureSpec的模式是UNSPECIFIED时,是否总是返回一个大小为0的值。在View的构造函数进行赋值,当targetSdkVersion <23时为true:

public View(Context context) {        ...        sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M;        ...        }    }

下面这张图片是getChildMeasureSpec(int spec, int padding, int childDimension)的表格形式,来自Android View系统解析(下) - 任玉刚 - CSDN博客。
这里写图片描述

还有个跟measureChild(View, int, int)类似的方法void measureChildWithMargins(View,int,int,int,int)

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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed,//            lp.height);    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

这个方法和measureChild(View, int, int)的区别
1、多接收两个参数widthUsed和heightUsed
2、调用getChildMeasureSpec确定子View measureSpec时,
确定宽度约束时:在传的padding参数中多加上widthUsed和子View的leftMargin、rightMargin,
确定宽度约束时:在传的padding参数中多加上heightUsed和子View的topMargin、bottomMargin

原创粉丝点击