View 工作原理(三)| 艺术探索笔记

这里介绍 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;
    }

    ...

}

于是得到主要步骤

  1. 绘制背景 drawBackground
  2. 绘制内容 onDraw
  3. 绘制子元素 dispatchDraw
  4. 绘制装饰 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 过程就完成了。

你可能感兴趣的:(View 工作原理(三)| 艺术探索笔记)