measure layout draw

measure:测量宽、高

layout:确定在父容器中的位置,布局

draw:绘制在屏幕上

一、总体流程:

视图的测量、布局、绘制都是按照视图树从上到下的,大致可分为DecorView--->ViewGroup--->View这样三个层级。

当Activity对象被创建完成,会将DecorView添加到Window中(显示),同时创建ViewRoot的实现对象ViewRootImpl与之关联。

ViewRootImpl会调用performTraversals来进行View的绘制过程。经过measure、layout、draw三个流程才能完成一个View的绘制过程,分别用于测量宽、高;确定在父容器中的位置;绘制在屏幕上三个过程。而measure方法会调用onMeasure函数,这其中又会调用子元素的measure函数,如此反复就能完成整个View树的遍历过程。其他两个流程也同样如此。

viewdesignicrud的使用 view的绘制流程_View绘制

measure决定了View的宽高,测量之后就可以根据getMeasuredWidth和getMeasuredHeight来获取View测量后的宽和高,几乎等于最终的宽和高,但有例外;layout过程决定了View四个顶点的位置和实际的宽和高 ,完成之后可以根据getTop,getBottom,getLeft,getRight来获得四个顶点的位置,并且可以使用getWidth和getHeight来获取实际的宽和高;draw过程就决定了View的显示,完成draw才能真正显示出来。

二、onMeasure()

我们先来看看方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //setMeasuredDimension方法必须由{@link #onMeasure(int, int)}调用以存储测量的宽度和测量的高度
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

里面有两个重要的参数,一个是widthMeasureSpec,一个是heightMeasureSpec,它们叫做MeasureSpec,它是系统将View的参数根据父容器的规则转换而成的,之后根据它来测量出View的宽和高。它实际上是一个32位的int值,高二位表示SpecMode,就是测量的模式;低30位表示SpecSize,即在某种测量模式下的规格大小。

MeasureSpec有三种模式,常用的有两种:EXACTLY和AT_MOST。EXACTLY表示父容器已经检测出View所需要的精确大小(即父容器根据View的参数已经确定View的大小) ,这时View的最终大小就是SpecSize的值,它对应于View参数中的match_parent和具体大小数值这两种模式;AT_MOST表示父容器指定了一个可用大小的数值,记录在SpecSize中,View的大小不能大于它,但具体的值还是看View的具体实现,它对应于View参数中的wrap_content。

DecorView的测量由窗口的大小和自身的LayoutParams决定,具体逻辑由getRootMeasureSpec决定,如果是具体值或者是match_parent,就是精确模式;如果是wrap_content就是最大模式;普通View的measure实际上是由父元素进行调用的(遍历),父元素调用child的measure之前使用getChildMeasureSpec来转换得到子元素的MeasureSpec,总结而来就是与自身的参数以及父元素的SpecMode有关:

1、如果View的参数是具体值,那么不管父元素的Mode是什么,子元素的Mode都是精确模式并且大小就是参数的大小;

2、如果View的参数是match_parent,如果父元素的mode是精确模式,那么View也是精确模式并且大小是父元素剩余的大小;如果父元素的mode是最大模式,那么View也是最大模式;

3、如果View的参数是wrap_content,那么View的模式一定是最大化模式,并且不能超过父容器的剩余空间。 

viewdesignicrud的使用 view的绘制流程_自定义_02

View自身的onMeasure方法就是把MeasureSpec的size设为最终的测量结果,这样的测量问题就是match_parent和wrap_content是一样的结果(因为wrap_content的size是最大可用的size),所以如果自定义View直接继承自View,就需要对wrap_content进行处理,ImageView等都对wrap_content进行了特殊处理。 

ViewGroup的measure过程:

ViewGroup不同于View,它是一个抽象类,没有实现onMeasure方法(因为具体的Layout布局特性各不相同),但它measure时会遍历children调用measureChild,执行getChildMeasure进行子元素的MeasureSpec创建,创建过程之前已经了解了,就是利用自身的Spec与子元素参数进行创建。

