安卓自定义view(二) - 测量

view的测量过程

之所以先讲view的测量过程,是因为ViewGroup测量的时候是先把他的所有子view测量完成后才能测量viewgroup自身,view的measure是viewGroup来调用的。在view的测量过程中,parentView首先会调用getChildMeasureSpec(int spec, int padding, int childDimension)

/**
 * Does the hard part of measureChildren: figuring out the MeasureSpec to
 * pass to a particular child. This method figures out the right MeasureSpec
 * for one dimension (height or width) of one child view.
 *
 * 这个方法将根据父容器的MeasureSpec和子View LayoutParams中的宽/高
 * 为子View生成最合适的MeasureSpec
 *
 * @param spec 父容器的MeasureSpec
 * @param padding 父容器的内间距(padding)加上子View的外间距(margin)
 * @param childDimension 子View的LayoutParams中封装的width/height
 * @return 子View的MeasureSpec
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // ① 对父容器的MeasureSpec进行解包
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    // ② 减去间距,得到最大可用空间
    int size = Math.max(0, specSize - padding);

    // 记录子View最终的大小和测量模式
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // ③ 父容器是精准测量模式
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {               //如果子view在LayoutParam中指定了大小,那么子view的resultSize 就是该大小,模式是EXACTLY
            resultSize = childDimension; 
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {       //如果子view中LayoutParams是MATCH_PARENT,则view的大小等于最大可用大小,测量模式是EXACTLY
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {   //如果子view中LayoutParams是WRAP_CONTENT,则view的大小等于最大可用大小,测量模式是AT_MOST

            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

     // ④ 父容器指定了一个最大可用的空间
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // ⑤ 父容器不对子View的大小作出限制
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    // ⑥ 将最终的size和mode打包为子View需要的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

计算该view的WidthMeasureSpec和HeightMeasureSpec,然后调用view的measure();

public final void measure(int widthMeasureSpec, int  heightMeasureSpec) {
...

        onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}

measure方法又调用了onMeasure方法,view.OnMeasure()才是真正完成了view的测量。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(   //设置测量的结果
    getDefaultSize(getSuggestedMinimumWidth(),
                                widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(),
                                 heightMeasureSpec));
}

在view.onMeasure中调用setMeasuredDimension来保存测量的结果,那么测量结果是怎么来的呢?我们继续看getDefaultSize();

public static int getDefaultSize(int size, int measureSpec) {
  int result = size;
 //1、获得MeasureSpec的mode
  int specMode = MeasureSpec.getMode(measureSpec);
 //2、获得MeasureSpec的specSize
  int specSize = MeasureSpec.getSize(measureSpec);

  switch (specMode) {
  case MeasureSpec.UNSPECIFIED:
    //这个我们先不看他
      result = size;
      break;
  case MeasureSpec.AT_MOST:
  case MeasureSpec.EXACTLY:
  //3、可以看到,最终返回的size就是我们MeasureSpec中测量得到的size
      result = specSize;
      break;
  }
  return result;
}

在getDefaultSize中传入了两个参数,一个是最小大小,一个是MeasureSpec。首先获得MeasureSpec中的测量模式和暂定大小,然后根据测量模式来返回不同的测量结果。如果测量模式是UNSPECIFIED,则测量的结果就是最小的大小,如果测量模式是AT_MOST或者EXACTLY,测量的大小都是MeasureSpec中的大小。
而MeasureSpec中的大小是viewgroup调用getChildMeasureSpec生成的,查看getChildMeasureSpec的逻辑我们会看出,如果子view的layoutParem是warp_content,那么测量结果的测量模式就是AT_MOST,测量大小就是父容器的最大可用空间,所以我们在继承view自定义view的时候,如果重写onMeasure,那么设置LayoutParam设置warp_content是不能用的,大小始终是可用的最大大小。

那么getDefaultSize的最小大小是怎么来的呢?是通过getSuggestedMinimumWidth()和getSuggestedMinimumHeight()得到,我们继续看getSuggestedMinimumWidth。

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

在getSuggestedMinimumWidth中,首先判断该view有没有背景,如果没有背景,返回android:minWidth(如果没有设置android:minWidth,就默认是0),如果有背景,就返回背景大小和android:minWidth中的最大值。

最后,onMeasure讲测量的结果调用

getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),

保存起来,这样一个view就算测量完成了。
画个图来总结一下。
首先viewGroup会计算子view的MeasureSpec,然后讲所计算的MeasureSpec传入子view的measure中,子view开始测量自身。measure又会调用onMeasure来完成真正的测量过程。onMeasure会调用setMeasureDemension来保存测量的结果,测量结果是通过getDefaultSize来计算的,getDefaultSize通过比较最小的大小和MeasureSpec中的暂定大小,最终确定测量大小。

view.png

viewGroup的测量过程

viewGroup中,首先会测量所有子view的大小,然后根据子view的大小来确定viewGroup的大小。由于不同的ViewGroup的测量结果不一样(LinearLayout的height是所有view的height之和,而FrameLayout的Height是所有子view的最大的height),所以viewGroup中没有实现具体的onMeasure();我们以FrameLayout来举例。
首先还是measure()方法调用onMeaure方法,具体的测量实现我们看onMeasure()即可

//这里的widthMeasureSpec、heightMeasureSpec
//其实就是我们frameLayout可用的widthMeasureSpec 、
//heightMeasureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  //1、获得frameLayout下childView的个数
  int count = getChildCount();
//2、看这里的代码我们可以根据前面的Measure图来进行分析,因为只要parent
//不是EXACTLY模式,以frameLayout为例,假设frameLayout本身还不是EXACTL模式,
 // 那么表示他的大小此时还是不确定的,从表得知,此时frameLayout的大小是根据
 //childView的最大值来设置的,这样就很好理解了,也就是childView测量好后还要再
//测量一次,因为此时frameLayout的值已经可以算出来了,对于child为MATCH_PARENT
//的,child的大小也就确定了,理解了这里,后面的代码就很 容易看懂了
  final boolean measureMatchParentChildren =
          MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
          MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
   //3、清理存储模式为MATCH_PARENT的child的队列
  mMatchParentChildren.clear();
  //4、下面三个值最终会用来设置frameLayout的大小
  int maxHeight = 0;
  int maxWidth = 0;
  int childState = 0;
  //5、开始便利frameLayout下的所有child
  for (int i = 0; i < count; i++) {
      final View child = getChildAt(i);
      //6、小发现哦,只要mMeasureAllChildren是true,就算child是GONE也会被测量哦,
      if (mMeasureAllChildren || child.getVisibility() != GONE) {
          //7、开始测量childView 
          measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

          //8、下面代码是获取child中的width 和height的最大值,后面用来重新设置frameLayout,有需要的话
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
          maxWidth = Math.max(maxWidth,
                  child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
          maxHeight = Math.max(maxHeight,
                  child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
          childState = combineMeasuredStates(childState, child.getMeasuredState());

        //9、如果frameLayout不是EXACTLY,
          if (measureMatchParentChildren) {
              if (lp.width == LayoutParams.MATCH_PARENT ||
                      lp.height == LayoutParams.MATCH_PARENT) {
//10、存储LayoutParams.MATCH_PARENT的child,因为现在还不知道frameLayout大小,
//也就无法设置child的大小,后面需重新测量
                  mMatchParentChildren.add(child);
              }
          }
      }
  }

    ....
  //11、这里开始设置frameLayout的大小
  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
          resolveSizeAndState(maxHeight, heightMeasureSpec,
                  childState << MEASURED_HEIGHT_STATE_SHIFT));

//12、frameLayout大小确认了,我们就需要对宽或高为LayoutParams.MATCH_PARENTchild重新测量,设置大小
  count = mMatchParentChildren.size();
  if (count > 1) {
      for (int i = 0; i < count; i++) {
          final View child = mMatchParentChildren.get(i);
          final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
          final int childWidthMeasureSpec;
          if (lp.width == LayoutParams.MATCH_PARENT) {
              final int width = Math.max(0, getMeasuredWidth()
                      - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                      - lp.leftMargin - lp.rightMargin);

  //13、注意这里,为child是EXACTLY类型的childWidthMeasureSpec,
  //也就是大小已经测量出来了不需要再测量了
  //通过MeasureSpec.makeMeasureSpec生成相应的MeasureSpec
              childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                      width, MeasureSpec.EXACTLY);
          } else {

  //14、如果不是,说明此时的child的MeasureSpec是EXACTLY的,直接获取child的MeasureSpec,
              childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                      getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                      lp.leftMargin + lp.rightMargin,
                      lp.width);
          }

  // 这里是对高做处理,与宽类似
          final int childHeightMeasureSpec;
          if (lp.height == LayoutParams.MATCH_PARENT) {
              final int height = Math.max(0, getMeasuredHeight()
                      - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                      - lp.topMargin - lp.bottomMargin);
              childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                      height, MeasureSpec.EXACTLY);
          } else {
              childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                      getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                      lp.topMargin + lp.bottomMargin,
                      lp.height);
          }

  //最终,再次测量child
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
  }
}
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        // 获取子视图的布局参数
        final LayoutParams lp = child.getLayoutParams();

        // 调用getChildMeasureSpec(),根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
         // getChildMeasureSpec()请回看上面的解析
         // 获取 ChildView 的 widthMeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        // 获取 ChildView 的 heightMeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        // 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

从上述代码我们可以将viewGroup的测量过程分为一下几个步骤

1.初始化变量

获取childCount,清理存储模式为MATCH_PARENT的child的队列,初始化最大宽度和最大高度。

2.遍历所有子view,获取子view的MeasureSpec,测量子view的大小,获得所有子view的最大宽度和最大高度

通过for循环遍历所有的子view,对于每一个子view,调用measureChildWithMargins,measureChildWithMargins中首先获得view的MeasureSpec,然后调用子view的measure完成子view的测量。

3. 根据自身MeasureSpec和子view的最大宽度和最大高度,确定自身大小

4. 重新确定LayoutParam==MATCH_PARENT的子view

由于LayoutParam==MATCH_PARENT的子view的大小还没有确定,根据已经确定的Viewgroup的大小,重新计算子view的精确大小。

最后viewGroup和view的测量过程结合起来画一张图

viewGroup.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容