这里介绍 View 的 Layout 和 Draw 过程。
Layout 过程
Layout 过程的作用是 ViewGroup 来确定子元素的位置,来看 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;
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 listenersCopy =
(ArrayList) 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;
}
首先通过 setFrame 方法来设定 View 四个顶点位置,分别是 mLeft、mRight、mTop、mBottom 的值,四个顶点一旦确定,View 在父容器的位置也就确定了。接着调用 onLayout 方法,它用来确定子元素的位置。与 onMeasure 方法类似,View 和 ViewGroup 都没有实现 onLayout 方法,于是再次选择 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);
}
}
由于 layoutVertical 与 layoutHorizontal 方法实现类似,所以只看 layoutVertical 方法的主要代码
void layoutVertical(int left, int top, int right, int bottom) {
...
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();
}
...
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 会逐渐增大,这意味元素会从上到下排列。
看下 setChildFrame 方法
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
setChildFrame 中仅仅是调用子元素的 layout 方法。其中 width 和 height 是子元素的测量宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
总结一下,父元素在 layout 方法中完成定位后,通过 onLayout 方法调用子元素的 layout 方法,而子元素会通过 layout 方法来确定自己的位置,这样一层层的传递就完成了 View 的 Layout 过程。
在 View 工作原理(一)中有提到测量宽高大部分情况下等于最终宽高,之所以说大部分情况,是因为测量宽高的赋值早于最终宽高,如果重写 View 的 layout 方法
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 10, b + 10);
}
那么这种情况下,最终宽高不等于测量宽高。另一种情况是,当 View 需要多次 measure 来测量宽高,那么前几次的测量值可能就不会与最终宽高相等。
Draw 过程
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;
/*
* 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
...
}
于是得到主要步骤
- 绘制背景 drawBackground
- 绘制内容 onDraw
- 绘制子元素 dispatchDraw
- 绘制装饰 onDrawScrollBars
来看 drawBackground 方法
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
drawBackground 方法使用 scrollX 和 scrollY 记录偏移值,然后根据偏移值进行绘制。
由于 View 没有实现 onDraw 方法,所以先跳过。
在普通 View 中,因为它没有子元素,所以 dispatchDraw 方法为空。于是看 ViewGroup 的 dispatchDraw 方法
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可以看到,dispatchDraw 方法会遍历所有子元素的 draw 方法,这样就将 Draw 过程传递了下去。
最后通过 onDrawScrollBars 方法绘制完滚动条(滚动条可能不显示)。这样 View 的 Draw 过程就完成了。