View 自定义 - 测量 Mersure

参考文章(包含处理margin的情况)

一、概念

1.1 为什么要重写 onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
自定义View给宽高设置 100dp 或 match_parent 都是一个具体值,设置 wrap_content  就需要计算出一个实际的大小,而 View 中的默认实现使得设置 wrap_content 效果和 match_parent 一样,想要实现该效果就需要手动重写。(参考计算子元素 MeasureSpec 的表,当子元素设置 wrap_content 时父容器 AT_MOST 和 EXACTLY 模式都返回 parentSize 大小,无法像 TextView 那样在不超过最大宽度的情况下显示实际内容宽度)
自定义ViewGroupViewGroup 是一个抽象类,它没有重写父类中的 onMeasure() 方法,因为不同内容测量起来代码无法统一实现,但提供了三个测量方法方便我们测量(measureChildren、measureChild、measureChildWithMargins)。

1.2 测量流程

父容器递归调用子元素的 measure( ) 方法来测量子元素,传入的测量规格(widthMeasureSpec 和 heightMeasureSpec)是父容器对子元素的尺寸限制。之后调用真正测量的地方 onMeasure( ) 正是我们要重写的,在这里算出【自身需要占用的尺寸】并结合【父容器给的尺寸限制】得出【实际占用大小】,最终调用 setMeasuredDimension( ) 保存,用在后面的 onLayout() 布局阶段使用。

1.3 测量规格 MeasureSpec

        onMeasure() 中的两个参数分别是该元素宽高的测量规格 MeasureSpec,这是在父容器中计算好后传过来的,这形成了一个链条。View 绘制流程开始于 ViewRootImpl 的 performTraversals() 中,链条头部 DecorView 的参数正是在这里被传入的(size是屏幕宽高,mode是EXACTLY)。

        通过使用二进制,将测量模式 mode(前2位)测量大小 size(后30位)打包成一个测量规格 MeasureSpac(32位int值)并提供打包和解包的方法,好处是减少创建对象。三种测量模式的值0、1、2的二进制为00、01、10刚好用2位能表示。

自定义View由于 MeasureSpec 都是在父容器中计算好后传到子元素中的,自定义 View 不包含子元素不用管。
自定义ViewGroup自身作为父容器就需要遍历子元素挨个算出它们的 MeasureSpec 并传递。为什么需要父容器参与才能确定子元素,是因为 match_parent 需要知道自身多大才能匹配父容器,wrap_content 需要知道自身不能超过多少。

mode

测量模式

EXACTLY

精准模式

父容器为子元素指定一个确切的大小。子元素 xml 设置了 match_parent 或具体数值如100dp。

AT_MOST

最大模式

父容器为子元素指定一个最大尺寸,子元素最大不可超过该尺寸。子元素 xml 设置了 wrap_content。

UNSPECIFIED

无限制模式

父容器不约束子元素,即子元素可设置任意尺寸。用于可滑动布局如系统的ListView、ScrollView,一般自定义控件用不到。

size

测量大小

并不是子元素的最终尺寸,它是父容器对子元素能多大的限制,子元素计算出实际尺寸后需要综合这个限制来决定最终尺寸,即调用 resolveSize(int size, int measureSpec)。
getMode()

public static int getMode(int measureSpec)

获取模式(即高2位)。

getSize()

public static int getSize(int measureSpec)

获取大小(即低30位)。

makeMeasureSpec()

public static int makeMeasureSpec(int size, int mode)

传入 size 和 mode 来生成 MeasureSpec。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)   //获取宽度的模式(占Int前2位)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)   //获取宽度的大小(占Int的后30位)
    val measureSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode) //通过模式和大小生成规格
}

二、自定义控件怎么写

2.1 修改已有控件

