View的工作原理(二):measure

本文主要描述measure过程。


一. View的measure过程

  1. View的measure方法为final,该方法中会调用View的onMeasure方法。下面是onMeasure方法:

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

    setMeasuredDimension会设置宽和高,而对于两个参数的值,我们继续追踪:

    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;
    }
    • EXACTLY && AT_MOST: 直接返回了measureSpecspecSize. 也就是View测量过后的MeasureSpec。
    • UNSPECIFIED: 返回了第一个参数,也就是getSuggestedMinimumWidth()getSuggestedMinimumHeight()的返回值。一般用于系统内部的测量过程。

      • 看看这两个方法的代码:

        protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
        }
        protected int getSuggestedMinimumWidth() {
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
      • 上面两个方法的源码很简单,首先判断是否设置了背景mBackground.

        • 如果没有背景(null),那么返回mMinHeightmMinWidth。这两个值对应于android:minHeightandroid:minHeight,如果不指定,那么默认值就为0.
        • 如果指定了背景,就返回max(mMinWidth, mBackground.getMinimumWidth()) (以width属性为例)。再次追踪背景DrawablegetMinimumWidth,可以看出getMinimumWidth返回的就是Drawable的原始宽度:

          public int getMinimumWidth() {
              final int intrinsicWidth = getIntrinsicWidth();
                  return intrinsicWidth > 0 ? intrinsicWidth : 0;
          }

          如果没有有原始宽度,就会返回0。一般来说ShapeDrawable没有原始宽高,BitmapDrawable有原始宽高,即图片的尺寸。

  2. 根据getDefaultSize方法的实现来看,如果自定义控件直接继承View,就需要重写onMeasure方法,否则即使使用wrap_content,其效果等同于match_parent

    • 自定义View使用wrap_content,那么View.specModeAT_MOST, View的宽高等于specSize。在这种情况下,View的specSizeparentSize, 而parentSize是父容器中目前可以使用的大小。此时View的宽高就是父容器当前剩余的空间大小——与match_parent效果一致。

      • 解决方案:给View指定一个默认的内部宽高(widthheight):

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
            if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(width,height);
            } else if (wSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(width, hSpecSize);
            } else if (hSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(wSpecSize,height);
            }
        }

        内部宽高的默认值,要根据自定义View的实际情况来定。

二. ViewGroup的measure过程

  1. ViewGroup除了完成自己的measure过程,还会遍历去调用所有子元素的measure方法,各个子元素再递归地执行这个过程。ViewGroup是个抽象类,它没有重写View的onMeasure方法,而是提供了measureChildren方法:

    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方法:

    //取出子元素的LP参数,通过getChildMeasureSpec来创建子元素的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);
    }
  2. ViewGroup 没有实现onMeasure,所以也没有测量的具体实现。因为不同的ViewGroup子类,有不同的测量特性,它们需要不同的测量实现。

    • LinearLayout的measure:

      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          if (mOrientation == VERTICAL) {
              measureVertical(widthMeasureSpec, heightMeasureSpec);
          } else {
              measureHorizontal(widthMeasureSpec, heightMeasureSpec);
          }
      }

      我们选择查看Vertical情况下的measureVertical……的一小段:

      //在measureVertical方法中
      for (int i = 0; i < count; ++i) {
          ...
          final View child = getVirtualChildAt(i);
          final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
          measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                              heightMeasureSpec, usedHeight);
          final int childHeight = child.getMeasuredHeight();
          final int totalLength = mTotalLength;
          mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                             lp.bottomMargin + getNextLocationOffset(child));
      
          ...
      }
      • 遍历子元素,并对每个子元素调用measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法。

        void measureChildBeforeLayout(View child, int childIndex,
                    int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
                    int totalHeight) {
            measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                        heightMeasureSpec, totalHeight);
        }

        同时通过mTotalLength来存储LinearLayout竖直方向上的初步高度。每测量一个子元素,mTotalLength就会增加一次: 子元素的高度+子元素的topMargin和bottomMargin+下一个子元素的位置偏移量。

      • 当所有子元素测量完毕,LinearLayout就会根据子元素的情况来测量自己的大小。

      • 针对竖直布局而言,它在水平方向的测量过程遵循View的测量过程。但竖直方向则不同:
        • 如果该布局的高度是match_parent或数值,那么它的测量过程和View一致,即高度为specSize.
        • 如果该布局的高度是wrap_content,那么它的高度是所有子元素占用的高度之和,但不能超过其父容器的剩余空间。
        • 其最终高度还要考虑竖直方向的padding。