三、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;
    //setFrame()  /  setOpticalFrame() : 确定View自身的位置
    //即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回
    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():确定该View所有的子View在父容器的位置
        onLayout(changed, l, t, r, b);
        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                //RoundScrollbarRenderer是用于在圆形Wear设备上绘制圆形滚动条的助手类
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

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

    final boolean wasLayoutValid = isLayoutValid();

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

    if (!wasLayoutValid && isFocused()) {
        mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
        if (canTakeFocus()) {
            // We have a robust focus, so parents should no longer be wanting focus.
            clearParentsWantFocus();
        } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
            // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
            // layout. In this case, there's no guarantee that parent layouts will be evaluated
            // and thus the safest action is to clear focus here.
            clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            clearParentsWantFocus();
        } else if (!hasParentWantsFocus()) {
            // original requestFocus was likely on this view directly, so just clear focus
            clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
        }
        // otherwise, we let parents handle re-assigning focus during their layout passes.
    } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
        mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
        View focused = findFocus();
        if (focused != null) {
            // Try to restore focus as close as possible to our starting focus.
            if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                // Give up and clear focus once we've reached the top-most parent which wants
                // focus.
                focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
        }
    }

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

    notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
}

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
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 our old position
        invalidate(sizeChanged);
        //通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;


        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }

        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            // If we are visible, force the DRAWN bit to on so that
            // this invalidate will go through (at least to our parent).
            // This is because someone may have invalidated this view
            // before this call to setFrame came in, thereby clearing
            // the DRAWN bit.
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            // parent display list may need to be recreated based on a change in the bounds
            // of any child
            invalidateParentCaches();
        }

        // Reset drawn bit to original value (invalidate turns it off)
        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

layout的作用是ViewGroup来确定子元素的位置,当ViewGroup的位置被确定了之后,它就在自己的onLayout函数中遍历所有的子元素并调用其layout方法,确定子元素的位置,对于子元素View,layout中又会调用其onLayout函数。View和ViewGroup中都没有真正实现onLayout方法。但View和ViewGroup的layout方法是一致的,作用都是用于确定自己的位置,layout方法会调用setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom四个值,这样就确定了View在父元素中的位置。

问题:getMeasuredHeight和getHeight有什么区别(同Width)? 

在measure之后就可以使用getMeasuredHeight来进行获取测量的高度,而layout过程是晚于measure的,layout之后就可以使用getHeight来进行获取真实的高度。

ViewGroup的setChildFrame会调用child的layout来确定child的真实位置,所以说如何child的layout不重写,那么就是一样的;如果child的layout函数被重写,就会有不一样的结果。

getMeasuredHeight是控件的实际高度,与屏幕无关。

getHeight得到的是View的显示高度,跟View在屏幕的显示情况有关。

四、onDraw()

/**
 * 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.
 */
/**
 * 手动将此视图(及其所有子视图)渲染到给定的Canvas。在调用此函数之前,视图必须已经完成了完整的布局。
 * 实现视图时,实现{@link #onDraw(android.graphics.Canvas)}而不是覆盖此方法。如果确实需要覆盖此
 * 方法,请调用超类版本
 *
 * @param canvas 呈现视图的画布
 */
