参考书籍:《Android开发艺术探索》 任玉刚
如有错漏,请批评指出!
View的工作流程
前面说过,View的工作流程主要是指 measure、layout、draw 这三大流程,下面来一一进行分析。
measure过程
View和ViewGroup的 measure 过程是不同的,如果只是一个普通View,那么通过 measure 方法就能完成其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历所有子元素,并调用其 measure 方法,各个子元素再递归去执行这个流程。
-
View 的 measure 过程
View的 measure 过程由其 measure() 方法来完成,measure()方法是一个 final 类型的方法,也就是说子类不能重写这个方法,在 measure() 方法中会调用 onMeasure() 方法,因此我们只需要看 onMeasure() 方法的源码实现:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure() 方法通过调用 setMeasuredDimension() 方法来设置 View 的测量宽高(不等同于实际宽高),而这个测量宽高是通过 getDefaultSize() 方法来处理的,我们再来看 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; }
这个方法的逻辑很简单,我们需要注意的是,传入的 measureSpec 参数就是 View 宽 / 高的测量值,也就是说,当View的测量模式为 AT_MOST 或 EXACTLY 模式时,这个方法返回的就是View测量后的 SpecSize。
当View测量模式为 UNSPECIFIED 模式时,一般用于系统内部的测量过程,这种情况下,会返回第一个参数,也就是getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 方法的返回值。我们来看一下它们的源码:protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
这两个方法的实现原理是相同的,我们只看 getSuggestedMinimumWidth(),这个方法里面涉及到三个值:
mBackground —— 即当前View的背景
mMinWidth —— android:mMinWidth 这个属性指定的值
mBackground.getMinimumWidth() —— 当前View背景Drawable的原始宽度
这个逻辑很简单,我们直接来看这三个值,前两个很好理解,第三个是什么意思呢?来看一下 Drawable 的 getMinimumWidth() 方法的实现:public int getMinimumWidth() { // Drawable 的原始宽高 final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }
可以看到,当Drawable有原始宽高时,这个方法返回的是 Drawable 的原始宽高。至于Drawable什么时候有原始宽高(暂且略过,或者可以先看看《Android开发艺术探索》第六章),这里先举个例子,ShapeDrawable 无原始宽高,而BitmapDrawable有原始宽高(图片尺寸)。
从getDefaultSize() 方法的实现可以得出一个结论,当我们自定义View时,如果直接继承View,就需要重写 onMeasure() 方法并设置 wrap_content(即AT_MOST模式) 的默认大小,否则在布局中使用 wrap_content 属性的效果
和 match_parent 属性的效果是一样的。原因如下:当我们给View的宽高指定 wrap_content 属性时,它的 specMode
是 AT_MOST模式, 在这种模式下,它的宽高等于 specSize。在 Android学习笔记(五)| View的工作原理(上) 的结尾,我们总结了一张表,在这种模式下,View的 specSize不会超过 parentSize,而 parentSize 是父容器的 可用空间,但是View 类并没有对这个specSize作任何处理,所以 specSize 就是 parentSize ,也就是说,这种模式下View会填充满父容器的可用空间,即和 match_parent 的效果相同。这个问题很好解决。只需要重写 onMeasure() 方法,给 AT_MOST 模式指定一个默认宽高即可:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec); int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec); int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec); int heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec); // mWidth 即指定的默认宽度 if (widthMeasureMode == MeasureSpec.AT_MOST){ widthMeasureSize = mWidth; } // mHeight 即指定的默认高度 if (heightMeasureMode == MeasureSpec.AT_MOST){ heightMeasureSize = mHeight; } setMeasuredDimension(widthMeasureSize, heightMeasureSize); }
-
ViewGroup 的 measure 过程
ViewGroup 除了完成自己的 measure 过程,还会遍历所有的子元素并调用其 measure 方法,各个子元素再递归去执行这个过程。不过ViewGroup是一个抽象类,没有重写View的 onMeasure() 方法,但是它提供了一个 measureChildren() 的方法: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); } } }
这个方法会依次遍历所有子元素,若当前子元素的 visibility 不为 GONE 时,就会调用 measureChild() 方法:
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); }
这个方法的逻辑也很简单,就是取出当前子元素的 LayoutParams,通过 getChildMeasureSpec() 方法(在前面关于 MeasureSpec 的内容中分析过这个方法的源码)来创建子元素的 MeasureSpec,然后将MeasureSpec直接传递给子元素的 measure() 方法进行测量。
通过上面的分析,我们会发现 ViewGroup 并没有定义其具体测量过程,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等。之所以这么设计,是因为不同的ViewGroup子类有不同的布局特性,因此他们的测量细节存在差异,无法统一实现。接下类会从一个具体的ViewGroup(LinearLayout)的源码层面来分析其 measure 过程。
-
LinearLayout 的 measure 过程
首先来看 LinearLayout 的 onMeasure() 方法:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
这里就是单纯的判断 LinearLayout 的布局方式,我们选择 VERTICAL 方式来进行分析,下面是 measureVertical() 方法的源码主干部分(因为这个方法很长,我们只看大概逻辑):
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { ... (变量定义) // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); ... (判断View是否需要测量以及是否有分割线) final LayoutParams lp = (LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; // 是否使用了额外的空间 final boolean useExcessSpace = lp.height == 0 && lp.weight > 0; if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) { // Optimization: don't bother measuring children who are only // laid out using excess space. These views will get measured // later if we have space to distribute. final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { if (useExcessSpace) { // The heightMode is either UNSPECIFIED or AT_MOST, and // this child is only laid out using excess space. Measure // using WRAP_CONTENT so that we can find out the view's // optimal height. We'll restore the original height of 0 // after measurement. lp.height = LayoutParams.WRAP_CONTENT; } // Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). final int usedHeight = totalWeight == 0 ? mTotalLength : 0; measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); final int childHeight = child.getMeasuredHeight(); if (useExcessSpace) { // Restore the original height and record how much space // we've allocated to excess-only children so that we can // match the behavior of EXACTLY measurement. lp.height = 0; consumedExcessSpace += childHeight; } final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); if (useLargestChild) { largestChildHeight = Math.max(childHeight, largestChildHeight); } } ... // 子元素测量完成 } ... }
从这段代码可以看出,LinearLayout 内部会遍历子元素并对需要测量的子元素执行 measureChildBeforeLayout() 方法,这个方法里面调用了 measureChildWithMargins() 方法(前面介绍 MeasureSpec 时分析过),这样每个子元素就依次进入 measure 流程了。并且 所有子元素的测量高度都会被累计起来,用 mTotalLength 这个变量来存储,每测量一个子元素,mTotalLength 就会增加(包括子View的高度和竖直方向的 mgrgin)。当子元素测量完毕后,LinearLayout会测量自己的大小,这部分代码刚才省略了:
... // Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); // Reconcile our calculated size with the heightMeasureSpec int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); heightSize = heightSizeAndState & MEASURED_SIZE_MASK; ... setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); ...
这段代码的逻辑也很清晰,就是确定 LinearLayout 的高度,然后调用 setMeasuredDimension() 方法来设置其宽高。对于竖直方向的 LinearLayout 而言,它在水平方向的测量过程遵循 View 的测量过程,这里调用了 resolveSizeAndState() 方法来确定测量宽度,具体逻辑与View的测量过程相似,这里不作分析。在竖直方向则有所不同,如果它的布局中高度采用的是 match_parent 或具体数值,则测量过程和View一致,即高度为specSize;如果它的布局中高度采用 wrap_content,那么它的高度是所有子View所占用高度的和(最大为它的父容器剩余空间);当然,它的最终高度还要减掉竖直方向上的 padding 值。
View的 measure 过程是三大流程中最复杂的一个, measure 完成后,通过 getMeasuredWidth() / getMeasuredHeight()方法可以获取到测量宽高。不过在某些极端情况,系统可能需要多次测量才能确定最终的测量宽高,这个时候 onMeasure() 方法中拿到的测量宽高可能不准确。比较好的做法就是在 onLayout() 方法中去获取View的测量宽高或最终宽高。
layout 过程
参考博客:自定义View Layout过程
Layout 过程的作用是确定View的位置,对于ViewGroup而言,layout() 方法确定自身位置,然后在layout() 方法中会调用 onLayout() 方法,这个方法中会遍历所有子元素,并调用其layout方法,依次递归;而对于普通View,则只需要确定自身的位置。
-
先来看普通 View 的 layout() 方法:
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // 确定自身位置参数 // 并判断当前View大小和位置是否发生变化 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // 普通View 没有子View,因此onLayout() 方法是一个空实现 onLayout(changed, l, t, r, b); ··· } ... }
可以看到,这里其实就是确定自身的位置,不过涉及到两个方法 setOpticalFrame() 和 setFrame() ,这两个方法的区别在于是否有 视觉边界,直接看源码:
/** * setFrame() * 设置View自身位置(包含视觉边界) */ private boolean setOpticalFrame(int left, int top, int right, int bottom) { Insets parentInsets = mParent instanceof View ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets(); // 实际还是调用 setFrame() 方法 return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); } /** * setFrame() * 设置View自身位置(不考虑视觉边界) * 返回值用来标记View大小和位置是否改变 */ protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; ··· if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bit int drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); ··· } return changed; }
从上面的代码可以看到,setOpticalFrame() 方法实际上还是调用了 setFrame() 方法,setFrame() 方法实际就是根据传入的位置参数确定View实际的位置,如果View的位置大小发生变化,就会调用invalidate() 方法,从而让系统重绘这个View。
-
对于ViewGroup,因为它是继承自View类的,因此它的 layout() 方法与 View 相似,而 onLayout() 的具体实现和具体布局有关,因此View和ViewGroup均没有具体实现onLayout() 方法,所以我们需要结合一个具体的ViewGroup来分析其 onLayout() 过程。这里以LinearLayout 为例,来看 LinearLayout的 onLayout() 方法:
/** * onLayout() 方法分两种情况(就是 orientation 属性指定的 vertical 或 horizontal) * 分为水平布局和垂直布局(下面只看垂直布局) */ @Override 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); } } /** * onLayout() 在垂直布局情况下的实现 */ void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // Where right end of child should go final int width = right - left; int childRight = width - mPaddingRight; // Space available for child int childSpace = width - paddingLeft - mPaddingRight; // 子View数量 final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; // 根据指定的停靠位置(gravity属性)来决定 子View 在竖直方向上的布局起始位置 switch (majorGravity) { case Gravity.BOTTOM: // mTotalLength contains the padding already childTop = mPaddingTop + bottom - top - mTotalLength; break; // mTotalLength contains the padding already case Gravity.CENTER_VERTICAL: childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; break; case Gravity.TOP: default: childTop = mPaddingTop; break; } // 依次遍历子Viwe for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); int gravity = lp.gravity; if (gravity < 0) { gravity = minorGravity; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; // 确定子View的位置 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); // 将当前View的高度(包括divider、padding、margin)累加到chileTop中,下一个View从这个位置开始 i += getChildrenSkipCount(child, i); } } } /** * 将 layout 过程传递到子View */ private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
逻辑其实很简单,就是依次从上往下排布子View,并且将layout过程传递到子View。这样自上而下就完成了整个View树的 layout 过程。
draw 过程
参考博客:自定义View Draw过程
-
draw 也分 普通 View 的 draw过程 和 ViewGroup 的 draw 过程,只不过差别不是很大,先来看View类的 draw() 方法:
public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); ··· // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; } ··· }
根据代码逻辑可以看到,View 的 draw 过程主要分为4步:
- step1 drawBackground() —— 绘制背景;
- step3 onDraw() —— 绘制View自身;
- step4 dispatchDraw() —— 绘制子View
- step6、step7 —— 绘制装饰
(step2和step5 是涉及到优化的内容,这里先不管)
对于普通View而言,显然是没有子View需要绘制的,因此对于普通View,dispatchDraw()方法是一个空实现;对于ViewGroup,则必须实现dispatchDraw() 方法。
我们先来看 drawBackground()、onDraw()、onDrawForeground()这三个方法的实现:/** * 如果给View指定了背景,就会 调用了个方法来绘制背景 */ private void drawBackground(Canvas canvas) { final Drawable background = mBackground; if (background == null) { return; } // 根据 layout 过程获得的位置参数,来确定绘制背景的边界 setBackgroundBounds(); ··· final int scrollX = mScrollX; final int scrollY = mScrollY; if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { // 若 scrollX 和 scrollY有值,则对画布(canvas)的坐标进行偏移(有没有联想到scrollTo和ScrollBy方法) canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } /** * 自定义View时 必须 且 只需 实现这个方法,来绘制View */ protected void onDraw(Canvas canvas) { // View 类没有实现,需要根据View自身的情况重写这个方法 } /** * 绘制装饰,如滚动条、点击效果(最常见的如水波纹点击效果) */ public void onDrawForeground(Canvas canvas) { onDrawScrollIndicators(canvas); onDrawScrollBars(canvas); final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (foreground != null) { if (mForegroundInfo.mBoundsChanged) { mForegroundInfo.mBoundsChanged = false; final Rect selfBounds = mForegroundInfo.mSelfBounds; final Rect overlayBounds = mForegroundInfo.mOverlayBounds; if (mForegroundInfo.mInsidePadding) { selfBounds.set(0, 0, getWidth(), getHeight()); } else { selfBounds.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); } final int ld = getLayoutDirection(); Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(), foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld); foreground.setBounds(overlayBounds); } foreground.draw(canvas); } }
上面这三个方法在ViewGroup类中均没有重写,说明无论是普通View亦或是ViewGroup,都是一样实现这三个过程的,所以,我们只需要看ViewGroup类的 dispatchDraw() 方法:
@Override protected void dispatchDraw(Canvas canvas) { ··· final int childrenCount = mChildrenCount; ··· for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) { more |= drawChild(canvas, transientChild, drawingTime); } ··· } ··· } ··· }
这里只给出了遍历子View的部分,其他的内容在这里不需要关心。其实就是遍历每一个子View,然后调用 drawChild() 方法:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
这个方法就是调用 子View 的draw方法,将draw过程传递给子View,依次递归。
这里还有一个细节问题需要注意:View类有一个 setWillNotDraw() 方法
public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
这个方法涉及到一个标记位 WILL_NOT_DRAW,设置这个标记位为 true 以后,表示当前View不需要绘制任何内容,系统会进行相应优化。
- 默认情况下,View没有启用这个标记位(false),而ViewGroup启用了(true);
- 当我们自定义控件继承 ViewGroup 类时,如果需要通过 onDraw() 方法绘制内容,就需要显式关闭这个标记位。
上一篇:Android学习笔记(五)| View的工作原理(上)
下一篇:Android学习笔记(七)| Android动画(上)—— View动画