Android View 工作原理分析

View的工作原理分析

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

measure过程

measure过程分两种,第一种是view,只需通过measure方法就可以完成测量过程。还有一种是ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

View的measure过程

View的measure过程由measure方法来完成,measure方法是一个final类型的方法,子类不能重写,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现:

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

getDefaultSize逻辑很简单,通过MeasureSpec获取specMode和specSize,specMode对于开发者来说只需要看AT_MOST和EXACTLY这两种情况,specSize就是view测量后的大小,view最终大小是在layout阶段确定的,但是view测量大小与view最终大小几乎等同。至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,View的宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值:

private Drawable mBackground;

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

从getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,View的宽度为mMinWidth,而mMinwidth对应于android:minWidth这个属性所指定的值,如果不指定,那么MinWidth则默认为0;如果View指定了背景,则View的宽度为mMinWidth与mBackground.getMinimumWidth两者间的最大值

mBackground是Drawable类型,我们看一下Drawable的getMinimumWidth方法:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,比如BitmapDrawable的原始宽/高(图片的尺寸),否则就返回0(ShapeDrawable无原始宽/高)。

从getDefaulSize方法的实现来看,如果specMode是AT_MOST与EXACTLY时,view的宽高都等于specSize,通过Android View 工作原理基础
结尾的表可知,这种情况下View的specSize是parentSize,而parentSize是父容器当前剩余的空间大小。所以设置wrap_content的效果与match_parent会一样,要解决这个问题的话需要重写onMeasure,代码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (eightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    }
}

在上面代码中,给View指定一个默认的内部宽高(mWidth和mHeight),并在wrap_content时设置此宽高即可。对于其他情形沿用系统的测量值,至于这个默认的内部宽/高的大小根据需要灵活指定即可。

view Measure 过程流程图

在这里插入图片描述

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再通归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫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);
        }
    }
}

从上述代码中看到,ViewGroup的measure时,会对每一个子元素进行measure,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);
}

measureChild的思想就是取出子元素的LayoutParams,然后再通过getChidMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getChildMeasureSpec是根据父容器的MeasureSpec结合View本身LayoutParams来确定子元素的MeasureSpec,它的工作过程已经在Android View 工作原理基础进行了详细分析。

LinearLayout的measure过程

ViewGroup并没有定义其测量的具体过程,因为ViewGroup是一个抽象类,不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。

首先看一下LinearLayout的onMeasure方法:

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

看一下measureVertical的源码,源码比较长,下面只描述大概逻辑:

// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
    ...
    // Determine how big this child would like to be. If this or
    // previous children have given a weight, then we allow it to
    // use all available space (and we will shrink things later
    // if needed).
    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
            heightMeasureSpec, usedHeight);

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

    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
            lp.bottomMargin + getNextLocationOffset(child));
    ...
}

从上面的代码可以看出,系统会遍历子元素并对每一个子元素执行measureChildBeforeLayout方法,这个方法最终会调用child.measure方法,这样各个子元素就开始依次进入measure过程,测量结果包括子元素的高度以及竖直方向上的margin等,最后通过mTotalLength这个变量来存储LinearLayout在竖直方向上的初步高度。当子元素测量完毕之后,LinearLayout会测量自己的大小,看源码:

// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

...

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);

当子元素测量完毕之后,LinearLayout会根据子元素的情况来测量自己的大小,它的最终高度还需要加上padding。
竖直的LinearLayout在水平方向的测量过程遵循View的测量过程,而竖直方向的测量过程和View有些不同,具体可以参考resolveSizeAndState的源码:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

如果布局中height用的是match_parent或者具体值,那么绘制过程和View一致,即高度为specSize。如果布局中height采用wrap_content,那么它的高度是所有的子元素所占用的高度总和,但不会超过它的父容器剩余空间(heightMeasureSpec中获取的specSize)。

measure完成以后,通过getMeasureWidth/Height就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下系统可能要多次调用measure方法进行测量,在这种情形下onMeasure方法中拿到的测量值很可能是不准确的。
一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

LinearLayout垂直方向measure流程图

在这里插入图片描述

layout过程

Layout过程是用于确定View位置的,layout方法确定了View本身的位置,而onLayout方法则会遍历子元素的layout方法确定所有子元素的位置,先看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<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);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

layout的方法的大致流程如下,首先会通过setFrame方法来设定View的四个顶点的位置(View本身的位置),即初始化mLeft,mTop,mRight,mBottom这四个值。接着会调用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);
    }
}

LinearLayout中onLayout逻辑和onMeasure类似,这里选择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);
        }
    }
}

可以看到,此方法会遍历所有子元素,childTop会逐渐累加子元素height和margin等,意味着下面的子元素会被放置在上面的子元素下方,符合竖直方向的线性布局原理。setChildFrame中的传递的width和height实际上就是子元素测量宽高。

setChildFrame实现如下:

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

setChildFrame方法会调用子元素的layout方法,这样父容器在layout方法中完成自己的定位,然后通过onLayout方法调用子元素的layout方法,确定子元素自己的位置,这样一层一层传递下去完成整个View树的layout过程。

最后我们回到Layout方法中的setFrame方法,该方法中有如下几句赋值语句,这样子元素的位置就确定了。

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

LinearLayout垂直方向layout流程图

在这里插入图片描述

draw过程

Draw过程就比较简单,它的作用是将View绘制到屏幕上面,View的绘制过程遵循如下几步:

  • 绘制背景 drawBackground(canvas)
  • 绘制自己 onDraw(canvas)
  • 绘制子元素 dispatchDraw(canvas)
  • 绘制装饰 onDrawForeground(canvas)

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

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

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

viewGroup中,draw过程传递是通过dispatchDraw来实现的,它会遍历调用所有子元素的draw方法,这样draw事件就一层层传递了下去,相关代码如下:

@Override
protected void dispatchDraw(Canvas canvas) {
    ...
    for (int i = 0; i < childrenCount; i++) {
        ...
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ...
}

View有一个特殊的方法setWillNotDraw,先看下源码:

/**
 * If this view doesn't do any drawing on its own, set this flag to
 * allow further optimizations. By default, this flag is not set on
 * View, but could be set on some View subclasses such as ViewGroup.
 *
 * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
 * you should clear this flag.
 *
 * @param willNotDraw whether or not this View draw on its own
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

从这个方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个校化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是。当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。

发布了174 篇原创文章 · 获赞 119 · 访问量 55万+

猜你喜欢

转载自blog.csdn.net/lj402159806/article/details/99855366