美文网首页自定义控件
高级UI<第十篇>:视图的测量(onMeasure)

高级UI<第十篇>:视图的测量(onMeasure)

作者: NoBugException | 来源:发表于2019-11-24 22:41 被阅读0次

    当自定义一个视图时,基本都会重写onMeasureonLayout以及onDraw这三个方法,本文的重点是onMeasure

    (1)onMeasure的作用

    简单的说,onMeasure的主要负责视图绘制之前的测量工作。

    (2)自定义视图时,必须重写onMeasure吗?

    不是必须重写onMeasure
    如果没有重写onMeasure,则默认测量当前视图;
    如果重写了onMeasure方法,也必须测量当前视图,否则会抛出异常,如下:

    java.lang.IllegalStateException: View with id -1: com.zyc.hezuo.myapplication.ViewGroupDemo#onMeasure() did not set the measured dimension by calling setMeasuredDimension()
    

    为了解决这个异常,我们必须测量当前视图,那么怎么去测量当前视图呢?

    (3)怎么去测量当前视图?
    • 方法一
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    
    • 方法二
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);//测量当前视图
    }
    

    那么,widthMeasureSpecheightMeasureSpec又是什么呢?为什么测量当前视图时必须传递这两个参数?

    (4)测量规格

    widthMeasureSpecheightMeasureSpec作为onMeasure方法的形参,为视图的测量提供了重要的帮助,我们必须了解这两个参数的意义和作用,这两个参数是由父视图通过一定的计算方式计算出来的,至于父视图是怎么计算出这两个参数的值,我们不需要去关注。(这里需要再次提醒,这里的两个参数是由父视图通过某个计算方式计算出来的,不排除嵌套自定义view的情况,也就是说,某自定义view的父视图是自定义viewGroup,它的这两个参数就是父视图计算出来的)

    那么,widthMeasureSpecheightMeasureSpec到底是什么呢?

    widthMeasureSpec可以分成三个英文单词,分别是:widthMeasureSpec,根据英文翻译组合起来的意思是:宽度测量规格,对应着heightMeasureSpec的意思就是:高度测量规格

    我翻阅了源码,发现这两个参数是这个方法计算的来的,如下:

    /**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    方法MeasureSpec.makeMeasureSpec有两个参数,第一个参数windowSize是父视图的实际大小,第二个参数是测量模式

    为了了解widthMeasureSpecheightMeasureSpec的计算方式,查看makeMeasureSpec的源码,源码如下:

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

    大致的意思如下,如果targetSdkVersion小于等于17,那么

    measureSpec = size + mode;
    

    否则

    measureSpec  = (size & ~MODE_MASK) | (mode & MODE_MASK);
    

    这里我们假设targetSdkVersion > 17(就目前而言,为了解决有些兼容问题,targetSdkVersion肯定>17了),所以最终的算法如下:

    measureSpec  = (size & ~MODE_MASK) | (mode & MODE_MASK);
    

    size是某视图的实际大小,暂定为100,mode是某视图的测量模式测量模式有三种,分别是:

    MeasureSpec.UNSPECIFIED:不准确的大小,不对View大小做限制,如:ListView,ScrollView
    MeasureSpec.EXACTLY:确切的大小,如:100dp或者march_parent
    MeasureSpec.AT_MOST:大小不可超过某数值,如:wrap_content

    这三种模式的取值可从源码获取,如下:

        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
    

    MODE_SHIFT的取值为30,所以UNSPECIFIED值为0向左移动30位,即0 * 230
    EXACTLY的取值为1向左移动30位,,即1 * 230
    AT_MOST的取值为2向左移动30位,,即2 * 230

    从源码中找到这样一句代码:

        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    

    所以MODE_MASK取值为 3 * 230

    万事俱备,只欠东风,现在开始计算measureSpec的取值,按照如下步骤计算:

    【size的二进制表示】

    size的值为100,二进制表示为

    00000000000000000000000001100100

    【MODE_MASK的二进制表示】

    11000000000000000000000000000000(30个0,加上前面两位共32位)

    MeasureSpec由两部分组成,测量模式大小,

    【~MODE_MASK的二进制表示】

    00111111111111111111111111111111(30个1,加上前面两位共32位)

    【size & ~MODE_MASK的二进制表示】

       00000000000000000000000001100100
    &  00111111111111111111111111111111
    --------------------------------------------------
       00000000000000000000000001100100
    

    【mode的二进制表示】

    UNSPECIFIED:

    00000000000000000000000000000000(32个0)

    EXACTLY:

    01000000000000000000000000000000(31个0)

    AT_MOST:

    11000000000000000000000000000000(30个0)

    【mode & MODE_MASK的二进制表示】

    UNSPECIFIED:

         00000000000000000000000000000000
    &    11000000000000000000000000000000
    --------------------------------------------------
         00000000000000000000000000000000
    

    EXACTLY:

         01000000000000000000000000000000
    &    11000000000000000000000000000000
    --------------------------------------------------
         01000000000000000000000000000000
    

    AT_MOST:

         10000000000000000000000000000000
    &    11000000000000000000000000000000
    --------------------------------------------------
         10000000000000000000000000000000
    

    【(size & ~MODE_MASK) | (mode & MODE_MASK)的二进制表示】

    UNSPECIFIED:

         00000000000000000000000001100100
    |    00000000000000000000000000000000
    --------------------------------------------------
         00000000000000000000000001100100
    

    计算结果为100。

    EXACTLY:

          00000000000000000000000001100100
    |     01000000000000000000000000000000
    --------------------------------------------------
          01000000000000000000000001100100
    

    计算结果为: 2 * 30 + 100 = 1073741924

    AT_MOST:

          00000000000000000000000001100100
    |     10000000000000000000000000000000
    --------------------------------------------------
          10000000000000000000000001100100
    

    计算结果为: -2 * 2 * 30 + 100 = -2147483548

    所以measureSpec的取值受到size和mode的影响。

    (5)怎么去获取测量规格?
    int measureSpec = MeasureSpec.makeMeasureSpec(当前视图的大小, 测量模式);
    

    measureSpec主要用于子视图的测量,因为onMeasure(int widthMeasureSpec, int heightMeasureSpec)中的两个形参是由父视图中计算的,所以这两个参数应当作为当前视图的测量参数。

    (6)根据测量规格获取测量模式和大小?
        //获取测量模式
        int mode = MeasureSpec.getMode(measureSpec);
        //获取测量大小
        int size = MeasureSpec.getSize(measureSpec);
    
    (7)怎么去测量子视图?

    如果您想要自定义一个类似于线性布局相对布局的ViewGroup,就必须测量子视图(如果是自定义View,非ViewGroup,不需要重写onMeasure方法,因为自定义View不可能存在子视图)。

    测量子视图有以下几种方法:

    • 方法一

      measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有的子布局
      
    • 方法二

        for (int index=0;index<getChildCount();index++){//测量所有的子布局
            View subview = getChildAt(index);
            measureChild(subview, widthMeasureSpec, heightMeasureSpec);
        }
      
    • 方法三

        for (int index=0;index<getChildCount();index++){//测量所有的子布局
            View subview = getChildAt(index);
            MarginLayoutParams lp = (MarginLayoutParams) subview.getLayoutParams();
            int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,0, lp.width);
            int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,0, lp.height);
            subview.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
      

    需要特别注意的是:获取subview的宽和高不能直接使用subview.getWidth()subview.getMeasuredWidth()subview.getHeight()subview.getMeasuredHeight(),因为获取到的数据有时候为0导致测量不准确,这里使用getLayoutParams来获取宽和高,当getLayoutParams获取到数字是-1时,代表MATCH_PARENT,当getLayoutParams获取到的数字是-2时,代表WRAP_CONTENT,当getLayoutParams获取到的数据是固定值时,说明subview的宽或高也是固定值。

    • 方法四

        for (int index=0;index<getChildCount();index++){//测量所有的子布局
            View subview = getChildAt(index);
            measureChildWithMargins(subview, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
      

    需要注意的是:如果用到measureChildWithMargins这个方法,就必须重写generateLayoutParams进行类型转换,否则会报错:

    java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
    

    以下是需要重写方法的代码:

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet params) {
        return new MarginLayoutParams(getContext(), params);
    }
    
    (8)onMeasure为什么会被执行两次?

    在源码中有这样的方法,源码如下:

    private void performTraversals() {
    
                    //...(省略)
    
                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;
    
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
    
                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(mTag,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }
    
                    //...(省略)
    }
    

    其中performMeasure就是执行onMeasure方法,这里performMeasure被执行了两次,原因是lp.horizontalWeight或者lp.verticalWeight的取值大于0;

    那么为什么onMeasure要执行两次呢?

    这里我给出的答案是:系统为了测量的准确性,在某种条件下进行多次测量工作。

    最后,有关onMeasure被执行多次的补充:

    本文的重点是onMeasure方法,从performMeasure源码角度分析,onMeasure可能只被执行一次,也有可能被执行两次,也就是说,最多被执行两次;

    也许你们会说,我的自定义ViewGroup的onMeasure方法怎么被执行了3次?4次?

    我只想确切的说,在执行onLayout方法之前,最多执行2次,如果一旦执行了onLayout方法方法,onMeasure可能会再次执行,因为从源码中可以得到答案:

    ViewRootImpl.java

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    
            //...(省略)
    
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    
            //...(省略)
    
    }
    
    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
    
            //...(省略)
    
            onLayout(changed, l, t, r, b);
    
            //...(省略)
    }
    

    layout方法中,可以找到onMeasure方法,当然,只有当(mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0满足条件的时候onMeasure才能被执行。

    除此之外,以下三个方法也会执行onMeasure方法,在代码中执行以下方法:

    • addView:添加一个控件到当前ViewGroup中
    linearLayout.addView(imageView);
    
    • setVisibility:从隐藏状态切换到显示状态
    linearLayout.setVisibility(View.VISIBLE);
    

    注意:setVisibility(View.GONE)不会触发重新测量。

    • setText:id为text的控件是当前ViewGroup下的子控件
    ((TextView)findViewById(R.id.text)).setText("11111");
    

    最后,兵来将挡,水来土掩,不管onMeasure执行多少次,都不会影响onMeasure下的代码逻辑,在后面篇章中,会详细讲解瀑布流布局,到时候一起来见识一下onMeasure的艺术吧。

    [本章完...]

    相关文章

      网友评论

        本文标题:高级UI<第十篇>:视图的测量(onMeasure)

        本文链接:https://www.haomeiwen.com/subject/mzklwctx.html