今天主要学习记录一下Android View绘制三部曲的第一步,onMeasure,测量。
起源
在Activity中,所有的View都是DecorView的子View,然后DecorView又是被ViewRootImpl所控制,当Activity显示的时候,ViewRootImpl的performTranversals
方法开始运行,这个方法很长,不过核心的三个流程就是依次调用performMeasure、performLayout、performDraw三个方法,从而完成DecorView的绘制。
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
方法,参数是两个经过设置的MeasureSpec,接下来我们分析一下MeasureSpec是如何设置的。
MeasureSpec
这个MeasureSpec不是实际测绘值,而是父View传递给子View的布局要求,MeasureSpec涵盖了对子View大小和模式的要求。其中,三种模式要求分别是:
- UNSPECIFIED:对子View无任何要求,想要测绘多少由子View决定。
- EXACTLY:父View已确定了自己确切的大小。子View将在这个边界内测绘自己的宽高。
- AT_MOST:父View对子View没有要求,子View可以达到它想要的大小。
首先这个MeasureSpec是个32位的int值,其中31,32两位代表的是三种模式的要求,分别是00….、01….、11….,makeMeasureSpec
方法中,sUseBrokenMakeMeasureSpec默认是false,所以一般执行(size & ~MODE_MASK) | (mode & MODE_MASK)这个语句,这个意思就是说,MeasureSpec的高两位代表的是模式,低30位代表父View的尺寸。
下面是对应的方法:
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; 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); } }
|
View#measure
计算完MeasureSpec,DecorView就该执行measure
方法了。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ··· final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; ··· final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) { ··· int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { ··· } ··· } ··· } ··· }
|
measure方法是final的,所以不能重写,不过measure方法最主要的作用就是调用了onMeasure方法,由于DecorView是继承的FrameLayout,所以本篇文章我们主要分析FrameLayout的onMeasure方法。
FrameLayout#onMeasure
这个onMeasure方法,可能是View绘制中最难理解的了,所以我们逐步分析。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //获取子View个数 int count = getChildCount();
//判断是否是确定宽高的 //如果宽高都确定,那么boolean为false //如果有一个不确定,那么boolean为true 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宽高,包括Margin参数以及padding参数 //该方法详细分析见下文 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 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); //用按位或的方法合并所有子View的State //getMeasuredState方法详细解析在下面 childState = combineMeasuredStates(childState, child.getMeasuredState()); //如果宽高有不确定的(即warp_content模式),将子View中宽或高是match_parent的添加到mMatchParentChildren中。 if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } }
// 计算padding maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// 与最小宽高作比较,二者取较大的 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 与前景图宽高作比较,二者取较大的 final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); }
//计算并保存measured宽高 //resolveSizeAndState方法分析在下面 //setMeasuredDimension方法分析在下面 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
//计算macth_parent的子View的个数 count = mMatchParentChildren.size(); //只有FrameLayout中宽或者高有warp_content属性, //并且match_parent的子view个数大于1才会执行下面代码。 //因为如果宽高都是match_parent的,或者设置好dp数值的,则mMatchParentChildren永远是空的。
//在这里会重新计算传递给子View的MeasureSpec值,并重新测绘子View。 //关于MeasureSpec值的计算,可以参考下文ViewGroup#getChildMeasureSpec的表格。 //这里需要注意的是,match_parent行所有的结果均改为:EXACTLY + parentSize 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); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); } else { 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.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
|
ViewGroup#measureChildWithMargins
该方法作用是测绘子View,在父View的onMeasure中循环调用,达到遍历的效果。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { //获取子View的LayoutParams final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//计算子View的MeasureSpec //getChildMeasureSpec方法分析见下文 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); //调用子View的measure方法,对子View进行测绘 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
|
ViewGroup#getChildMeasureSpec
此方法通过父View的MeasureSpec值以及LayoutParams的宽高,来生成子View的MeasureSpec值,具体代码如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //计算父View的size和mode int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec);
//计算刨除padding的size 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: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } 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 asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
|
上面代码,其实通过一张表格,就能很清楚的解释转换规律
竖列代表LayoutParams的宽或高属性\横行父View传递的Mode |
EXACTLY |
AT_MOST |
UNSPECIFIED |
精确值 |
EXACTLY + childSize |
EXACTLY + childSize |
EXACTLY + childSize |
match_parent |
EXACTLY + parentSize |
AT_MOST + parentSize |
UNSPECIFIED + parentSize |
warp_content |
AT_MOST + parentSize |
AT_MOST + parentSize |
UNSPECIFIED + parentSize |
View#getMeasuredState
public final int getMeasuredState() { return (mMeasuredWidth&MEASURED_STATE_MASK) | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT) & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)); }
|
该方法返回一个表达View宽高measure_state值的整数,第8位代表height是否是MEASURED_STATE_TOO_SMALL,第24为代表width是否是MEASURED_STATE_TOO_SMALL的。
例如:
宽是MEASURED_STATE_TOO_SMALL的
返回:0000 0001 0000 0000 0000 0000 0000 0000
高是MEASURED_STATE_TOO_SMALL的
返回:0000 0000 0000 0000 0000 0001 0000 0000
宽高均是MEASURED_STATE_TOO_SMALL的
返回:0000 0001 0000 0000 0000 0001 0000 0000
View#resolveSizeAndState
public static final int MEASURED_STATE_TOO_SMALL = 0x01000000; public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { //计算父view传递的size和mode final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; switch (specMode) { case MeasureSpec.AT_MOST: if (specSize < size) { //如果父View给的size小于自身测绘出的size, //则在第24位上加上measure_state标记。 result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } //如果child在第24为上有measure_state标记,则在result的第24位上也加上measure_state标记,然后返回result。 return result | (childMeasuredState & MEASURED_STATE_MASK); }
|
View#setMeasuredDimension
该方法将计算好的measuredWidth和measuredHeight设置给成员变量mMeasuredWidth及mMeasuredHeight,并且将flag设置成PFLAG_MEASURED_DIMENSION_SET。
在4.3版本以上,如果设置了optical模式,则还要对width、height进一步修改,然后再设置mMeasuredWidth、mMeasuredHeight。
mMeasuredWidth和mMeasuredHeight的值,不光包括size,同时还包括state,具体请看getMeasuredWidth
、getMeasuredWidthAndState
、getMeasuredHeight
、getMeasuredHeightAndState
方法
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
|
时序图
图为onMeasure时序图
小结
到这里,关于View的测绘我们大概的走了一遍。Measure的原理就是通过遍历,从上至下,利用传递的MeasureSpec以及子View的LayoutParams,依次进行测绘。不同的layout可能会进行多次的measure,所以熟读源码,合理布局,可以帮我们避免不必要的measure开销,达到提升性能的效果。
最后,感谢阅读,也希望可以和大家多多交流,共同进步。
系列文章
Android 视图及View绘制分析笔记之setContentView
View绘制分析笔记之onMeasure
View绘制分析笔记之onLayout
View绘制分析笔记之onDraw
- 3.View绘制分析笔记之onLayout
上一篇文章我们了解了View的onMeasure,那么今天我们继续来学习Android View绘制三部曲的第二步,onLayout,布局. ViewRootImpl#performLayout pr ...
- 4.View绘制分析笔记之onDraw
上一篇文章我们了解了View的onLayout,那么今天我们来学习Android View绘制三部曲的最后一步,onDraw,绘制. ViewRootImpl#performDraw private ...
- 1.Android 视图及View绘制分析笔记之setContentView
自从1983年第一台图形用户界面的个人电脑问世以来,几乎所有的PC操作系统都支持可视化操作,Android也不例外.对于所有Android Developer来说,我们接触最多的控件就是View.通常 ...
- Android笔记--View绘制流程源码分析(二)
Android笔记--View绘制流程源码分析二 通过上一篇View绘制流程源码分析一可以知晓整个绘制流程之前,在activity启动过程中: Window的建立(activit.attach生成), ...
- Android笔记--View绘制流程源码分析(一)
Android笔记--View绘制流程源码分析 View绘制之前框架流程分析 View绘制的分析始终是离不开Activity及其内部的Window的.在Activity的源码启动流程中,一并包含 着A ...
- Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
- Android中View绘制流程以及invalidate()等相关方法分析
[原文]http://blog.csdn.net/qinjuning 整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简 ...
- Android之View绘制流程源码分析
版权声明:本文出自汪磊的博客,转载请务必注明出处. 对于稍有自定义View经验的安卓开发者来说,onMeasure,onLayout,onDraw这三个方法都不会陌生,起码多少都有所接触吧. 在安卓中 ...
- Android中View绘制流程以及invalidate()等相关方法分析(转)
转自:http://blog.csdn.net/qinjuning 前言: 本文是我读<Android内核剖析>第13章----View工作原理总结而成的,在此膜拜下作者 .同时真挚地向渴 ...
随机推荐
- Hive : UDFArgumentTypeException Exactly one argument is expected.
Hive执行时Failed.. 分段执行发现排除一些聚合函数或者内置函数后可以正常执行.. 因为Hive-Sql语句比较长..有很多的case when then 排除后发现是聚合函数的用法问题.. ...
- win10连vpn
1.首先卸载网络适配器下所有的WAN Miniport 2.打开命令提示符,输入:netsh interface ipv4 uninstall 卸载TCP/IPv4协议. 3.重启电脑后再次打开“命令 ...
- 向maven中央仓库提交jar
从来都是从中央仓库下载jar,这次需要向中央仓库提交jar, 利用Sonatype OSSRH可以把jar等资源提交给Maven的中央仓库. Sonatype OSSRH介绍: Sonatype OS ...
- 参加SFDC的感触
今天参加了SFDC. 第一次参加这样的技术大会,感触总是有点. 简单的记录如下 1.自动化运维在行业已经成为趋势,未来DevOps 作为开发和运维统一已经是新时代对开发人员的要求.公司架构一个自己的自 ...
- vmware workstation安装 Mosrosoft Runtime DLL安装程序未能完成安装
不要点确定.开始菜单运行输入'%temp%',在弹出的窗体中找到一个文件名中含'{132E3257-14F1-411A-BC6C-0CA32D3A9BC6}~setup'(不一定一样,反正就是第一行的 ...
- 又一枚精彩的弹幕效果jQuery实现
精彩的弹幕效果分享给大家,具有一定的参考价值,感兴趣的朋友可以尝试制作弹幕,具体内容如下 简易弹幕效果:将发布的内容随机显示在弹幕右侧,逐渐左移最后消失. 涉及知识点:val().random ...
- 移动前端开发之viewport的深入理解
在移动设备上进行网页的重构或开发,首先得搞明白的就是移动设备上的viewport了,只有明白了viewport的概念以及弄清楚了跟viewport有关的meta标签的使用,才能更好地让我们的网页适配或 ...
- js div及table首行顶部吸附示例
js div顶部吸附示例,例如这样: 以下为DIV顶部吸附示例:(此示例来自于网络,在此记录与分享!感谢原作者!) 示例1:兼容IE6 <div style="height:300px ...
- WPF程序将DLL嵌入到EXE的两种方法
WPF程序将DLL嵌入到EXE的两种方法 这一篇可以看作是<Visual Studio 版本转换工具WPF版开源了>的续,关于<Visual Studio 版本转换工具WPF版开源了 ...
- Maven生命周期小记
1.Maven生命周期是为了所有的构建过程进行抽象和统一.Maven从大量的项目和构建工具中学习和反思,总结了一套高度完善.易扩展的生命周期.这个生命周期包含了项目的清理.初始化.编译.测试.打包.集 ...