上篇文章我们已经讲解了 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 的工作原理就介绍完了。
本文详细介绍了Android中View的Layout过程与Draw过程。Layout过程由ViewGroup确定子元素的位置,Draw过程则是将View绘制在屏幕上。文章还讨论了View的测量宽高与最终宽高的区别,并解释了View绘制过程的传递机制。
1401

被折叠的 条评论
为什么被折叠?



