上篇文章我们已经讲解了 View 的 measure 过程,这篇我们来继续分下下边的内容 View 的 Layout 过程和 Draw 过程
1. Layout 过程
Layout 的作用是 ViewGrrou 用来确定子元素的位置,当 ViewGrrou 的位置被确定后,它在 onLayout 中会遍历所有子元素并调用其 layout 方法,在 layout 方法中,onLayout 方法又会被调用,Layout 过程和 measure 过程相比就简单多了,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,先看下 View 的 layout 方法,代码如下:
/** * Assign a size and position to a view and all of its * descendants * * <p>This is the second phase of the layout mechanism. * (The first is measuring). In this phase, each parent calls * layout on all of its children to position them. * This is typically done using the child measurements * that were stored in the measure pass().</p> * * <p>Derived classes should not override this method. * Derived classes with children should override * onLayout. In that method, they should * call layout on each of their children.</p> * * @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) { 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; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个定点的位置,即初始化 mLeft,mTop,mRight,mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了,接着会调用 onLayout 方法,这个方法用处是父容器确定子元素的位置,和 onMeasure 方法相似,onLayout 的具体实现同样和具体的布局有关,
@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); } }LinearLayout 中 onLayout 的实现逻辑和 onMeasure 的实现逻辑类似,这里选择 layoutVertical 继续理解,为了更好的理解逻辑,这里只给出主要的代码:
final int count = getVirtualChildCount(); 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; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } }
这里分析一下 layoutVertical 的代码逻辑,可以看到,此方法会遍历所有子元素调用 setChildFrame 方法来为子元素指定位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合 LinearLayout 的特性,至于 setChildFrame,它仅仅调用子元素的 layout 方法而已,这样父元素在 layout 方法中完成自己的定位以后,就通过 onLayout 方法去掉用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样一层一层的传递下去就完成了整个 View 树的 layout 过程,setChildFrame 对的代码如下:
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }实际上,setChildFrame 里边的 width 和 height 就是子元素的测量宽高,我们来看下 layoutVertical 方法中的一段代码,就会明白为什么这么说了:
final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); setChildFrame(child, childLeft + getLocationOffset(child), childTop, childWidth, childHeight);而在 layout 方法中会通过 setFrame 去设置 子元素的四个顶点,在 setFrame 中有如下几句赋值语句,这样一来子元素的位置就确定了:
mLeft = left; mTop = top; mRight = right; mBottom = bottom;上面我们提到一个问题,那就是大部分情况下,测量宽高就是最终宽高。那到底为什么这么说呢?这个问题可以具体为,View 的 getMeasureWidth 和 getWidth 这两个方法有什么区别,至于 getMeasureHeight 和 getHeight 的区别前两者完全一样。为了回答这个问题,首先我们来看下 getWidth 和 getHeight 的源码:
/** * Return the width of the your view. * * @return The width of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; } /** * Return the height of your view. * * @return The height of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getHeight() { return mBottom - mTop; }从 getWidth 和 getHeight 的源码再结合 mLeft,mTop,mRight,mBottom 这四个变量的赋值过程来看,getWidth 方法的返回值刚好就是 View 的测量宽度,而 getHeight 刚好就是 View 的测量高。经过上面的分析,我们可以回答这个问题了,在 View 的默认视线中,View 的测量宽高和最终宽高是相等的,只不过测量宽高形成于 View 的 measure 过程,而最终宽高形成于 View 的 layout 过程,即两者的赋值时机不同。因此,在日常开发中,我们可以认为 View 的测量宽高就等于 View 的最终宽高,但是的确存在特殊情况会导致两者不一致,如重写 layout 方法:
@Override public void layout(int l, int t, int r, int b) { super.layout(l, t, r + 100, b + 100); }
上述代码会导致在任何情况下 View 的测量宽高总是比最终宽高小 100dp,虽然这样写会导致 View 显示不正常并且没有实际的意义,但是这也证明了测量宽高不等于最终宽高,另外一种情况实在某些情况下,View 需要多次 measure 才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相等。
2. Draw 过程
Draw 过程就比较简单了,它的作用是将 View 绘制在屏幕上,View 的绘制过程遵循如下几步:
(1) 绘制背景 backgrouond.draw(canvas);
(2) 绘制自己(onDraw);
(3) 绘制 children(dispatchDraw)
(4) 绘制装饰(onDrawScrollBars)
这一点通过 draw 方法可以看出来,代码如下:
/** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is * called. When implementing a view, implement * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */ @CallSuper 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; /* * 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 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); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; }View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层的传递了下去,View 有一个特殊的方法 setWillNotDraw,先看下他的源码:
/** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }从 setWillNotDraw 方法的注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true以后,系统会进行相应的优化,默认情况下,View 没有用这个优化标记为,但是 ViewGroup 会启用这个标记位。这个标记为对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备会职功能时,就可以开启这个标记位从而便于系统进行后续的优化,当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们要显式的关闭 WILL_NOT_DRAW 这个标记位。
到此,View 的工作原理就介绍完了。