文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android View 测量流程(Measure)全面解析

2022-06-06 04:25

关注

前言

上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而ViewRootImpl则负责渲染视图,它调用了一个performTraveals方法使得ViewTree开始三大工作流程,然后使得View展现在我们面前。本篇文章主要内容是:详细讲述View的测量(Measure)流程,主要以源码的形式呈现,源码均取自Android API 21.

从ViewRootImpl#PerformTraveals说起

我们直接从这个方法说起,因为它是整个工作流程的核心,我们看看它的源码:


private void performTraversals() {
  ...
 if (!mStopped) {
  int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
  int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 
  }
 }
 if (didLayout) {
  performLayout(lp, desiredWindowWidth, desiredWindowHeight);
  ...
 }
 if (!cancelDraw && !newSurface) {
  if (!skipDraw || mReportNextDraw) {
  if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
   for (int i = 0; i < mPendingTransitions.size(); ++i) {
   mPendingTransitions.get(i).startChangingAnimations();
   }
   mPendingTransitions.clear();
  }
  performDraw();
  }
 } 
 ...
}

方法非常长,这里做了精简,我们看到它里面主要执行了三个方法,分别是performMeasure、performLayout、performDraw这三个方法,在这三个方法内部又会分别调用measure、layout、draw这三个方法来进行不同的流程。我们先来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法,它传入两个参数,分别是childWidthMeasureSpec和childHeightMeasure,那么这两个参数代表什么意思呢?要想了解这两个参数的意思,我们就要先了解MeasureSpec。

理解MeasureSpec

MeasureSpec是View类的一个内部类,我们先看看官方文档对MeasureSpec类的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是说,该类封装了一个View的规格尺寸,包括View的宽和高的信息,但是要注意,MeasureSpec并不是指View的测量宽高,这是不同的,是根据MeasueSpec而测出测量宽高。
MeasureSpec的作用在于:在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的测量宽高。
我们来看看这个类的源码:


public static class MeasureSpec {
 private static final int MODE_SHIFT = 30;
 private static final int MODE_MASK = 0x3 << MODE_SHIFT;
  
 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;
 //将size和mode打包成一个32位的int型数值
 //高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小
 public static int makeMeasureSpec(int size, int mode) {
  if (sUseBrokenMakeMeasureSpec) {
  return size + mode;
  } else {
  return (size & ~MODE_MASK) | (mode & MODE_MASK);
  }
 }
 //将32位的MeasureSpec解包,返回SpecMode,测量模式
 public static int getMode(int measureSpec) {
  return (measureSpec & MODE_MASK);
 }
 //将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小
 public static int getSize(int measureSpec) {
  return (measureSpec & ~MODE_MASK);
 }
 //...
 }

可以看出,该类的思路是相当清晰的,对于每一个View,包括DecorView,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽高信息,在其他流程通过getMode或getSize得到模式和宽高。那么问题来了,上面提到MeasureSpec是LayoutParams和父容器的模式所共同影响的,那么,对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢?

为了解决这个疑问,我们回到ViewRootImpl#PerformTraveals方法,看①号代码处,调用了getRootMeasureSpec(desiredWindowWidth,lp.width)方法,其中desiredWindowWidth就是屏幕的尺寸,并把返回结果赋值给childWidthMeasureSpec成员变量(childHeightMeasureSpec同理),因此childWidthMeasureSpec(childHeightMeasureSpec)应该保存了DecorView的MeasureSpec,那么我们看一下ViewRootImpl#getRootMeasureSpec方法的实现:



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;
 //省略...
 }
 return measureSpec;
}

思路也很清晰,根据不同的模式来设置MeasureSpec,如果是LayoutParams.MATCH_PARENT模式,则是窗口的大小,WRAP_CONTENT模式则是大小不确定,但是不能超过窗口的大小等等。