现有的控件已经有了自己的正确尺寸算法,结果可以作为参考值根据我们的需求进行调整。

  1. 自定义子类继承已有的控件,重写构造。
  2. 重写 onMeasure(),调用 super.onMeasure() 进行一次原有的测量,通过 getMeasuredHeight()、getMeasuredWidth() 获取测量结果并修改成想要的值。
  3. 最终调用 setMeasuredDimension() 保存。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    //先触发原有测量(计算完后已经调用过setMeasuredDimension(),所以可通过getMeasuredHeight()、getMeasuredWidth()拿到测量结果)
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    //获取测量结果并修改为想要的尺寸
    val needWidth = measuredWidth + 1
    val needHeight = measuredHeight + 1
    //保存最终值(是自己的期望尺寸,父容器用作参考,实际值父元素通过layout()传过来)
    setMeasuredDimension(needWidth, needHeight)
}

2.2 自定义View

  1. 自定义子类继承 View,重写构造。
  2. 计算内部每个部分的尺寸最后加起来就是自身需要占用的尺寸。
  3. 综合父容器给的限制(widthMeasureSpec 、heightMeasureSpec),一并传入到 resolveSize() 中得出实际能占用的尺寸。
  4. 最终调用 setMeasuredDimension() 保存。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    //计算出的占用尺寸
    val needWidth = 0
    val needHeight = 0
    //得到最终期待尺寸
    val finalWidth = resolveSize(needWidth, widthMeasureSpec)
    val finalHeight = resolveSize(needHeight, heightMeasureSpec)
    //保存
    setMeasuredDimension(finalWidth, finalHeight)
}

2.3 自定义 ViewGroup

  1. 自定义子类继承 ViewGroup,重写构造。
  2. 对宽高的 MeasureSpec 进行判断,如果是确切值EXACTLY(如100dp或match_parent)就直接调用 setMeasuredDimension() 保存,否则计算实际大小。
  3.  
    1. 如果是自定义ViewGroup中代码添加子元素:计算出子元素的宽高,算出子元素的 MeasureSpec 调用子元素的 measure() 测量子元素,通过 getMeasuredXXX() 获取单个子元素测量后的宽高,通过累加计算出自身需要占用的尺寸。
    2. 如果是xml布局包裹添加子元素:先调用 measureChildren() 测量所有子元素,再遍历子元素,通过 getMeasuredXXX() 获取单个子元素测量后的宽高,通过累加计算出自身需要占用的尺寸。
  4. 综合自身的 MeasureSpec 一并传入 resolveSize() 中得出实际能占用的尺寸。
  5. 最终调用 setMeasuredDimension() 保存。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //需要的尺寸
        var needWidth = 0
        var needHeight = 0

        //获取自身测量模式和可用大小
        val selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val selfHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val selfHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec)

        //自身宽高是固定值或者mutch_parent就直接设置确切值,否则计算实际大小
        if (selfWidthSpecMode == MeasureSpec.EXACTLY && selfHeightSpecMode == MeasureSpec.EXACTLY) {
            setMeasuredDimension(selfWidthSpecSize, selfHeightSpecSize)
        } else {
            //测量所有子元素
            measureChildren(widthMeasureSpec, heightMeasureSpec)
            //遍历子元素
            for (i in 0 until childCount) {
                val childView = getChildAt(i)
                //获取测量后的子元素的宽高
                val childMeasuredWidth = childView.measuredWidth
                val childMeasuredHeight = childView.measuredHeight
                //通过子元素计算自身需要的尺寸
                //needWidth = 
                //needHeight = 
            }
            //综合需要的尺寸和MeasureSpec的限制得出实际占用尺寸
            val finalWidth = resolveSize(needWidth, widthMeasureSpec)
            val finalHeight = resolveSize(needHeight, heightMeasureSpec)
            //设置尺寸
            setMeasuredDimension(finalWidth, finalHeight)
        }
    }

三、源码

以上是 View 默认实现流程。 

3.1 measure()

测量的入口,由父容器调用,父容器计算出该子元素的MeasureSpec后通过此方法传递过来,该方法会调用 onMeasure()。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    ...
}