三. 获取View的宽和高

  1. 问题:在ActivityonCreateonResumeonStart中,获取到的View宽高都是0。怎样才能获取到正确的宽和高呢?
    • 无法正确获取的原因很简单,前文也说过,在我们获取宽高的时候,View的measure尚未完成。
  2. 解决办法1:覆盖Activity/viewonWindowFocusChanged

    • onWindowFocusChanged表示View已经初始化完毕了,此时来获取宽高是没有问题的。
    • 正如方法名,每次Activity的窗口得到或失去焦点时,均会被调用一次。所以,当Acitivity继续执行和暂停执行时,onWindowFocusChanged均会被调用,比如当onResumeonPause被频繁调用时。
    • 使用示例:

      @Override
      public void onWindowFocusChanged(boolean hasFocus) {
          super.onWindowFocusChanged(hasFocus);
          if (hasFocus) {
              int width = btnAddHttp.getMeasuredWidth();
              int height = btnAddHttp.getMeasuredHeight();
          }
      }
  3. 解决办法2:view.post(runnable)

    • 通过post将一个raunnable传递给消息队列,然后等待Looper调用此runnable的时候,View也已经初始化完毕了。
    • 使用示例:

      @Override
      protected void onStart() {
          super.onStart();
          btnAddHttp.post(new Runnable() {
              @Override
              public void run() {
                  int width = btnAddHttp.getMeasuredWidth();
                  int height = btnAddHttp.getMeasuredHeight();
              }
          });
      }
  4. 解决办法3:ViewTreeObserver

    • 正如其名,这是一个View树观察者,我们为这个观察者设置回调接口OnGlobalLayoutListener,然后当View的状态发生改变时,观察者就会被通知到。
    • 随着View的状态改变,观察者会被多次通知,而回调接口也会随之被调用多次。
    • 使用示例:

      @Override
      protected void onStart() {
          super.onStart();
          final ViewTreeObserver viewTreeObserver = btnAddHttp.getViewTreeObserver();
          viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
              @Override
              public void onGlobalLayout() {
                  viewTreeObserver.removeGlobalOnLayoutListener(this);
                  int width = btnAddHttp.getMeasuredWidth();
                  int height = btnAddHttp.getMeasuredHeight();
              }
          });
      }
  5. 解决办法4:view.measure(int widthMesasureSpec, int heightMeasureSpec)

    • 通过手动对View进行measure来得到View的宽高。这里要根据其LP参数来区分具体的情况:

      • match_parent:在这种情况下,无法measure出具体的宽和高。因为按照measure过程和MeasureSpec机制,此时我们无法获得父容器的剩余大小,所以理论上无法测量出View的具体大小。
      • 具体数值(dp/px):如果宽和高分别设置为100和50像素,可以使用下面的measure方法:

        int wMSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int hMSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY);
        btnAddHttp.measure(wMSpec,hMSpec);
      • wrap_content

        int wMSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        int hMSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        btnAddHttp.measure(wMSpec,hMSpec);

        注意这个maic number: (1 << 30) - 1,观察MeasureSpec源码会发现View的尺寸使用30位2进制表示,所以其最大的值是2^30 - 1,也就是我们构造的这个magic number。在最大化模式下,使用理论上的最大值去构造MeasureSpec是合理的。

    • 不太推荐此法,有很多局限性,比如match_parent的情况。
  6. View.MeasureSpec.makeMeasureSpec方法的参数说明

    • @param size the size of the measure specification
      • 应当传入尺寸参数具体数值,而不是LP的MATCH_PARENTWRAP_CONTENT
    • @param mode the mode of the measure specification
      • 应当出入下面三种SpecMode之一:
        • MeasureSpec.AT_MOST
        • MeasureSpec.EXACTLY
        • MeasureSpec.UNSPECIFIED
    • 请构造正确的参数,不要使用错误的调用姿势,比如:

      int wMSpec = View.MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);

      系统无法根据这俩参数得到合法的SpecMode,也不一定能measure出正确的结果。

猜你喜欢

转载自blog.csdn.net/cangely/article/details/80208016