那么到目前为止,就已经获得了一份DecorView的MeasureSpec,它代表着根View的规格、尺寸,在接下来的measure流程中,就是根据已获得的根View的MeasureSpec来逐层测量各个子View。我们顺着①号代码往下走,来到performMeasure方法,看看它做了什么工作,ViewRootImpl#performMeasure:


private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
 try {
 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 } finally {
 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
 }
}

方法很简单,直接调用了mView.measure,这里的mView就是DecorView,也就是说,从顶级View开始了测量流程,那么我们直接进入measure流程。

measure 测量流程

ViewGroup的测量流程

由于DecorView继承自FrameLayout,是PhoneWindow的一个内部类,而FrameLayout没有measure方法,因此调用的是父类View的measure方法,我们直接看它的源码,View#measure:


public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
 boolean optical = isLayoutModeOptical(this);
 if (optical != isLayoutModeOptical(mParent)) {
 ...
 if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
  widthMeasureSpec != mOldWidthMeasureSpec ||
  heightMeasureSpec != mOldHeightMeasureSpec) {
  ...
  if (cacheIndex < 0 || sIgnoreMeasureCache) {
  // measure ourselves, this should set the measured dimension flag back
  onMeasure(widthMeasureSpec, heightMeasureSpec);
  mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
  } 
 ...
}

可以看到,它在内部调用了onMeasure方法,由于DecorView是FrameLayout子类,因此它实际上调用的是DecorView#onMeasure方法。在该方法内部,主要是进行了一些判断,这里不展开来看了,到最后会调用到super.onMeasure方法,即FrameLayout#onMeasure方法。

由于不同的ViewGroup有着不同的性质,那么它们的onMeasure必然是不同的,因此这里不可能把所有布局方式的onMeasure方法都分析一遍,因此这里选择了FrameLayout的onMeasure方法来进行分析,其它的布局方式读者可以自行分析。那么我们继续来看看这个方法:


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 //获取当前布局内的子View数量
 int count = getChildCount();
 //判断当前布局的宽高是否是match_parent模式或者指定一个精确的大小,如果是则置measureMatchParent为false.
 final boolean measureMatchParentChildren =
  MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
  MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
 mMatchParentChildren.clear();
 int maxHeight = 0;
 int maxWidth = 0;
 int childState = 0;
 //遍历所有类型不为GONE的子View
 for (int i = 0; i < count; i++) {
 final View child = getChildAt(i);
 if (mMeasureAllChildren || child.getVisibility() != GONE) {
  //对每一个子View进行测量
  measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
  final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  //寻找子View中宽高的最大者,因为如果FrameLayout是wrap_content属性
  //那么它的大小取决于子View中的最大者
  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());
  //如果FrameLayout是wrap_content模式,那么往mMatchParentChildren中添加
  //宽或者高为match_parent的子View,因为该子View的最终测量大小会受到FrameLayout的最终测量大小影响
  if (measureMatchParentChildren) {
  if (lp.width == LayoutParams.MATCH_PARENT ||
   lp.height == LayoutParams.MATCH_PARENT) {
   mMatchParentChildren.add(child);
  }
  }
 }
 }
 // Account for padding too
 maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
 maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
 // Check against our minimum height and width
 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
 maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
 // Check against our foreground's minimum height and width
 final Drawable drawable = getForeground();
 if (drawable != null) {
 maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
 maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
 }
 //保存测量结果
 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
  resolveSizeAndState(maxHeight, heightMeasureSpec,
   childState << MEASURED_HEIGHT_STATE_SHIFT));
 //子View中设置为match_parent的个数
 count = mMatchParentChildren.size();
 //只有FrameLayout的模式为wrap_content的时候才会执行下列语句
 if (count > 1) {
 for (int i = 0; i < count; i++) {
  final View child = mMatchParentChildren.get(i);
  final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
  //对FrameLayout的宽度规格设置,因为这会影响子View的测量
  final int childWidthMeasureSpec;
  
  if (lp.width == LayoutParams.MATCH_PARENT) {
  final int width = Math.max(0, getMeasuredWidth()
   - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
   - lp.leftMargin - lp.rightMargin);
  childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
   width, MeasureSpec.EXACTLY);
  } else {
  childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
   getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
   lp.leftMargin + lp.rightMargin,
   lp.width);
  }
  //同理对高度进行相同的处理,这里省略...
  //对于这部分的子View需要重新进行measure过程
  child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 }
 }
}