3.2 onMeasure()

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

3.3 getSuggestedMinimumXXX()

若 View 未设置背景,宽度为 android:minWidth 属性所指定的值,未指定则为0。若设置了背景,宽度为该属性和背景Drawable宽度中的最大值。高同理。

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

3.4 getDefaultSize()

综合 MeasureSpec 和最小值得出。

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:    //正是这里使得 wrap_content 和 matchParent 效果一样
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
    return result;
}

3.5 setMeasuredDimension()

保存测量尺寸。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
}

3.6 resolveSize()

综合【计算出的需要占用的大小】和【自身的测量规格】得出【最终实际占用大小】,解决了默认实现中 wrap_content 直接返回 parentSize 和 match_parent 效果一样的问题。

public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        //对于AT_MOST,取实际值和最大值中最小的(实际值超过最大值界面会显示不完全)
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        //对于EXACTLY,直接返回确切值
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        //对于UNSPECIFIED,直接返回实际值
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

3.7 measureChildran()

遍历并测量子元素。

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);
        }
    }
}

3.8 measureChild()

获取子元素的 LayoutParams,获取父容器的 Padding,综合父容器的 MeasureSpec,计算出子元素的 MeasureSpec,然后调用子元素的 measure() 传递过去。

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);
}

3.9 获取子元素xml中设置的宽高 getLayoutParams()

LayoutParams 是 ViewGrogup 中的静态内部类,封装了 xml 中对控件 layout_width、layout_height 设置的宽高值(MARCH_PARENT=-1、WRAP_CONTENT=-2、或者具体值dp/px),通过属性调用 .width 和 .height 拿到具体宽高值。 

3.10 获取父容器设置的内边距 getPaddingXXX()

计算子元素的 widthMeasureSpec 时传入 getPaddingLeft( ) + getPaddingRight( )。

计算子元素的 heightMeasureSpec 时传入 getPaddingTop( ) + getPaddingBottom( )。

 

3.11 计算子元素的测量规格 getChildMeasureSpec()

父容器的测量模式有3种情况(EXACTLY、AT_MOST、UNSPECIFIED),子元素xml设置的值也有3种情况(具体值、match_parent、wrap_content),一共有9种可能如下表。

算出子元素 MeasureSpec父容器测量模式 mode

EXACTLY

(只给多少)

AT_MOST

(最多能给多少)

UNSPECIFIED

(不限制)

子元素宽高 LayoutParams

100dp

(具体值)

EXACTLY + childSizeEXACTLY + childSizeEXACTLY + childSize

match_parent

(全要)

EXACTLY + parentSize

(父容器剩余空间)

AT_MOST + parentSize

(大小不超过父容器剩余空间)

UNSPECIFIED + 0

wrap_content

(不确定要多少)

AT_MOST + parentSize

(大小不超过父容器剩余空间)

AT_MOST + parentSize

(大小不超过父容器剩余空间)

UNSPECIFIED + 0
//参数spec:父容器的MeasureSpec。
//参数padding:父容器设置的padding。
//参数childDimension:子元素在xml中设置的宽高。
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);
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //父容器EXACTLY + 子元素固定值 = EXACTLY + childSize
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //父容器EXACTLY + 子元素much_parent = EXACTLY + parentSize
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //父容器EXACTLY + 子元素wrap_content = AT_MOST + parentSize
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            //父容器AT_MOST + 子元素固定值 = EXACTLY + childSize
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //父容器AT_MOST + 子元素much_parent = AT_MOST + parentSize
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //父容器AT_MOST + 子元素wrap_content = AT_MOST + parentSize
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            //父容器UNSPECIFIED + 子元素固定值 = EXACTLY + childSize
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //父容器UNSPECIFIED + 子元素MATCH_PARENT = UNSPECIFIED + 0
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //父容器UNSPECIFIED + 子元素WRAP_CONTENT = UNSPECIFIED + 0
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //生成并返回子元素MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值