一、前言
Android View 显示在屏幕中需经过 measure、layout(ViewGroup 独有)、draw 三个步骤完成,其中 View 的尺寸测量是三个步骤中最为复杂的一个,理解好 View 的 measure 过程,是理解另外两个流程的基础。
View 的整个绘制流程从 ViewRootImpl 类的 performTraversals 方法开始,该类是连接 DecorView 和 WindowManager 的纽带,实现了 ViewParent 接口,负责将整个视图树绘制在 Android 应用程序窗口中。
performTraversals 方法会依次经过 performMeasure、performLayout、performDraw 三个方法,分别完成 View 的测量、布局和绘制流程。
其中,performMeasure 方法中会调用 View 的 measure 方法,而 measure
方法又会调用 onMeasure 方法。以此类推,其他两个流程基本也是这样的调用顺序。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
实际开发中,我们只需要关心如何实现 onMeasure、onLayout、onDraw 方法,但是实现这三个方法进行自定义 View 的关键还是要理解 View 的整个渲染流程和原理。
二、measure 过程
2.1 MeasureSpec
MeasureSpec 是 View 尺寸测量的基本单位,其通过一个 30 位的 int 值承载了 View 尺寸、规格信息,高 2 位表示 SpecMode,低 30 位表示 SpecSize。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << View.MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
可通过该类的 makeMeasureSpec 方法构造一个包含 View 尺寸、规格信息的 int 值,通过 getMode、getSize 方法分别获取指定的 int 值中包含的规格、尺寸信息。
其实,MeasureSpec 没有特别的含义,只是一个将 View 尺寸和规格信息打包和解包成具体 int 值的工具类。
来看一下 SpecMode 包含的三种类型:
模式 | 说明 |
---|---|
EXACTLY |
精确模式,表示父视图希望子视图的大小应该由 SpecSize 决定,ViewGroup 默认采用该规则设置子 View 的大小。实际开发中,开发人员可灵活设置 View 的大小,不必依赖系统默认的这一规则。match_parent、固定值(如 20dp)一般对应该模式,当然,前面已经说了,开发人员可以灵活设置 View 大小和规格。也就是说,不管父 View 的大小和模式如何,只要达到开发目的,可以设置子 View 为任何模式和大小。 |
AT_MOST |
至多模式,表示子视图的大小最好不要超过 SpecSize 指定的大小。和 EXACTLY 模式一样,开发人员可自由设置 View 的大小和规格,但是这样可能出现 View 显示不全,具体要结合实际开发过程中实现的功能而定。一般而言,wrap_content对应该模式。 |
UNSPECIFIED |
未指定模式,表示开发人员可设置 View 的任意大小和规格,一般用于系统,实际开发过程中应用场景不多,可以忽略。 |
2.2 LayoutParams
我们对 LayoutParams 一点也不陌生,但是关于 View 的测量过程,必须要了解一下 LayoutParams 至关重要的作用:
我们知道,一个子视图的宽高不仅取决于其自身的 LayoutParams 属性,也取决于父 view 的大小。
其中,父 View 的大小对应 MeasureSpec 的 int 值。所以,子 View 的 MeasureSpec 则由父 View 的 MeasureSpec 和其自身的 LayoutParams 共同决定。
那么,MeasureSpec 的 int 值第一次是如何创建的呢?其实,是在 ViewRootImpl 的 measureHierarchy 方法中,performMeasure 时创建的:
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
而此时的 DecorView 由于是顶层 View,所以其大小取决于整个应用程序窗口的大小和自身的 LayoutParams。
上面是顶层 View 的 MeasureSpec 创建过程,对于普通 View 而言,ViewGroup 默认测量子 View 的方法 measureChild 中调用了 getChildMeasureSpec 方法,而 get 方法决定了子 View MeasureSpec int 值 的生成过程:
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);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
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 == ViewGroup.LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 == ViewGroup.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);
}
而最终通过 getChildMeasureSpec 方法生成的 int 值就是子 View 的 onMeasure 方法的两个入参:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
根据 getChildMeasureSpec 方法可得出 View 的 MeasureSpec 的创建规则:
![](https://img.haomeiwen.com/i1234624/ed89811b43f80925.png)
也就是说,该方法反映了父视图的 MeasureSpec 和子视图的 LayoutParams 共同产生子 View 中 onMeasure 方法的两个入参的值的过程。
从中可以看出,当子视图的 LayoutParams 是 wrap 时,Android 默认传递了 parentSize 给了子 View。结合 View 的 onMeasure 方法可知,此时 View 默认的尺寸被设置了父 View 的大小。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
所以,当开发人员实现一个继承于 View 的自定义 View 的时候,若直接使用默认的 getDefaultSize 方法,则该自定义 View 的 LayoutParams 是 wrap 时,总是充满了父布局。
2.3 View 测量的灵活性
由此,View 的测量完全可以依赖 Android 系统提供的默认测量规则进行,也可以按照自己的测量规则设置 View 的宽高。
总而言之,一个 View 的大小由 onMeasure 传递进来的两个 MeasureSpec 的 int 值决定的,我们可以根据这个 int 值自由设置 View 的大小,这是第一层的改变。
只不过传递进来的子 View 的 int 值,由父 View 在测量自身尺寸时遍历测量子 View 的过程中通过 getChildMeasureSpec 方法计算得出的。getChildMeasureSpec 方法是默认实现,也是上述“MeasureSpec 的创建规则”表格中的反映。当然,我们完全可以打破这个方法,完全不按照表格中的规则计算子 View 的 MeasureSpec,这是第二层改变。
再者,通常情况下建议使用 getChildMeasureSpec 的默认实现,但是传递进来的尺寸信息可考虑 padding、margin 的情况,灵活取用,这是第三层的改变。
由此可以看出,View 的整个测量过程相对 layout、draw 的流程来说比较复杂。
三、layout 过程
和测量过程一样,layout 方法决定自身的位置,而 onLayout 方法决定子 View 的位置,层层递进。
layout 的过程就是确定 View 的 mTop、mBottom、mLeft、mRight 的值的过程,也进一步为 draw 流程提供了基本的绘制依据。
四、draw 过程
layout 过程中测算出的位置信息为 draw 提供了绘制基础。draw 过程中,通过 onDraw 方法绘制自身,通过 dispatchDraw 方法绘制子 View。
在 dispatchDraw 方法中,ViewGroup 会遍历调用子 View 的 draw 方法:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
最后会走到 onDraw 方法,由开发人员自行实现 View 的绘制。
五、自定义 View
一个功能强大的自定义 View 除了要处理 View 的测量、布局、绘制流程,还要处理 View 的触摸事件、动画等。
自定义 View 原则上应该尽量使用 Android 系统提供的 API 辅助完成实现,不必过多地重复造轮子。同时应该注意:
让 View 支持 wrap_content
上文已经提及,View 的默认 wrap_content 是充满父布局的,所以如有必要,需自行实现 wrap_content 功能。
使用 Checkable、performClick
实现具有 Check 功能的自定义 View 时,应继承系统自带的 Checkable 接口,同时利用 performClick方法和 toggle 方法辅助完成点击、切换功能。必要时,需处理好 Drawable State。
处理 padding、margin
对于一个 View 来说,需要在 onMeasure()、onDraw() 中考虑到 padding 和 margin,ViewGroup 还要在 onLayout() 中考虑到这两个属性。
及时释放资源
在 onDetachedFromWindow 方法中要处理动画的结束、Handler 尚未处理的 Message、Runnable 等。
存储和恢复 View 状态
在有些需要实时恢复 View 状态的场景下,应定义 BaseSavedState 的子类,并在 onSaveInstanceState() 和 onRestoreInstanceState() 中分别处理好 View 状态的保存和恢复。
最后
漫漫开发之路,我们只是其中的一小部分……只有不断的学习、进阶,才是我们的出路!才跟得上时代的进步!
我做事不喜欢光说不练,既然指出了年薪50万的程序员要会这些知识,那我也把我的一些库存都分享给大家。
我从事Android开发十多年了,今年年初我花两个月的时间收录整理了一套知识体系,包括移动开发高级架构技术脑图、架构学习视频和面试专题文档,有想法深入的系统化的去学习的,可以加入Android开发交流群(820198451),我会把我收录整理的资料都送给大家,帮助大家更快的进阶。
上述的图片都来自于《移动开发高级架构技术脑图》想要这个可以另外说明清楚
Android架构师之路很漫长,一起共勉吧!
![](https://img.haomeiwen.com/i15844317/700160dec67709cd.png)
关注+加群:
Android进阶技术交流 (895077617 )免费获取Android进阶高级开发资料
网友评论