简介
我们知道,在 Android 中,View 绘制主要包含 3 大流程:
measure(测量):主要用于确定 View 的测量宽/高。
layout(布局):主要用于确定 View 在父容器中的放置位置。
draw(绘制):结合前面两步结果,将 View 真正绘制到屏幕上。
Android 中,主要有两种视图:View
和ViewGroup
,其中:
-
View
:就是一个独立的视图 -
ViewGroup
:一个容器组件,该容器可容纳多个子视图,即ViewGroup
可容纳多个View
或ViewGroup
,且支持嵌套。
虽然ViewGroup
继承于View
,但是在 View 绘制三大流程中,某些流程需要区分View
和ViewGroup
,它们之间的操作并不完全相同,比如:
-
View
和ViewGroup
都需要进行 measure,确定各自的测量宽/高。View
只需直接测量自身即可,而ViewGroup
通常都必须先测量所有子View,最后才能测量自己 - 通常
ViewGroup
先定位自己的位置(layout
),然后再定位其子View 位置(onLayout
) -
View
需要进行 draw 过程,而ViewGroup
通常不需要(当然也可以进行绘制),因为ViewGroup
更多作为容器存在,起存储放置功能
measure 流程
对 View 进行测量,主要包含两个步骤:
- 求取 View 的测量规格
MeasureSpec
。 - 依据上一步求得的
MeasureSpec
,对 View 进行测量,求取得到 View 的最终测量宽/高。
MeasureSpec
对于第一个步骤,即求取 View 的MeasureSpec
,首先我们来看下MeasureSpec
的源码定义:
// frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* 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;
// 生成测量规格
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 获取测量模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 获取测量大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
MeasureSpec
是View
的一个公有静态内部类,它是一个 32 位的int
值,高 2 位表示 SpecMode(测量模式),低 30 位表示 SpecSize(测量尺寸/测量大小)。
MeasureSpec
将两个数据打包到一个int
值上,可以减少对象内存分配,并且其提供了相应的工具方法可以很方便地让我们从一个int
值中抽取出 View 的 SpecMode 和 SpecSize。
一个MeasureSpec
表达的是:该 View 在该种测量模式(SpecMode)下对应的测量尺寸(SpecSize)。其中,SpecMode 有三种类型:
UNSPECIFIED
:表示父容器对子View 未施加任何限制,子View 尺寸想多大就多大。EXACTLY
:如果子View 的模式为EXACTLY
,则表示子View 已设置了确切的测量尺寸,或者父容器已检测出子View 所需要的确切大小。
这种模式对应于LayoutParams.MATCH_PARENT
和子View 设置具体数值两种情况。AT_MOST
:表示自适应内容,在该种模式下,View 的最大尺寸不能超过父容器的 SpecSize,因此也称这种模式为 最大值模式。
这种模式对应于LayoutParams.WRAP_CONTENT
。
LayoutParams
对 View 进行测量,最关键的一步就是计算得到 View 的MeasureSpec
,子View 在创建时,可以指定不同的LayoutParams
(布局参数),LayoutParams
的源码主要内容如下所示:
// frameworks/base/core/java/android/view/ViewGroup.java
public static class LayoutParams {
...
/**
* Special value for the height or width requested by a View.
* MATCH_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. Introduced in API Level 8.
*/
public static final int MATCH_PARENT = -1;
/**
* Special value for the height or width requested by a View.
* WRAP_CONTENT means that the view wants to be just large enough to fit
* its own internal content, taking its own padding into account.
*/
public static final int WRAP_CONTENT = -2;
/**
* Information about how wide the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
public int width;
/**
* Information about how tall the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
public int height;
...
}
其中:
-
LayoutParams.MATCH_PARENT
:表示子View 的尺寸与父容器一样大(注:需要减去父容器padding
部分空间,让父容器padding
生效) -
LayoutParams.WRAP_CONTENT
:表示子View 的尺寸自适应其内容大小(注:需要包含子View 本身的padding
空间) -
width
/height
:表示 View 的设置宽/高,即layout_width
和layout_height
设置的值,其值有三种选择:LayoutParams.MATCH_PARENT
、LayoutParams.WRAP_CONTENT
和 具体数值。
LayoutParams
会受到父容器的MeasureSpec
的影响,测量过程会依据两者之间的相互约束最终生成子View 的MeasureSpec
,完成 View 的测量规格。
简而言之,View 的MeasureSpec
受自身的LayoutParams
和父容器的MeasureSpec
共同决定(DecorView
的MeasureSpec
是由自身的LayoutParams
和屏幕尺寸共同决定,参考后文)。也因此,如果要求取子View 的MeasureSpec
,那么首先就需要知道父容器的MeasureSpec
,层层逆推而上,即最终就是需要知道顶层View(即DecorView
)的MeasureSpec
,这样才能一层层传递下来,这整个过程需要结合Activity
的启动过程进行分析。
Activity 视图基本结构
我们知道,在 Android 中,Activity
是作为视图组件存在,主要就是在手机上显示视图界面,可以供用户操作,Activity
就是 Andorid 中与用户直接交互最多的系统组件。
Activity
的基本视图层次结构如下所示:
Activity
中,实际承载视图的组件是Window
(更具体来说为PhoneWindow
),顶层View 是DecorView
,它是一个FrameLayout
,DecorView
内部是一个LinearLayout
,该LinearLayout
由两部分组成(不同 Android 版本或主题稍有差异):TitleView
和ContentView
,其中,TitleView
就是标题栏,也就是我们常说的TitleBar
或ActionBar
,ContentView
就是内容栏,它也是一个FrameLayout
,主要用于承载我们的自定义根布局,即当我们调用setContentView(...)
时,其实就是把我们自定义的布局设置到该ContentView
中。
当Activity
启动完成后,最终就会渲染出上述层次结构的视图。
DecorView 测量规格
因此,如果我们要求取得到子View 的MeasureSpec
,那么第一步就是求取得到顶层View(即DecorView
)的MeasureSpec
。大致过程如下所示:
-
在
Activity
启动过程中,会调用到ActivityThread.handleResumeActivity(...)
,该方法就是 View 视图绘制的起始之处:// frameworks/base/core/java/android/app/ActivityThread.java final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { ... ActivityClientRecord r = performResumeActivity(token, clearHide); ... // 此处的 window 为与 Activity 绑定的 PhoneWindow,即 Activity.mWindow r.window = r.activity.getWindow(); // PhoneWindow 绑定的顶层视图:DecorView View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); // 获取与 Activity 绑定的 WindowManager,实际上是 PhoneWindow 的 WindowManager ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); ... // 添加 DecorView 到 PhoneWindow 上(相当于设置 Activity 根视图) wm.addView(decor, l); ... }
其中,
r.window.getDecorView()
实际调用的是PhoneWindow.getDecorView()
,其会返回顶层DecorView
(不存在时会自动实例化):// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java public class PhoneWindow extends Window implements MenuBuilder.Callback { // This is the top-level view of the window, containing the window decor. private DecorView mDecor; ... @Override public final View getDecorView() { if (mDecor == null) { installDecor(); } return mDecor; } private void installDecor() { if (mDecor == null) { mDecor = generateDecor(); ... } ... } protected DecorView generateDecor() { // 实例化 DecorView return new DecorView(getContext(), -1); } ... }
然后,
r.window.getAttributes()
实际调用的是Window.getAttributes()
:// frameworks/base/core/java/android/view/Window.java public abstract class Window { private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); ... public final WindowManager.LayoutParams getAttributes() { return mWindowAttributes; } } // frameworks/base/core/java/android/view/WindowManager.java public interface WindowManager extends ViewManager { ... public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable { public LayoutParams() { // DecorView 的布局参数为 MATCH_PARENT super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); ... } } }
这里可以看到,此处
r.window.getAttributes()
返回的是一个WindowManager.LayoutParams
实例,对应的最终宽/高布局参数为LayoutParams.MATCH_PARENT
,最后通过wm.addView(decor,l)
将DecorView
添加到WindowManager
上(最终其实是设置到ViewRootImpl
上),所以DecorView
的布局参数为MATCH_PARENT
。 -
View 的绘制流程真正开始的地方为
ViewRootImpl.performTraversals()
,在其中,有如下代码片段:// frameworks/base/core/java/android/view/ViewRootImpl.java private void performTraversals() { ... int desiredWindowWidth; int desiredWindowHeight; ... // Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); ... } private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { ... childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... }
此处的
desiredWindowWidth
和desiredWindowHeight
是屏幕的尺寸,内部最终会调用到ViewRootImpl.getRootMeasureSpec(...)
,其源码如下所示:// frameworks/base/core/java/android/view/ViewRootImpl.java 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; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
ViewRootImpl.getRootMeasureSpec(...)
见名知意,其实就是用来获取顶层View(即DecorView
)的MeasureSpec
,其逻辑如下:- 当
DecorView
的LayoutParams
为MATCH_PARENT
时,说明DecorView
的大小与屏幕一样大,而又由于屏幕大小是确定的,因此,其 SpecMode 为EXACTLY
,SpecSize 为windowSize
,; - 当
DecorView
的LayoutParams
为WRAP_CONTENT
时,说明DecorView
自适应内容大小,因此它的大小不确定,但是最大不能超过屏幕大小,故其 SpecMode 为AT_MOST
,SpecSize 为windowSize
; - 其余情况为
DecorView
设置了具体数值大小或UNSPECIFIED
,故以DecorView
为主,其 SpecMode 为EXACTLY
,SpecSize 就是自己设置的值,即rootDimension
;
结合我们上面的分析,由于
DecorView
的LayoutParams
为MATCH_PARENT
,因此,DecorView
的MeasureSpec
最终为:MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY)
,即DecorView
的 SpecMode 为EXACTLY
,SpecSize 为屏幕大小。 - 当
默认测量(measure)
经过上述步骤求取得到 View 的MeasureSpec
后,接下来就可以真正对 View 进行测量,求取 View 的最终测量宽/高:
Android 内部对视图进行测量的过程是由View#measure(int, int)
方法负责的,但是对于View
和ViewGroup
,其具体测量过程有所差异。
因此,对于测量过程,我们分别对View
和ViewGroup
进行分析:
-
View
测量:View
的测量过程由View.measure(...)
方法负责,其源码如下所示:// frameworks/base/core/java/android/view/View.java public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
View#measure(int, int)
中参数widthMeasureSpec
和heightMeasureSpec
是由父容器传递进来的,具体的测量过程请参考后文内容。需要注意的是,
View#measure(int, int)
是一个final
方法,因此其不可被覆写,实际真正测量 View 自身使用的是View#onMeasure(int, int)
方法,如下所示:// frameworks/base/core/java/android/view/View.java protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure(...)
主要做了三件事:-
首先通过
getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
方法获取得到 View 的推荐最小测量宽/高:// frameworks/base/core/java/android/view/View.java protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
这两个方法的实现原理是一致的,这里就只分析
getSuggestedMinimumWidth()
方法实现,该方法内部是一个三目运算符,可以很清晰看出,当 View 没有设置背景时,它的宽度就为mMinWidth
,mMinWidth
就是android:minWidth
这个属性对应设置的值(未设置android:minWidth
时,其值默认为0
),当 View 设置了背景时,它的宽度就是mMinWidth
和mBackground.getMinimumWidth()
之中的较大值,其中,mBackground.getMinimumWidth()
源码如下:// frameworks/base/graphics/java/android/graphics/drawable/Drawable.java /* * @return The minimum width suggested by this Drawable. If this Drawable * doesn't have a suggested minimum width, 0 is returned. */ public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; } // 不同子类可实现具体大小 public int getIntrinsicWidth() { return -1; }
Drawable.getMinimumWidth()
就是返回 Drawable 的原始宽度,如果该 Drawable 未设置宽度,则返回0
。综上,
getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
其实就是用于获取 View 的最小测量宽/高,其具体逻辑为:当 View 没有设置背景时,其最小宽/高为android:minWidth
/android:mMinHeight
所指定的值,当 View 设置了背景时,其最小测量宽/高为android:minWidth
/android:minHeight
与其背景图片宽/高的较大值。简而言之,View 的最小测量宽/高为
android:minWidth
/android:minHeight
和其背景宽/高之间的较大值。 -
通过
getDefaultSize(...)
获取到 View 的默认测量宽/高,具体获取过程如下所示:// frameworks/base/core/java/android/view/View.java 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; }
此处的
size
是通过getSuggestedMinimumWidth()
/getSuggestedMinimumHeight()
方法获取得到系统建议 View 的最小测量宽/高。参数
measureSpec
是经由View.measure(...)
->View.onMeasure(...)
->View.getDefaultSize(...)
调用链传递进来的,表示的是当前 View 的MeasureSpec
。getDefaultSize(...)
内部首先会获取 View 的测量模式和测量大小,然后当 View 的测量模式为UNSPECIFIED
时,也即未限制 View 的大小,因此此时 View 的大小就是其原生大小(也即android:minWidth
或背景图片大小),当 View 的测量模式为AT_MOST
或EXACTLY
时,此时不对这两种模式进行区分,一律将 View 的大小设置为测量大小(即 SpecSize)。
注:实际上,这里可以看到,默认情况下,View 不区分AT_MOST
和EXACTLY
,也即,当自定义 View 时,LayoutParams.WRAP_CONTENT
和LayoutParams.MATCH_PARENT
效果是一样的,均为MATCH_PARENT
的效果,原因是 子View 的MeasureSpec
是由父容器传递进来的,父容器是通过ViewGroup#getChildMeasureSpec(...)
方法获取得到 子View 的MeasureSpec
,在该方法内部,子View 的测量模式无论是AT_MOST
或是EXACTLY
,其测量大小都为父容器大小(确定的说,是父容器剩余空间大小),因此其效果就等同于MATCH_PARENT
,具体源码详情分析请参考后文。总之,一般自定义 View 时,都需要覆写
onMeasure(...)
,并为其LayoutParams.WRAP_CONTENT
设置一个默认大小,如下所示:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 先进行默认测量 super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 默认大小依据自己灵活配置,这里为 400px int defaultSize = 400; // 获取默认测量宽/高 int width = this.getMeasuredWidth(); int height = this.getMeasuredHeight(); // 获取 View 的布局参数 ViewGroup.LayoutParams lp = this.getLayoutParams(); // 宽度为自适应,则设置一个默认大小 if(lp.width == ViewGroup.LayoutParams.WARP_CONTENT) { width = defaultSize; } // 高度为自适应,则设置一个默认大小 if(lp.height == ViewGroup.LayoutParams.WARP_CONTENT) { height = defaultSize; } this.setMeasuredDimension(width, height); }
-
获取到 View 的测量宽/高后,通过
setMeasuredDimension(...)
记录 View 的测量宽/高:// frameworks/base/core/java/android/view/View.java protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { ... setMeasuredDimensionRaw(measuredWidth, measuredHeight); } // 记录测量宽/高 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
setMeasuredDimension(...)
其实就是将 View 的最终测量宽/高设置到View.mMeasuredWidth
/View.mMeasuredHeight
属性中,完成测量过程。
-
-
ViewGroup
测量:ViewGroup
是一个抽象类,其继承于View
:public abstract class ViewGroup extends View implements ViewParent, ViewManager {...}
ViewGroup
的测量过程也是由View.measure(...)
负责,因此实际负责测量的是ViewGroup.onMeasure(...)
方法,但是由于ViewGroup
的作用是用于容纳子View,如果想测量ViewGroup
,则必须先测量其子View,而又由于不同的ViewGroup
有不同的布局特性,因此无法抽象出一套标准的测量流程,所以ViewGroup
本身没有覆写onMeasure(...)
方法(交由具体自定义ViewGroup
覆写),但是它提供了一些测量子View 的辅助方法,比如:measureChildren(...)
、measureChildrenWithMargins(...)
、measureChild(...)
、getChildMeasureSpec(...)
等等,自定义ViewGroup
可借助这些辅助方法,在onMeasure(...)
中完成子View 的测量,然后最终才能完成自己的测量。我们随便选择一个辅助方法,比如
ViewGroup#measureChildWithMargins(...)
,查看其源码:// android/view/ViewGroup.java protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // 获取 子View 的 LayoutParams final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 获取 子View 的 MeasureSpec // 父容器已使用的空间为:自身已使用空间 + 自身的 padding + 子View的 margin 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 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
代码非常简洁易懂,其核心就是先获取得到 子View 的
MeasureSpec
(getChildMeasureSpec(...)
),然后就可以对 子View 进行测量(child.measure(...)
)。View#measure(...)
的测量详情上述我们已经介绍过了,这里我们主要来看下ViewGroup#getChildMeasureSpec(...)
获取 子View 测量规格的具体过程:// android/view/ViewGroup.java /** * * @param spec 父容器的 MeasureSpec * @param padding 父容器已使用的空间(比如:父View自身的 padding + 子View的 margin) * @param childDimension 子View的 LayoutParams * @return 子View 的 MeasureSpec */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 当前View(即父容器)的测量模式 int specMode = MeasureSpec.getMode(spec); // 父容器的测量大小 int specSize = MeasureSpec.getSize(spec); // 父容器剩余可用空间 int size = Math.max(0, specSize - padding); // 子View 最终测量大小 int resultSize = 0; // 子View 最终测量模式 int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us // 父容器大小已确定 case MeasureSpec.EXACTLY: if (childDimension >= 0) { // 子View 设置了具体大小(精确数值) resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子View 大小撑满父容器 // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子View 自适应内容大小 // 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; } // 子View 的最终测量规格 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
getChildMeasureSpec(...)
其实就是ViewGroup
对其内部 子View 的默认测量过程,其核心逻辑为:-
如果父容器的测量模式为
EXACTLY
:即父容器测量大小是确切的,且其剩余空间精确为size
,此时:- 如果 子View 的
LayoutParams
为具体数值:则表示 子View 已明确设置了具体大小,因此,此时 子View 的测量大小即为自己设置的值,即childDimension
,测量模式为EXACTLY
。 - 如果 子View 的
LayoutParams
为MATCH_PARENT
:表示 子View 的大小撑满父容器,由于父容器是EXACTLY
,即大小已知,因此,子View 也是大小已知,故其测量模式为EXACTLY
,且其测量大小就是父容器剩余空间大小,具体为size
。 - 如果 子View 的
LayoutParams
为WRAP_CONTENT
:表示 子View 自适应内容大小,但是其尺寸最大不能超过父容器剩余空间,因此其测量模式为AT_MOST
,测量大小为父容器剩余空间size
。
- 如果 子View 的
-
如果父容器的测量模式为
AT_MOST
:即父容器自适应其内容大小,也即父容器大小不确定,此时:- 如果 子View 的
LayoutParams
为具体数值:则表示 子View 已明确设置了具体大小,因此,此时 子View 的测量大小即为自己设置的值,即childDimension
,测量模式为EXACTLY
。 - 如果 子View 的
LayoutParams
为MATCH_PARENT
:表示 子View 的大小撑满父容器,由于父容器是AT_MOST
,即大小未知,因此,子View 也是大小未知,即其测量模式为AT_MOST
,且其测量大小不超过父容器剩余空间大小size
。 - 如果 子View 的
LayoutParams
为WRAP_CONTENT
:表示 子View 自适应内容大小,但是其尺寸最大不能超过父容器剩余空间,因此其测量模式为AT_MOST
,测量大小为父容器剩余空间size
。
- 如果 子View 的
-
如果父容器的测量模式为
UNSPECIFIED
:即父容器大小无限制,此时:- 如果 子View 的
LayoutParams
为具体数值:则表示 子View 已明确设置了具体大小,因此,此时 子View 的测量大小即为自己设置的值,即childDimension
,测量模式为EXACTLY
。 - 如果 子View 的
LayoutParams
为MATCH_PARENT
:表示 子View 的大小撑满父容器,由于父容器大小无限制,因此,子View 的大小也是无限制的,所以,子View 的测量模式为UNSPECIFIED
,测量大小未知,通常设置为0
,表示无限。 - 如果 子View 的
LayoutParams
为WRAP_CONTENT
:表示 子View 自适应内容大小,由于父容器大小无限制,因此,子View 的测量大小也是无限制的,所以其模式为UNSPECIFIED
,测量大小无限,通常使用0
进行表示。
- 如果 子View 的
上述的逻辑总结如下图所示:(注:图片来源于互联网,侵删)
注:前面我们一直强调:子View 的
MeasureSpec
是由其LayoutParams
和父容器的MeasureSpec
共同约束构造而成,其实这部分逻辑就是ViewGroup#getChildMeasureSpec(...)
方法负责的,可以很清晰看到,子View 的MeasureSpec
就是在父容器MeasureSpec
约束下,与其自身LayoutParams
共同协商决定的。 -
综上,无论是对View
的测量还是ViewGroup
的测量,都是由View#measure(int widthMeasureSpec, int heightMeasureSpec)
方法负责,然后真正执行 View 测量的是 View 的onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法。
具体来说,View
直接在onMeasure(...)
中测量并设置自己的最终测量宽/高。在默认测量情况下,View
的测量宽/高由其父容器的MeasureSpec
和自身的LayoutParams
共同决定,当View
自身的测量模式为LayoutParams.UNSPECIFIED
时,其测量宽/高为android:minWidth
/android:minHeight
和其背景宽/高之间的较大值,其余情况皆为自身MeasureSpec
指定的测量尺寸。
而对于ViewGroup
来说,由于布局特性的丰富性,只能自己手动覆写onMeasure(...)
方法,实现自定义测量过程,但是总的思想都是先测量 子View 大小,最终才能确定自己的测量大小。
layout 流程
当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。
View layout
View 的布局过程由View#layout(...)
负责,其源码如下:
// android/view/View.java
/**
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
...
setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
...
}
View#layout(...)
主要就做了两件事:
-
setFrame(...)
:首先通过View#setFrame(...)
来确定自己的布局位置,其源码如下:// android/view/View.java protected boolean setFrame(int left, int top, int right, int bottom) { ... // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; }
setFrame(...)
其实就是更新记录 View 的四个顶点位置,这样 View 在父容器中的坐标位置就确定了。 -
onLayout(...)
:setFrame(...)
是用于确定 View 自身的布局位置,而onLayout(...)
主要用于确定 子View 的布局位置:protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
由于 View 不包含子组件,因此其
onLayout
是一个空实现。
ViewGroup layout
ViewGroup 的布局流程由ViewGroup#layout(...)
负责,其源码如下:
// android/view/ViewGroup.java
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
@Override
public final void layout(int l, int t, int r, int b) {
...
super.layout(l, t, r, b);
...
}
可以看到,ViewGroup#layout(...)
最终也是通过View#layout(...)
完成自身的布局过程,一个注意的点是,ViewGroup#layout(...)
是一个final
方法,因此子类无法覆写该方法,主要是ViewGroup#layout(...)
方法内部对子视图动画效果进行了相关设置。
由于ViewGroup#layout(...)
内部最终调用的还是View#layout(...)
,因此,ViewGroup#onLayout(...)
就会得到回调,用于处理 子View 的布局放置,其源码如下:
// android/view/ViewGroup.java
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
由于不同的ViewGroup
,其布局特性不同,因此ViewGroup#onLayout(...)
是一个抽象方法,交由ViewGroup
子类依据自己的布局特性,摆放其 子View 的位置。
draw 流程
当 View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。
View 的绘制过程由View#draw(...)
方法负责,其源码如下:
// android/view/View.java
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
...
if (drawTop) {
...
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
...
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
...
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
...
canvas.drawRect(right - length, top, right, bottom, p);
}
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
其实注释已经写的很清楚了,View#draw(...)
主要做了以下 6 件事:
绘制背景:
drawBackground(...)
如果有必要的话,保存画布图层:
Canvas.saveLayer(...)
-
绘制自己:
onDraw(...)
,其源码如下:// android/view/View.java protected void onDraw(Canvas canvas) { }
View#onDraw(...)
是一个空方法,因为每个 View 的绘制都是不同的,自定义 View 时,通常会覆写该方法,手动绘制该 View 内容。 -
绘制子View:
dispatchDraw(...)
,其源码如下:// android/view/View.java protected void dispatchDraw(Canvas canvas) { }
由于 View 没有子元素,因此其
dispatchDraw
是一个空实现。查看下
ViewGroup#dispatchDraw(...)
,其源码如下:// android/view/ViewGroup.java @Override protected void dispatchDraw(Canvas canvas) { ... final int childrenCount = mChildrenCount; final View[] children = mChildren; ... for (int i = 0; i < childrenCount; i++) { ... more |= drawChild(canvas, child, drawingTime); ... } ... } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
可以看到,其内部主要就是遍历子View,最后通过
child.draw(...)
让子View自己进行绘制。 如果有必要的话,绘制淡化效果并恢复图层:
Canvas.drawRect(...)
-
绘制装饰:
onDrawForeground(...)
,其源码如下:// android/view/View.java public void onDrawForeground(Canvas canvas) { onDrawScrollIndicators(canvas); onDrawScrollBars(canvas); final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; ... foreground.draw(canvas); } }
其实主要就是绘制滚动条,前景图片等视图相关的装饰。
绘制起始流程
我们知道,在Activity
启动过程中,会调用到ActivityThread.handleResumeActivity(...)
,该方法就是 View 视图绘制的起始之处:
// frameworks/base/core/java/android/app/ActivityThread.java
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
// 回调 Activity.onResume() 方法
ActivityClientRecord r = performResumeActivity(token, clearHide);
...
// 获取当前 Activity 实例
final Activity a = r.activity;
...
// 此处的 window 为与 Activity 绑定的 PhoneWindow,即 Activity.mWindow
r.window = r.activity.getWindow();
// PhoneWindow 绑定的顶层视图:DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
// 获取与 Activity 绑定的 WindowManager,实际上是 PhoneWindow 的 WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
// 添加 DecorView 到 PhoneWindow 上(相当于设置 Activity 根视图)
wm.addView(decor, l);
...
}
可以看到,ActivityThread.handleResumeActivity(...)
主要就是获取到当前Activity
绑定的ViewManager
,最后调用ViewManager.addView(...)
方法将DecorView
设置到PhoneWindow
上,也即设置到当前Activity
上。ViewManager
是一个接口,WindowManager
继承ViewManager
,而WindowManagerImpl
实现了接口WindowManager
,此处的ViewManager.addView(...)
实际上调用的是WindowManagerImpl.addView(...)
,源码如下所示:
// frameworks/base/core/java/android/view/WindowManagerImpl.java
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
...
}
WindowManagerImpl.addView(...)
内部转发到WindowManagerGlobal.addView(...)
:
// frameworks/base/core/java/android/view/WindowManagerGlobal.java
public final class WindowManagerGlobal {
...
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
...
// 实例化一个 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
...
// 将 ViewRootImpl 与 DecorView 关联到一起
root.setView(view, wparams, panelParentView);
...
}
...
}
在WindowManagerGlobal.addView(...)
内部,会创建一个ViewRootImpl
实例,然后调用ViewRootImpl.setView(...)
将ViewRootImpl
与DecorView
关联到一起:
// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
...
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// 将 DecorView 绑定到 ViewRootImpl.mView 属性上
mView = view;
...
mWindowAttributes.copyFrom(attrs);
...
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
...
}
...
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查是否处于主线程
checkThread();
...
scheduleTraversals();
}
}
...
}
ViewRootImpl.setView(...)
内部首先关联了传递过来的DecorView
(通过属性mView
指向DecorView
即可建立关联),然后最终调用requestLayout()
,而requestLayout()
内部又会调用方法scheduleTraversals()
:
// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
...
Choreographer mChoreographer;
...
final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 开始执行绘制
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
...
void scheduleTraversals() {
if (!mTraversalScheduled) { // 同一帧内不会多次调用遍历
mTraversalScheduled = true;
// 发送一个同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 将 UI 绘制任务发送到 Choreographer,回调触发 mTraversalRunnable,执行绘制操作
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
...
void doTraversal() {
...
performTraversals();
...
}
...
}
ViewRootImpl.scheduleTraversals()
内部主要做了两件事:
- 调用
MessageQueue.postSyncBarrier()
方法发送一个同步屏障,同步屏障可以拦截Looper
对同步消息的获取与分发,即加入同步屏障后,此时Looper
只会获取和处理异步消息,如果没有异步消息,则进入阻塞状态。 - 通过
Choreographer.postCallback(...)
发送一个Choreographer.CALLBACK_TRAVERSAL
的异步视图渲染消息。因为前面已经发送了一个同步屏障,因此此处的视图绘制渲染消息会优先被处理。
Choreographer.postCallback(...)
会申请一次 VSYNC 中断信号,当 VSYNC 信号到达时,便会回调Choreographer.doFrame(...)
方法,内部会触发已经添加的回调任务,Choreographer
的回调任务有以下四种类型:
// 回调 INPUT 任务
doCallbacks(Choreographer.CALLBACK_INPUT, mframeTimeNanos);
// 回调 ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 回调 View 绘制任务 TRAVERSAL
doCallbacks(Choreographer,CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 新增,COMMIT
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
因此,ViewRootImpl.scheduleTraversals(...)
内部通过mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)
发送的异步视图渲染消息就会得到回调,即回调mTraversalRunnable.run()
方法,最终会执行doTraversal()
方法,而doTraversal()
内部又会调用performTraversals()
方法,该方法才是真正开始执行 View 绘制流程的地方,其源码如下所示:
// frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
...
private void performTraversals() {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
...
}
综上,performTraversals()
会依次调用performMeasure(...)
、performLayout(...)
和performDraw()
三个方法,这三个方法会依次完成顶层View(即DecorView
)的测量(measure
)、布局(layout
)和绘制(draw
)流程,具体详情请参考后文。
到此,我们才真正进入 View 绘制流程,总结一下上述流程,如下图所示:
performMeasure
书接前文,我们知道,真正开始 View 绘制流程是ViewRootImpl.performTraversals()
,该方法内部首先进行的是performMeasure(...)
流程:
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
// 调用 DecorView.measure(...)
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
此处的mView
其实就是DecorView
,其赋值指向在ViewRootImpl.setView(...)
中进行,可以看到,performMeasure(...)
实际调用的是DecorView.measure(...)
,所以最终会回调DecorView#onMeasure(...)
方法,其源码如下:
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
...
}
可以看到,DecorView#onMeasure(...)
内部将测量过程交由其父类,即FrameLayout
进行处理,那我们看下FrameLayout#onMeasure(...)
源码:
// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取 子View 数量
int count = getChildCount();
...
// 最大高度
int maxHeight = 0;
// 最大宽度
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
// 获取 子View
final View child = getChildAt(i);
// 只对可见的 子View 进行测量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 测量子View
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 获取 子View 的布局参数
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取当前子View的宽度,包含其外边距,记录子View的最大宽度
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 记录子View的最大高度
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
...
}
}
// Account for padding too
// 最大宽度包含前景偏移量:padding
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
// 最大高度包含前景偏移量:padding
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
// 比较子View 和 系统建议的 子View 最小高度,获取两者中的较大值
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
// 比较子View 和 系统建议的 子View 最小宽度,获取两者中的较大值
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
// 子View 高度和 前景图片高度比较,记录其中较大值
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
// 子View 高度和 前景图片宽度比较,记录其中较大值
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
// 记录测量结果
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
...
}
FrameLayout
的布局特性为:所有 子View 层叠在一起,所以FrameLayout
的测量宽/高就是其所有 子View 中最大的宽和高,因此FrameLayout#onMeasure(...)
的核心逻辑就是遍历其所有子View,然后通过measureChildWithMargins(...)
(该方法前面内容已详细介绍)测量子View,然后就可以获取 子View 的宽/高,记录其中最大的宽/高值,作为自己的测量宽/高。
经过以上步骤,DecorView
的测量就已经完成了。
综上,ViewRootImpl#performMeasure(...)
其实就是对DecorView
的测量过程(DecorView#measure(...)
),DecorView
是一个FrameLayout
,其测量过程主要由FrameLayout#onMeasure(...)
负责,内部主要测量逻辑是先遍历所有子View,让 子View 先自己进行测量(child.measure(...)
),然后就可以获取 子View 的测量大小,记录所有 子View 中占比最大的测量宽/高,作为自己的最终测量大小。
performLayout
ViewRootImpl#performMeasure(...)
完成对DecorView
的测量后,接下来执行的是ViewRootImpl#performLayout(...)
,其源码如下:
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
// cache mView since it is used so much below...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
其中,参数lp
的width
和height
均为MATCH_PARENT
,desiredWindowWidth
和desiredWindowHeight
为屏幕宽/高,mView
为DecorView
。
所以,performLayout(...)
内部其实就是调用DecorView#layout(...)
,前面 layout 流程中介绍过,ViewGroup#layout(...)
内部最终会通过View#layout(...)
进行布局,而View#layout(...)
内部最终通过View#setFrame(...)
方法记录四个顶点位置,这样DecorView
自己的布局位置就已确定了,即host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())
。
确定了DecorView
自身的布局位置后,接下来就是要布局其 子View 了,因此,这里最终回调的是DecorView#onLayout(...)
方法,其源码如下所示:
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
...
}
...
}
DecorView#onLayout(...)
内部转交给FrameLayout#onLayout(...)
进行 子View 布局操作,其源码如下:
// frameworks/base/core/java/android/widget/FrameLayout.java
public class FrameLayout extends ViewGroup {
...
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 布局子View
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
// 获取 子View 数量
final int count = getChildCount();
// 左边可放置起始点坐标
final int parentLeft = getPaddingLeftWithForeground();
// 右边可放置终点坐标
final int parentRight = right - left - getPaddingRightWithForeground();
// 顶部可放置起始点坐标
final int parentTop = getPaddingTopWithForeground();
// 底部可放置终点坐标
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
// 遍历 子View
for (int i = 0; i < count; i++) {
// 获取 子View
final View child = getChildAt(i);
// 不放置状态为 GONE 的子View
if (child.getVisibility() != GONE) {
// 获取 子View 布局参数
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取 子View 测量宽/高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
// 当前 子View 的布局左边界
int childLeft;
// 当前 子View 的布局右边界
int childTop;
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
...
}
FrameLayout#onLayout(...)
内部是通过FrameLayout#layoutChildren(...)
进行 子View 的布局操作,其主要逻辑就是遍历所有 子View,计算得到 子View 的四个顶点位置坐标,最后将结果传递给child.layout(...)
,让 子View 记录自己在父容器中的布局位置,完成 子View 的布局过程。
综上,ViewRootImpl#performLayout(...)
就是对DecorView
的布局过程,此过程会递归计算各个 子View 的布局位置,调用 子View 的布局方法,完成各个 子View 的布局。
performDraw
完成了performMeasure(...)
和performLayout(...)
后,最后一步就是performDraw(...)
过程,其源码如下:
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}
private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
可以看到,ViewRootImpl#performDraw()
内部会经由ViewRootImpl#draw(...)
、ViewRootImpl#drawSoftware(...)
,最终执行的还是DecorView#draw(...)
过程,其源码如下:
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
...
}
由于FrameLayout
没有覆写draw(...)
方法,因此,super.draw(...)
最终调用的是View#draw(...)
方法,所以DecorView
默认采用的就是 View 的绘制方法,具体绘制详情上文已介绍过了,主要就是对DecorView
的背景、内容、子View、滚动条等装饰视图进行绘制。
至此,View 绘制的整个流程已基本介绍完毕。
总结
View 的绘制主要有以下一些核心内容:
-
三大流程:View 绘制主要包含如下三大流程:
- measure:测量流程,主要负责对 View 进行测量,其核心逻辑位于
View#measure(...)
,真正的测量处理由View#onMeasure(...)
负责。默认的测量规则为:如果 View 的布局参数为LayoutParams.WRAP_CONTENT
或LayoutParams.MATCH_PARENT
,那么其测量大小为 SpecSize;如果其布局参数为LayoutParams.UNSPECIFIED
,那么其测量大小为android:minWidth
/android:minHeight
和其背景之间的较大值。
自定义View 通常覆写
onMeasure(...)
方法,在其内一般会对WRAP_CONTENT
预设一个默认值,区分WARP_CONTENT
和MATCH_PARENT
效果,最终完成自己的测量宽/高。而ViewGroup
在onMeasure(...)
方法中,通常都是先测量子View,收集到相应数据后,才能最终测量自己。-
layout:布局流程,主要完成对 View 的位置放置,其核心逻辑位于
View#layout(...)
,该方法内部主要通过View#setFrame(...)
记录自己的四个顶点坐标(记录与对应成员变量中即可),完成自己的位置放置,最后会回调View#onLayout(...)
方法,在其内完成对 子View 的布局放置。注:不同于 measure 流程首先对 子View 进行测量,最后才测量自己,layout 流程首先是先定位自己的布局位置,然后才处理放置 子View 的布局位置。
-
draw:绘制流程,就是将 View 绘制到屏幕上,其核心逻辑位于
View#draw(...)
,主要就是对 背景、自身内容(onDraw(...)
)、子View(dispatchDraw(...)
)、装饰(滚动条、前景等) 进行绘制。注:通常自定义View 覆写
onDraw(...)
方法,完成自己的绘制即可,ViewGroup 一般充当容器使用,因此通常无需覆写onDraw(...)
。
- measure:测量流程,主要负责对 View 进行测量,其核心逻辑位于
Activity 的根视图(即
DecorView
)最终是绑定到ViewRootImpl
,具体是由ViewRootImpl#setView(...)
进行绑定关联的,后续 View 绘制的三大流程都是均有ViewRootImpl
负责执行的。对 View 的测量流程中,最关键的一步是求取 View 的
MeasureSpec
,View 的MeasureSpec
是在其父容器MeasureSpec
的约束下,结合自己的LayoutParams
共同测量得到的,具体的测量逻辑由ViewGroup#getChildMeasureSpec(...)
负责。
DecorView
的MeasureSpec
取决于自己的LayoutParams
和屏幕尺寸,具体的测量逻辑位于ViewRootImpl#getRootMeasureSpec(...)
。
最后,稍微总结一下 View 绘制的整个流程:
-
首先,当 Activity 启动时,会触发调用到
ActivityThread#handleResumeActivity(..)
,其内部会经历一系列过程,生成DecorView
和ViewRootImpl
等实例,最后通过ViewRootImpl#setView(decor,MATCH_PARENT)
设置 Activity 根View。注:
ViewRootImpl#setView(...)
内容通过将其成员属性ViewRootImpl#mView
指向DecorView
,完成两者之间的关联。 ViewRootImpl
成功关联DecorView
后,其内部会设置同步屏障并发送一个CALLBACK_TRAVERSAL
异步渲染消息,在下一次 VSYNC 信号到来时,CALLBACK_TRAVERSAL
就会得到响应,从而最终触发执行ViewRootImpl#performTraversals(...)
,真正开始执行 View 绘制流程。-
ViewRootImpl#performTraversals(...)
内部会依次调用ViewRootImpl#performMeasure(...)
、ViewRootImpl#performLayout(...)
和ViewRootImpl#performDraw(...)
三大绘制流程,其中:performMeasure(..)
:内部主要就是对DecorView
执行测量流程:DecorView#measure(...)
。DecorView
是一个FrameLayout
,其布局特性是层叠布局,所占的空间就是其 子View 占比最大的宽/高,因此其测量逻辑(onMeasure(...)
)是先对所有 子View 进行测量,具体是通过ViewGroup#measureChildWithMargins(...)
方法对 子View 进行测量,子View 测量完成后,记录最大的宽/高,设置为自己的测量大小(通过View#setMeasuredDimension(...)
),如此便完成了DecorView
的测量流程。performLayout(...)
:内部其实就是调用DecorView#layout(...)
,如此便完成了DecorView
的布局位置,最后会回调DecorView#onLayout(...)
,负责 子View 的布局放置,核心逻辑就是计算出各个 子View 的坐标位置,最后通过child.layout(...)
完成 子View 布局。performDraw()
:内部最终调用到的是DecorView#draw(...)
,该方法内部并未对绘制流程做任何修改,因此最终执行的是View#draw(...)
,所以主要就是依次完成对DecorView
的 背景、子View(dispatchDraw(...)
) 和 视图装饰(滚动条、前景等) 的绘制。
参考
- Android开发艺术探索
- Android自定义View篇之(一)View绘制流程