ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] View 是 Android 应用开发中相当重要的角色,重要程度丝毫不亚于四大组件,甚至远高于 Broadcast 和 ContentProvider。本文为学习《Android 开发艺术探索》中 View 的工作原理后,自己对于 View 的三大流程的源码分析。 # ViewRoot Activity在添加Window时,会先创建一个ViewrRootImpl,并通过ViewRootImpl来完成界面更新和Window的添加操作。View 的绘制流程从 ViewRootImpl 的 performTraversals 方法开始,在其内部调用View的measure、layout 和 draw 方法将一个 View 绘制出来。 这个过程如下图所示: ![](https://img.kancloud.cn/15/3d/153d56e6b3f961b1f4c22bf36c230dc8_773x573.jpg) 每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()。从父布局开始往子布局依次绘制。 # 测量measure View的measure方法在ViewGroup中被调用,ViewGroup会传递宽高两个方向的测量说明书MeasureSpec过来,View根据MeasureSpec设置自身的测量宽高。 ```java public final void measure(int widthMeasureSpec, int heightMeasureSpec) {} ``` ## LayoutParams的概念 ```java // View.java protected ViewGroup.LayoutParams mLayoutParams; ``` 每个View都有一个ViewGroup.LayoutParams属性,该属性是给父布局ViewGroup在测量、布局、绘制子View时使用的,告诉父容器自己想要被布局成什么样。 ViewGroup.LayoutParams是ViewGroup的一个静态内部类,描述了当前ViewGroup的子View可以配置哪些属性。ViewGroup的LayoutParams仅仅描述了View的宽、高,ViewGroup的子类可以在LayoutParams中添加一些其他的属性,比如LinearLayout的LayoutParams增加了weight属性和gravity属性。 在LinearLayout测量、布局以及绘制子View时,都会从子View的LayoutParams中取出相关属性的值进行使用。 ## MeasureSpec的概念 MeasureSpec 代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode 记录规格,SpecSize 记录大小。 SpecMode 有三种模式: * EXACTLY:父视图希望 View 的大小由 specSize 决定 * AT_MOST:View 最大只能是 specSize 中指定的大小 * UNSPECIFIED:View 的大小没有限制 每一个 View 都会有自己的 MeasureSpec,可以理解为测量说明书。每个View 的 MeasureSpec 都由父视图提供给它,所以 View 的测量大小会受父视图的影响。 View在拿到父视图传递过来的两个MeasureSpec后,怎么设定自己的宽高呢,一起来看看: ## View 的 measure 过程 View的measure方法会调用自身的onMeasure方法,在onMeasure方法中设置自身的测量宽高。想要改变View的测量宽高时,可以重写onMeasure方法,onMeasure方法源码如下: ```java /** * 两个参数为宽高两个方向的 MeasureSpec(测量说明书) * 由 measure 方法传递过来,View 的 measure 方法则是在 ViewGroup 的 measurechildren 方法中调用 */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置测量出来的宽/高 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } ``` 先调用getDefaultSize方法取到自己的测量宽高,再调用setMeasuredDimension方法进行设置,依次来看: 1、getDefaultSize 方法是关键的方法,用来从父布局传来的MeasureSpec中获取自己的测量宽/高,源码如下: ```java /** * @param size:View 的默认大小 * @param measureSpec:子 View 的 measureSpec(父视图给过来的) * @return 此 View 的测量宽/高 */ 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; } ``` 可以看到: * 测量模式 为 UNSPECIFIED 模式时,此 View 的测量值为默认值,默认值通过getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()方法获得 * 测量模式 为 AT_MOST 模式和 EXACTLY 模式时,此 View 的测量值为父视图所给 measureSpec 中的 specSize getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法类似,选择一个看源码: ```java protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } ``` 可以看出: * View 未指定背景时,View 的宽度为 mMinWidth,即 android:minWidth 属性所指定的值(默认为 0) * View 指定背景时,View 的宽度为 mMinWidth 和背景最小宽度 这两者中的较大值 2、开始设置测量宽高,setMeasuredDimension方法如下: ```java protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { //... setMeasuredDimensionRaw(measuredWidth, measuredHeight); } // 设置测量宽、测量高的值 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { //测量出来的宽高设置给 View 的两个成员变量:mMeasuredWidth 和 mMeasuredHeight mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; //... } ``` 通过上面代码看到,View从父布局传递来的MeasureSpec中,获取SpecMode和SpecSize来确定自身的大小。那么父布局又是如何给子View生成MeasureSpec的呢?来看看ViewGroup的测量过程。 ## ViewGroup 的 measure 过程 ViewGroup 继承自View类,为一个抽象类,因此没有重写 View 的 onMeasure 方法,都留给子类去实现。但是提供了测量子元素的方法 measureChildren。 ViewGroup 的子类可以重写 onMeasure 方法,调用 measureChildren 方法,来测量所有的子View: ViewGroup在onMeasure方法中设置自身的测量宽高,同时通过父布局给自己的MeasureSpec,来为自己的子View设置MeasureSpec。 ```java // 这两个参数为ViewGroup从它的父布局那里获取来的MeasureSpec, // 本来是用来设置ViewGroup自己的测量宽、高的,现在需要用到这两个参数来为子View生成MeasureSpec protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } ``` 其中measureChild方法如下: ```java protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { // 获取子View的LayoutParams final LayoutParams lp = child.getLayoutParams(); // 获取子元素的 MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 调用子元素View的 measure 方法 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } ``` 可以看到,先是得到子元素的 LayoutParams,然后根据 MeasureSpec、自身的padding以及子元素的 LayoutParams 来计算出子元素的 MeasureSpec。getChildMeasureSpec方法源码如下: ```java /** * @param spec ViewGroup的 MeasureSpec * @param padding ViewGroup的padding 值 * @param childDimension 子元素自身设定的宽、高值 */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 得到ViewGroup的测量模式和测量值 int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); // ViewGroup的剩余空间 int size = Math.max(0, specSize - padding); // 子元素的测量值和测量模式 int resultSize = 0; int resultMode = 0; switch (specMode) { // ViewGroup为精准模式 case MeasureSpec.EXACTLY: if (childDimension >= 0) { // 子元素设置了固定的宽高值时 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子元素想和ViewGroup一样大,就让它一样大 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子元素想自己确定它的大小,但是不能比ViewGroup更大 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // ViewGroup为最大值模式时 case MeasureSpec.AT_MOST: if (childDimension >= 0) { // 子元素想设置成一个具体的值,让它设 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子元素想和ViewGroup一样大,但是ViewGroup大小还不确定呢 // 要约束子元素不能比ViewGroup还大 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子元素想决定它自己的大小,可以,但是不能比ViewGroup还大 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // ViewGroup问我们我们想多大(ViewGroup为未指定模式时) case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 子元素想指定一个具体的值,让它去吧 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子元素想和ViewGroup一样大,来确定下 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子元素想自己确定它的大小,帮它确定下 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //返回子元素的 MeasureSpec return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } ``` 可以看到,首先拿到ViewGroup的 SpecMode 和 SpecSize,再根据ViewGroup的 SpecMode 的不同,给子 View 设置不同的 MeasureSpec。 当子 View 的宽高设置不同的值时,它的 MeasureSpec 也不同: * View 采用固定宽高时,不管ViewGroup的 MeasureSpec 是什么,View 的 MeasureSpec 都是 Exactly 模式并且大小等于其 LayoutParams 中的设置大小 * View 宽高是 match_parent 时:ViewGroup是精准模式,View 也是精准模式,大小是ViewGroup的剩余空间;ViewGroup是最大化模式时,View 也是最大化模式,且大小也是ViewGroup的剩余空间 * View 宽高是 wrap_content 时,不管ViewGroup是精准模式还是最大化模式,View 的模式总是最大化模式且大小均为ViewGroup的剩余空间 ## 问题拓展 1、在上面的分析中可以看到,当 View 的 SpecMode 为 AT_MOST 和 EXACTLY 时,测量值均为 specSize。因此就有一个问题: 当View配置wrap\_content时,View的SpecMode总是为AT_MOST,SpecSize总是为ViewGroup的剩余空间。View在根据MeasureSpec设置测量宽高时,就会设置成ViewGroup的剩余空间,与期望的wrap\_content不符 解决方案为自定义View时重写onMeasure方法,在View的SpecMode为AT_MOST时,为View指定一个宽高。ImageView、TextView等都是如此操作,可参考其源码。 以 TextView 的源码为例: ```java if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desired, heightSize); } ``` 2、另一个问题,根布局没有父视图,MeasureSpec由哪里来呢?一起来看看。 在 ViewRootImpl 的 performTraversals 方法中通过如下方式获取MeasureSpec: ```java // lp.width和lp.height在创建ViewGroup实例的时候已被赋值为MATCH_PARENT childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); ``` getRootMeasureSpec方法源码如下: ```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; } ``` 可以看到根布局的 MeasureSpec 由 windowSize 和 ViewGroup.LayoutParams 决定。由于第二个参数为MATCH_PARENT,所以根布局的SpecMode为EXACTLY模式,SpecSize为windowsSize,也就是根布局会充满全屏。 ## 整体总结【重点关注】 1、不管是View还是ViewGroup,每个人都会有一个父布局给过来的MeasureSpec,用来设置自己的宽高 * 测量模式为AT_MOST或EXACTLY时,测量宽高值为MeasureSpec中的specSize * 测量模式为UNSPECIFIED时,测量宽高值为默认值 2、View在onMeasure方法中,根据从父布局ViewGroup那里得到的MeasureSpec,来设置自身的宽高。 3、同样,ViewGroup也是在onMeasure方法中设置自己的宽高。 4、ViewGroup在onMeasure方法中设置自己宽高之前会多做一步,循环为所有子View设置MeasureSpec,并调用每个子View的measure方法,让子View测量自己的宽高。 5、ViewGroup根据自己的MeasureSpec、padding以及子View的LayoutParams,为子View设置MeasureSpec。 * 子View设置为固定宽高时,子View的specMode为:Exactly,specSize为:LayoutParams中设置的值,子View的测量宽高为LayoutParams中设置的值。 * 子View设置为match_parent时,子View的specMode为:等同ViewGroup的specMode,specSize为:ViewGroup的剩余空间,子View的测量宽高为ViewGroup的剩余空间。 * 子View设置为wrap_content时,子View的specMode为:AT_MOST,specSize为:ViewGroup的剩余空间,子View的测量宽高为ViewGroup的剩余空间。 6、自定义ViewGroup通常会重写onMeasure方法,在onMeasure方法中为所有子View生成MeasureSpec并测量子View宽高。 # layout ## View 的 layout 方法 View的layout方法会为View自己及所有的childView指定位置,View 的 layout 方法源码如下: ```java /** * l:View 左侧坐标 * t:View 上方坐标 * r:View 右侧坐标 * b:View 下方坐标 * 以上均指相对于父视图 */ public void layout(int l, int t, int r, int b) { // 设置当前View的位置 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // 设置每个childView的位置 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); //... } } ``` 两个关键方法,一个是setFrame方法设置当前View的位置,一个是onLayout方法设置childView的位置,分别来看看: 1、setFrame方法源码如下: ```java protected boolean setFrame(int left, int top, int right, int bottom) { //... if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; int newWidth = right - left; int newHeight = bottom - top; // 设置当前View的位置(相对于父布局的) mLeft = left; mTop = top; mRight = right; mBottom = bottom; // 当前View大小改变,我们可以重写onSizeChanged方法做相应处理 if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { //请求重新绘制View invalidate(sizeChanged); invalidateParentCaches(); } } return changed; } ``` 2、在ViewGroup代码中,onLayout方法是一个抽象方法,子类需要实现此方法,在实现中计算出每个childView的位置。 ```java protected abstract void onLayout(boolean changed, int l, int t, int r, int b); ``` 以LinearLayout为例: ```java // LinearLayout的onLayout方法 protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } } // 以纵向为例 void layoutVertical(int left, int top, int right, int bottom) { int childTop; int childLeft; int width = right - left; int childRight = width - mPaddingRight; int childSpace = width - paddingLeft - mPaddingRight; for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child.getVisibility() != GONE) { // 这里使用了childView的测量宽、高的值 final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); //...中间省略部分设置childLeft、childTop的代码 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); //... } } } // 调用childView的layout方法进行位置设定 private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); } ``` ## 整体总结【重点关注】 1、View的layout方法中,调用setFrame方法为View自己指定位置,调用onLayout方法为所有的childView指定位置 2、setFrame方法为mLeft、mTop、mRight、mBottom几个属性赋值,来确定View自己相对于父布局的位置 3、ViewGroup的onLayout方法是一个抽象方法,子类需要实现此方法,并在实现中根据ViewGroup的布局逻辑计算出每个childView的位置,然后调用childView的layout方法进行子View布局 # draw 1、View的draw方法 View的draw方法有两个实现,一个参数的draw方法是被三个参数的方法调用的,三个参数的方法是被父布局ViewGroup的drawChild方法调用的。 ```java boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {} public void draw(Canvas canvas) {} ``` ```java // 由ViewGroup的drawChild方法调用 boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { //... if (!drawingWithDrawingCache) { if (drawingWithRenderNode) { //... } else { //... draw(canvas); } } } ``` 一个参数的draw方法源码如下: ```java public void draw(Canvas canvas) { // 绘制背景 if (!dirtyOpaque) { drawBackground(canvas); } if (!verticalEdges && !horizontalEdges) { // 绘制View内容 if (!dirtyOpaque) onDraw(canvas); // 绘制childView dispatchDraw(canvas); //... // 绘制装饰(前景内容) onDrawForeground(canvas); // 绘制默认焦点高亮显示 drawDefaultFocusHighlight(canvas); return; } // 我们通过重写onDraw方法,拿到Canvas来绘制我们想要绘制的内容。 protected void onDraw(Canvas canvas) {} ``` View的绘制过程有如下几步: * 绘制背景 * 绘制View内容 * 绘制childView * 绘制前景 * 绘制默认焦点高亮显示 View中的dispatchDraw方法是一个空方法,ViewGroup中有相应的实现。 2、ViewGroup绘制childView ```java // 这个方法用于绘制childView protected void dispatchDraw(Canvas canvas) { for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { //... more |= drawChild(canvas, transientChild, drawingTime); } if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } ``` 在dispatchDraw方法中循环调用了drawChild方法,也就是调用了每个childView的draw方法,将childView绘制出来: ```java protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); } ``` ## 整体总结【重点关注】 1、View的draw方法按流程分别绘制背景、content、childView、前景、默认焦点等。其中onDraw方法绘制View的内容。 2、如果是ViewGroup,会调用dispatchDraw方法绘制childView。 3、ViewGroup的dispatchDraw方法,会调用所有的childView的draw方法进行绘制子View。 4、可通过重写View的onDraw方法,拿到Canvas来绘制想要绘制的内容。 # 参考文档 [Android 视图绘制流程完全解析,带你一步步深入了解 View 系列文章](http://blog.csdn.net/guolin_blog/article/details/16330267) [Android 开发艺术探索](https://book.douban.com/subject/26599538/)