@CallSuper
public void draw(Canvas canvas) {
    // 所有的视图都是调用View的draw()绘制视图(ViewGroup没有复写此方法)
    // 在自定义View时,不应该重写该方法,而是重写onDraw(Canvas)方法进行绘制。
    // 如果自定义的视图确实要重写该方法,那么需要先调用super.draw(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)
     */
     /*
     * 绘制遍历执行几个绘制步骤,必须按适当的顺序执行:
     *
     *      1、绘制背景
     *      2、如有必要,保存画布的图层以准备褪色
     *      3、绘制View的内容
     *      4、绘制子View
     *      5、如有必要,绘制淡入淡出的边缘并恢复图层
     *      6、绘制装饰,例如滚动条
     */

    // Step 1, draw the background, if needed
    // 步骤1:绘制View本身背景
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    // 如果有必要,就保存图层(还有一个复原图层)
    // 优化技巧:
    // 当不需要绘制Layer时,“保存图层”和“复原图层”这两步会跳过
    // 因此在绘制的时候,节省layer可以提高绘制效率
    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
        // 步骤3:绘制View的内容
        if (!dirtyOpaque) 
        onDraw(canvas);
        
        // Step 4, draw the children
        // 步骤4:绘制子View,默认为空实现,单一View中不需要实现,ViewGroup中已经实现了该方法
        // 再对dispatchDraw方法进行一些说明:该方法是在draw方法中被调用以绘制子视图。另外
        // 在子类中可能重写该方法以在绘制其子级之前(但在绘制其自己的视图之后)获得控制权
        dispatchDraw(canvas);
        // 如果视图是自动填充的,则在视图上绘制{@link View#isAutofilled()}突出显示
        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)
        // 步骤6:绘制滑动条和前景色等等
        // 再对onDrawForeground方法进行一些说明:该方法是为该视图绘制任何前景内容
        // 前景内容可能由滚动条、{@link #setForeground foreground}可绘制或其他特定于
        // 视图的装饰组成。前景绘制在主视图内容的顶部。
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        // 步骤7:绘制默认焦点突出显示
        drawDefaultFocusHighlight(canvas);

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

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

    /*
     * Here we do the full fledged routine...
     * (this is an uncommon case where speed matters less,
     * this is why we repeat some of the tests that have been
     * done above)
     */
    /*
     * 在这里,我们执行完整的例行程序......
     * (这是速度不那么重要的罕见情况,这就是我们重复上面已经完成的一些测试的原因)
     */ 

    boolean drawTop = false;
    boolean drawBottom = false;
    boolean drawLeft = false;
    boolean drawRight = false;

    float topFadeStrength = 0.0f;
    float bottomFadeStrength = 0.0f;
    float leftFadeStrength = 0.0f;
    float rightFadeStrength = 0.0f;

    // Step 2, save the canvas' layers
    int paddingLeft = mPaddingLeft;

    final boolean offsetRequired = isPaddingOffsetRequired();
    if (offsetRequired) {
        paddingLeft += getLeftPaddingOffset();
    }

    int left = mScrollX + paddingLeft;
    int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
    int top = mScrollY + getFadeTop(offsetRequired);
    int bottom = top + getFadeHeight(offsetRequired);

    if (offsetRequired) {
        right += getRightPaddingOffset();
        bottom += getBottomPaddingOffset();
    }

    final ScrollabilityCache scrollabilityCache = mScrollCache;
    final float fadeHeight = scrollabilityCache.fadingEdgeLength;
    int length = (int) fadeHeight;

    // clip the fade length if top and bottom fades overlap
    // overlapping fades produce odd-looking artifacts
    if (verticalEdges && (top + length > bottom - length)) {
        length = (bottom - top) / 2;
    }

    // also clip horizontal fades if necessary
    if (horizontalEdges && (left + length > right - length)) {
        length = (right - left) / 2;
    }

    if (verticalEdges) {
        topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
        drawTop = topFadeStrength * fadeHeight > 1.0f;
        bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
        drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
    }

    if (horizontalEdges) {
        leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
        drawLeft = leftFadeStrength * fadeHeight > 1.0f;
        rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
        drawRight = rightFadeStrength * fadeHeight > 1.0f;
    }

    saveCount = canvas.getSaveCount();

    int solidColor = getSolidColor();
    if (solidColor == 0) {
        if (drawTop) {
            canvas.saveUnclippedLayer(left, top, right, top + length);
        }

        if (drawBottom) {
            canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
        }

        if (drawLeft) {
            canvas.saveUnclippedLayer(left, top, left + length, bottom);
        }

        if (drawRight) {
            canvas.saveUnclippedLayer(right - length, top, right, bottom);
        }
    } else {
        scrollabilityCache.setFadeColor(solidColor);
    }

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    final Paint p = scrollabilityCache.paint;
    final Matrix matrix = scrollabilityCache.matrix;
    final Shader fade = scrollabilityCache.shader;

    if (drawTop) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        matrix.setScale(1, fadeHeight * bottomFadeStrength);
        matrix.postRotate(180);
        matrix.postTranslate(left, bottom);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        matrix.setScale(1, fadeHeight * leftFadeStrength);
        matrix.postRotate(-90);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        matrix.setScale(1, fadeHeight * rightFadeStrength);
        matrix.postRotate(90);
        matrix.postTranslate(right, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(right - length, top, right, bottom, p);
    }

    canvas.restoreToCount(saveCount);

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

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

View的绘制主要分为4步:

1、绘制背景 background.draw(canvas) 

2、绘制自己的内容 onDraw(canvas)

3、绘制子View dispatchDraw(canvas)

4、绘制装饰(滑动条、前景色等等)onDrawForeground(canvas)

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了dispatchDraw()方法,而在单一View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

onDraw中进行绘制自己的操作。使用canvas进行绘制等等,简单来说就是利用代码画自己需要的图形。

绘制过程中的save和restore

Android中的旋转rotate旋转的是坐标系,也就是说旋转画布实际上画布本身的内容是不动的,不会直接把画布上已经存在的内容进行移动,只是旋转坐标系,这样旋转之后的操作全部是针对这个新的坐标系。

save就是保存当前的状态,之后再调用restore时,坐标系复原到save保存的状态。

常用优化技巧:

1、onDraw()中不要创建新的局部对象

onDraw()会被频繁调用,如果方法内部创建了局部变量,则会一瞬间产生大量的临时对象,这使得占用过多的内存,系统频繁GC,降低了程序执行效率。

2、避免onDraw()中执行大量耗时操作

View的最佳绘制效率为60fps,因为LCD的频率是60HZ,显示每一帧的间隔是16ms,所以每一个VSync信号的时间间隔是16ms,接收到该信号时视图会进行刷新,如果你绘画时间过长就会导致View绘制不流畅,可以使用多线程来解决。

3、避免Overdraw

在同一个地方绘制多次肯定是浪费资源的,也避免浪费资源去渲染那些不必要和看不见的背景。我们可以在手机的开发者设置中开启调试GPU过度绘制选项来查看视图绘制的情况。

五、总结

从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免与match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View,一般会重写onMeasure()方法,循环测量子View。

onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是一个抽象方法,实现该方法,来对子View进行布局。

onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是一个空方法。