Android学习笔记之View的绘制流程(二)——layout、draw过程

一、layout过程

layout过程的作用是确定View的最终宽高和四个顶点的位置。其传递过程与measure类似,首先在layout方法中确定ViewGroup的位置后,再去调用onLayout方法,然后在onLayout中遍历所有子元素并调用其layout方法,从而完成layout过程的传递。不同的是,在measure过程中普通的View(不是ViewGroup)的measure过程是一直进行到onMeasure方法的,因为在onMeasure方法中确定了View的测试宽高尺寸是SpecSize还是SuggestedMinimum。但是在layout过程中onLayout只用于ViewGroup去遍历子元素,普通View的该方法为空,ViewGroup的该方法待实现。如下:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

1.1.View的layout方法

注意这里讲的View,不再像是measure过程中分为普通View和ViewGroup,而是泛指所有View。大致流程分为两步:1.调用setFrame方法来确定View的四个顶点位置  2.调用onLayout方法来遍历子元素。源码如下:

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);

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        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;

    if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
        mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
        notifyEnterOrExitForAutoFillIfNeeded(true);
    }
}

1.2.View的onLayout方法

之前说过View的onLayout方法是个空的方法,因为onLayout的具体实现与具体的布局特性有关,需要在具体的ViewGroup中去实现。这里我们看一下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);
    }
}
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);
        }
    }
}

可以看到,在onLayout方法中会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,而setChildFrame方法仅仅是调用了layout方法而已,其中子元素的宽高为测量宽高。

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

除此之外,在onLayout方法中还会使childTop不断增大(在layoutHorizontal方法中则是childLeft不断增大),因为该布局特性是竖直排列,每测量完一个子元素都会加上子元素的高度、外边距、偏移量等。

至此,我们可以总结一下layout过程中方法大致的调用顺序为:从ViewGroup开始,layout→onLayout→child.layout(如果子元素仍是ViewGroup则继续)→子元素的onLayout(再重复上述步骤)

1.3.测量宽高和最终宽高的区别

我们要知道,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高。因此如果想要准确地获取测量宽高的值,应该在measure过程结束后比如在onLayout方法中去获取测量宽高(getMeasuredWidth)。另一方面,最终宽高的值是通过下列方法获取的:

public final int getWidth() {
    return mRight - mLeft;
}
public final int getHeight() {
    return mBottom - mTop;
}

结合之前onLayout过程的分析可以看到,这样获取的最终宽高的值就是childWidth(childHeight),也就是测量宽高。

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

因此在View的默认实现中,View的测量宽高和最终宽高是相等的,区别在于:测量宽高的形成时机(measure过程)在最终宽高的形成时机(layout过程)之前。

二、draw过程

draw的过程主要有以下几个步骤,也能从源码的注释中看出来:

  1. 绘制背景(background.draw(canvas))
  2. 绘制自己(onDraw(canvas))
  3. 绘制子元素(dispatchDraw(canvas))
  4. 绘制装饰(onDrawForeground(canvas))
/*
 * 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);

    drawAutofilledHighlight(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);

    // Step 7, draw the default focus highlight
    drawDefaultFocusHighlight(canvas);

    if (debugDraw()) {
        debugDrawFocus(canvas);
    }

    // we're done...
    return;
}

值得注意的是draw过程的传递是通过dispatchDraw方法来实现的,具体调用顺序为:从ViewGroup开始,draw→onDraw(绘制自己)、dispatchDraw(传递)→drawChild→child.draw→子元素的onDraw(如果子元素仍是ViewGroup则继续)、dispatchDraw(重复上述步骤)

你可能感兴趣的:(Android,layout过程,draw过程,测量宽高和最终宽高,Android)