美文网首页
Android View的工作原理(二)View的工作流程

Android View的工作原理(二)View的工作流程

作者: 怡红快绿 | 来源:发表于2019-05-05 16:04 被阅读0次

View的工作流程主要指measure、layout和draw三大流程,其中measure负责确定View的测量宽高,layout负责确定View的最终宽高和四个顶点位置,draw负责将View绘制到屏幕上。

一、measure过程

对于原始View来说,它只需要调用measure方法完成自己的测量;但是对于ViewGroup来说,它不仅要完成自己的测量过程,还要遍历去调用所有子元素的measure方法,这样各个子元素才能完成自己的测量过程。因此measure过程我们要分情况进行讨论。

(1)View的measure过程

View的测量过程由它的measure()方法完成,measure()方法是一个final方法,这就意味着它不能被子类重写。measure()方法中会调用onMeasure()方法,因此我们直接从onMeasure()方法入手。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension方法会设置View宽高的测量值,因此我们需要知道getDefaultSize方法是如何获取宽高测量值的:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

对于我们来说,我们只需要看AT_MOST和EXACTLY两种模式,这样getDefaultSize()方法返回的大小就是MeasureSpec中的SpecSize,这个SpecSize就是View的测量后大小。至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,我们暂时可以不考虑它。

从getDefaultSize方法的实现来看,View的宽高由SpecSize决定,所以我们可以得出结论:

直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content相当于使用match_parent。

根据上一节【View的工作原理(一)基本概念】介绍的普通View的MeasureSpec创建规则可以知道:在布局中使用wrap_content的时候,不论父容器是AT_MOST模式还是EXACTLY模式,View都是最大模式并且大小不会超过父容器的剩余空间,也就是说View会填充满父容器的剩余空间,这和使用match_parent达到的效果显然是相同的。这显然不是我们期待的结果,因此我们需要在onMeasure方法内进行处理:

private int defaultWidth = 144;
private int defaultHeight = 144;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(defaultWidth, defaultHeight);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(defaultWidth, heightSize);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSize, defaultHeight);
    }
}

从上面的代码可以看到,在布局中使用wrap_content的时候我们为View直接设置一个默认宽高值,这个默认宽高值通常是需要根据需求进行计算得出的,想了解更多细节可以阅读TextView、ImageView等控件,观察它们是如何对测量过程进行特殊处理的。

(2)ViewGroup的measure过程

对于ViewGroup来说,它不仅要完成自己的measure过程,而且会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,它会提供一个measureChildren方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

上述代码很简单,通过遍历找出所有的子View,并对其中非GONE状态的View调用measureChild方法。继续看measureChild方法:

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

getChildMeasureSpec方法使用父容器的MeasureSpec和padding参数计算出子元素的最终MeasureSpec,并调用View的measure方法进行测量。getChildMeasureSpec方法在上一节【View的工作原理(一)基本概念】已经提到过,这里就不重复介绍了。

不同的ViewGroup子类有不同的布局特性,因此它的onMeasure方法需要子类根据实际情况去实现。我们来看看LinearLayout是如何实现的:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

首先判断是竖直布局还是水平布局,我们接下来只分析measureVertical方法,measureHorizontal方法类似。由于代码比较长,以下简要概括它的主要逻辑:

  1. 首先遍历并measure所有子元素,使用mTotalLength变量存储它在竖直方向上的初步高度;
  2. 子元素测量完成后,开始测量自身大小;
  3. 水平方向上的测量过程遵循View的测量过程,利用maxWidth变量存储所有子元素的最大宽度;
  4. 竖直方向上与View有所不同:如果高度采取的是固定值或者match_parent,它的测量过程与View一致,即为SpecSize;如果采取的是wrap_content,那么它的高度是所有子元素所占高度总和,但是仍然不能超过父容器的剩余空间。

二、layout过程

layout方法是View用来确定自身位置的,当位置被确定,它紧接着会调用onLayout方法:如果它有子元素,将会遍历所有的子元素并调用所有子元素的layout方法,这些layout方法内又会调用onLayout方法,如此循环往复。

先看View的layout方法,这里只贴出核心代码:

//为视图及其所有后代指定大小和位置
public void layout(int l, int t, int r, int b) {
    ……
    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);
    ……
    }
    ……
}

layout方法的大致流程是这样的:

  • 首先通过setFrame方法设定View的四个顶点,为顶点变量赋值:
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    四个顶点确定了,那么View在父容器中的位置也就确定了。
  • 接着调用onLayout方法,这个方法的作用是父容器确定子元素的位置,这点和onMeasure方法类似。onLayout方法在View和ViewGroup中都是没有实现的,因为不同的父容器对子元素的布局会有不同的要求,因此需要根据具体的布局情况对onLayout方法进行重写。

接下来看看LinearLayout是如何重写onLayout方法的:

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

由于横向和竖直两种布局原理差不多,所以下面只研究竖直布局的情况:

/**
     * Position the children during a layout pass if the orientation of this
     * LinearLayout is set to {@link #VERTICAL}.
     *
     * @see #getOrientation()
     * @see #setOrientation(int)
     * @see #onLayout(boolean, int, int, int, int)
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    void layoutVertical(int left, int top, int right, int bottom) {
        //需要考虑padding属性的影响
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go 处理最右边的子元素
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child  计算容器的最大可用空间
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        //判断不同gravity情况下子元素的childTop值
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already  
              //mTotalLength是包含padding在内的
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
        //遍历所有子元素
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {//view状态为GONE时不占据空间
                //获取测量后宽高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                //获取LayoutParams
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                //计算不同gravity情况下子元素的childLeft 
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }
                //考虑divider占据的空间
                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);
            }
        }
    }

setChildFrame方法:

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

到此,子元素的四个顶点位置就确定了。

Tips:自定义普通的View不需要重写onLayout方法,定义ViewGroup需要重写onLayout方法

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

        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()方法的源码我们可以分析出绘制的主要步骤如下:

  1. 绘制View的背景 drawBackground(canvas);
  2. 绘制View的内容 onDraw(canvas);
  3. 绘制children dispatchDraw(canvas);
  4. 绘制一些其他的装饰部分,例如foreground, scrollbars等等 onDrawForeground(canvas);

View的绘制过程的传递是通过dispatchDraw方法实现的,dispatchDraw会遍历调用所有子元素的draw方法,这样draw事件就一层一层传递下去了。


参考

《Android开发艺术探索》

相关文章

网友评论

      本文标题:Android View的工作原理(二)View的工作流程

      本文链接:https://www.haomeiwen.com/subject/abbvnqtx.html