由以上的FrameLayout的onMeasure过程可以看出,它还是做了相当多的工作的,这里简单总结一下:首先,FrameLayout根据它的MeasureSpec来对每一个子View进行测量,即调用measureChildWithMargin方法,这个方法下面会详细说明;对于每一个测量完成的子View,会寻找其中最大的宽高,那么FrameLayout的测量宽高会受到这个子View的最大宽高的影响(wrap_content模式),接着调用setMeasureDimension方法,把FrameLayout的测量宽高保存。最后则是特殊情况的处理,即当FrameLayout为wrap_content属性时,如果其子View是match_parent属性的话,则要重新设置FrameLayout的测量规格,然后重新对该部分View测量。

在上面提到setMeasureDimension方法,该方法用于保存测量结果,在上面的源码里面,该方法的参数接收的是resolveSizeAndState方法的返回值,那么我们直接看View#resolveSizeAndState方法:


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) {
 case MeasureSpec.AT_MOST:
  if (specSize < size) {
  result = specSize | MEASURED_STATE_TOO_SMALL;
  } else {
  result = size;
  }
  break;
 case MeasureSpec.EXACTLY:
  result = specSize;
  break;
 case MeasureSpec.UNSPECIFIED:
 default:
  result = size;
 }
 return result | (childMeasuredState & MEASURED_STATE_MASK);
}

可以看到该方法的思路是相当清晰的,当specMode是EXACTLY时,那么直接返回MeasureSpec里面的宽高规格,作为最终的测量宽高;当specMode时AT_MOST时,那么取MeasureSpec的宽高规格和size的最小值。(注:这里的size,对于FrameLayout来说,是其最大子View的测量宽高)。

小结:那么到目前为止,以DecorView为切入点,把ViewGroup的测量流程详细地分析了一遍,在ViewRootImpl#performTraversals中获得DecorView的尺寸,然后在performMeasure方法中开始测量流程,对于不同的layout布局有着不同的实现方式,但大体上是在onMeasure方法中,对每一个子View进行遍历,根据ViewGroup的MeasureSpec及子View的layoutParams来确定自身的测量宽高,然后最后根据所有子View的测量宽高信息再确定父容器的测量宽高。

那么接下来,我们继续分析对于一个子View来说,是怎么进行测量的。

View的测量流程

还记得我们上面在FrameLayout测量内提到的measureChildWithMargin方法,它接收的主要参数是子View以及父容器的MeasureSpec,所以它的作用就是对子View进行测量,那么我们直接看这个方法,ViewGroup#measureChildWithMargins:


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

由源码可知,里面调用了getChildMeasureSpec方法,把父容器的MeasureSpec以及自身的layoutParams属性传递进去来获取子View的MeasureSpec,这也印证了“子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定”这个结论。那么,我们一起来看看ViewGroup#getChildMeasureSpec方法:


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
 int specMode = MeasureSpec.getMode(spec);
 int specSize = MeasureSpec.getSize(spec);
 //size表示子View可用空间:父容器尺寸减去padding
 int size = Math.max(0, specSize - padding);
 int resultSize = 0;
 int resultMode = 0;
 switch (specMode) {
 // Parent has imposed an exact size on us
 case MeasureSpec.EXACTLY:
 if (childDimension >= 0) {
  resultSize = childDimension;
  resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.MATCH_PARENT) {
  // Child wants to be our size. So be it.
  resultSize = size;
  resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  // Child wants to determine its own size. It can't be
  // bigger than us.
  resultSize = size;
  resultMode = MeasureSpec.AT_MOST;
 }
 break;
 // Parent has imposed a maximum size on us
 case MeasureSpec.AT_MOST:
 //省略..具体可自行参考源码
 break;
 // Parent asked to see how big we want to be
 case MeasureSpec.UNSPECIFIED:
 //省略...具体可自行参考源码
 break;
 }
 return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面方法也非常容易理解,大概是根据不同的父容器的模式及子View的layoutParams来决定子View的规格尺寸模式等。那么,这里根据上面的逻辑,列出不同的父容器的MeasureSpec和子View的LayoutParams的组合情况下所出现的不同的子View的MeasureSpec:

(注:该表格呈现形式参考自《Android 开发艺术探索》 任玉刚 著)

当子View的MeasureSpec获得后,我们返回measureChildWithMargins方法,接着就会执行①号代码:child.measure方法,意味着,绘制流程已经从ViewGroup转移到子View中了,可以看到传递的参数正是我们刚才获取的子View的MeasureSpec,接着会调用View#measure,这在上面说过了,这里不再赘述,然后在measure方法,会调用onMeasure方法,当然了,对于不同类型的View,其onMeasure方法是不同的,但是对于不同的View,即使是自定义View,我们在重写的onMeasure方法内,也一定会调用到View#onMeasure方法的,因此我们看看它的源码:


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

显然,这里调用了setMeasureDimension方法,上面说过该方法的作用是设置测量宽高,而测量宽高则是从getDefaultSize中获取,我们继续看看这个方法View#getDefaultSize:


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

好吧,又是类似的代码,根据不同模式来设置不同的测量宽高,我们直接看AT_MOST和EXACTLY模式,它直接把specSize返回了,即View在这两种模式下的测量宽高直接取决于specSize规格。也即是说,对于一个直接继承自View的自定义View来说,它的wrap_content和match_parent属性的效果是一样的,因此如果要实现自定义View的wrap_content,则要重写onMeasure方法,对wrap_content属性进行处理。
接着,我们看UNSPECIFIED模式,这个模式可能比较少见,一般用于系统内部测量,它直接返回的是size,而不是specSize,那么size从哪里来的呢?再往上看一层,它来自于getSuggestedMinimumWidth()或getSuggestedMinimumHeight(),我们选取其中一个方法,看看源码,View#getSuggestedMinimumWidth:


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

从以上逻辑可以看出,当View没有设置背景的时候,返回mMinWidth,该值对应于android:minWidth属性;如果设置了背景,那么返回mMinWidth和mBackground.getMinimumWidth中的最大值。那么mBackground.getMinimumWidth又是什么呢?其实它代表了背景的原始宽度,比如对于一个Bitmap来说,它的原始宽度就是图片的尺寸。到此,子View的测量流程也完成了。

总结

这里简单概括一下整个流程:测量始于DecorView,通过不断的遍历子View的measure方法,根据ViewGroup的MeasureSpec及子View的LayoutParams来决定子View的MeasureSpec,进一步获取子View的测量宽高,然后逐层返回,不断保存ViewGroup的测量宽高。

从文章开始到现在,View的测量流程已经全部分析完毕,View的measure流程是三大流程中最复杂的一个流程,其中的MeasureSpec贯穿了整个测量流程,占有非常重要的地位,希望读者仔细体会这个流程,最后希望这篇文章能帮助你对View的测量流程有进一步的了解,谢谢阅读。

更多阅读
Android View 布局流程(Layout)完全解析
Android View 绘制流程(Draw) 完全解析

您可能感兴趣的文章:Android测量每秒帧数Frames Per Second (FPS)的方法Android View如何测量Android视图的绘制流程(上) View的测量Android精确测量文本宽高及基线位置的方法


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