天天看点

从源码学习自定义View(三):Layout和Draw过程

Layout

  前面讲述了measure过程和LayoutParams的生成问题,到这里View的整个绘制过程已经差不多了,剩下的就是layout过程和draw过程了。

  另外对于布局,就是将View的位置固定下来。值得注意的是,这里的位置是相对于父View而言的,也就是说,以父View的左上角为坐标零点的相对位置。

// View.java

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;

	// 最终会调用setFram来设置位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

	// 若是位置发生了改变,则调用onLayout方法进行重新布局
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);

        ...
    }
	...
}
           

  同样的,layout方法虽然不是final修饰的,但是也是没有View覆写它,实际上,measure,layout,draw都没有被覆写,他们是android提供的一套布局框架。从它的实现来看,首先通过setFrame(setOpticalFrame内部也是调用setFrame)方法进行设置,该方法会判断View的位置是否发生变化,若是发生了变化就会返回true,然后在layout中就会调用onLayout 方法进行重新布局。

  在View中,onLayout方法是个空实现,而在ViewGroup中,该方法是被重写为抽象方法。因为ViewGroup可以存在子View,因此必须实现该方法来对子View进行布局。

// View.java

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (DBG) {
        Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                + right + "," + bottom + ")");
    }

	// 新设置的位置与原来的坐标不一致
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;

		// 获取新旧的宽高,并且判断是否是宽高发生的变化
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // 若是不一致则进行重绘
        invalidate(sizeChanged);

		// 重新赋值新的位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
       ...
    }
    return changed;
}
           

  从setFrame中可以看到的当layout的位置发生变化的时候,会保存新的位置,然后根据尺寸是否发生变化进行不同的重绘过程。这与View的measure一样,都是默认的一种方法,而我们想要自己布局的话,只需要在onLayout方法中改变即可。

  在setFrame中,若是位置发生了变化,则会进行保存新的位置,这时候View的宽高就确定了,可以通过getWidth/Height方法进行获取。

// View.java

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

  layout 的过程比较简单,就是先通过setFrame方法进行判断旧的位置是否和新设置的位置相同,不相同的话则会设置新的位置并调用onLayout方法。但是在View中onLayout是空实现,也就是说在View中可以不用重写这个方法进行布局,而单纯使用setFrame即可。但是在ViewGroup中,把layout给加了final,也不能够重写,并且把onLayout给加了abstract。

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}
@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);
           

  因为在ViewGroup中,可能会有子View,因而需要对子View进行布局。也就是说我们自定义的ViewGroup不能重写layout,而只能重写onLayout,并且应该在onLayout中对子View进行layout。

总结

  对于我们自定义的View,我们不应该重写layout方法,而ViewGroup更是将其final化从而禁止重写。并且对于View,我们使用默认的layout方法即可完成布局任务,若是有其他需求,可以重写onLayout进行重新布局。对于ViewGroup,抽象化的onLayout方法要求我们必须重写它,并且我们应该根据要求在onLayout中对子View进行布局。

  1,layout阶段只需要重写onLayout方法并在其中进行布局的操作

  2,若是普通View甚至可以不重写onLayout方式,默认方式进行布局即可

  3,若是ViewGroup,则需要在onLayout中进行循环遍历子View,然后计算子View的位置并调用子View的layout将布局分发下去

  4,layout结束

Draw

  对于三大过程的measure和layout我们已经分析完了,从测量到布局最后只剩下一个绘制了。其实绘制细节很复杂,因为这将会导致View展示在屏幕上,涉及到它的具体表现。但是他的流程却很简单,每一步都分得很清。

// View.java

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

        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方法可以看到,绘制过程分为6个部分:

1,绘制背景
	2, 如果必要,保存canvas的layer来准备fading
	3, 绘制本身
	4, 绘制孩子View
	5, 如果必要,绘制衰退效果并恢复layer
	6, 绘制装饰效果,例如滚动条等
           

  而就上面的6条而言,2和5通常是不必要的,可以忽略的。那么绘制的重点就留在了1346这几条上了。

  其中,绘制背景和绘制装饰一般也是不需要我们关注的,因为这两者也基本都是固定的。所以我们主要关注的就是绘制本身和绘制子View。

// View.java

 // 绘制自身
 protected void onDraw(Canvas canvas) {
}

// 绘制子View
protected void dispatchDraw(Canvas canvas) {
}
           

  但是在View中这两个方法都是空实现,这是因为每个View的绘制都各不相同,因此绘制本身不可能有具体的实现,而是交由具体的子类去实现自身的绘制。

  另外对于普通子View而言,它是不会有子View,那么dispatchDraw方法当然也是一个空实现。但是对于ViewGroup,由于它可能会含有子View,所以dispatchDraw就应该被重写,去实现子View的绘制过程。

// ViewGroup.java

protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;

    if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
        final boolean buildCache = !isHardwareAccelerated();
        // 循环子View,绑定本身带的动画
        for (int i = 0; i < childrenCount; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                final LayoutParams params = child.getLayoutParams();
                attachLayoutAnimationParameters(child, params, i, childrenCount);
                bindLayoutAnimation(child);
            }
        }
        ...
    }

    boolean more = false;
 
    ...
    for (int i = 0; i < childrenCount; i++) {
  
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        // 绘制子View
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }

    while (transientIndex >= 0) {
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
            break;
        }
    }

    if (mDisappearingChildren != null) {
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        final int disappearingCount = disappearingChildren.size() - 1;
        for (int i = disappearingCount; i >= 0; i--) {
            final View child = disappearingChildren.get(i);
            more |= drawChild(canvas, child, drawingTime);
        }
    }
  ...
}
           

  上面的dispatchDraw比较复杂,考虑的比较多,包括隐藏的View,以及View的动画,但是我们可以看到的是它最终都是是调用了drawChild方法从而进一步调用child的draw方法进行绘制子View。因此,对于绘制过程,我们也只需要关注onDraw方法,即只绘制自身,而不用担心子View的分发绘制。

总结

  View的绘制过程比较复杂,但是对于我们而言,它的流程比较简单。并且,在我们自定义View的时候,只用考虑onDraw方法。只需要在onDraw中进行自身的绘制即可。对于ViewGroup,其中的dispatchDraw已经实现了子View的分发绘制,我们也不必进行其他操作。

   1,重写onDraw,在其中做自身的绘制过程

总结:

  View整个表现在屏幕上总共有三个过程,分别是measure,layout和draw。其中,measure过程要根据父View的模式和子View的宽高模式共同计算,得出measuredWidth/Height。而layout过程则根据measuredWidth/Height来设置View的位置,一旦layout结束,View的大小也就确定了。draw按照绘制背景->自身->子View->装饰,这个过程进行绘制。

  在这个过程中我们也都总结了各个阶段重要的一些步骤方法,这些步骤都是我们自定义View所涉及到的,根据这些步骤,我们可以很轻松的自定义出想要的view。

相关文章目录:

  从源码学习自定义View(一):Measure过程

  从源码学习自定义View(二):LayoutParams

  从源码学习自定义View(三):Layout和Draw过程

继续阅读