View的工作原理

这篇文章主要整理一下Android系统中,View的工作流程。主要就是measure、layout、draw三个过程。

我们知道,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立联系。View的绘制流程从ViewRootImpl的performTraversals方法开始,方法中会调用performMeasure、performLayout和performDraw方法分别开启measure、layout及draw过程。其中measure用来测量View的宽和高,layout用来确定View在父容器中放置的位置,而draw则负责将View绘制在屏幕上。大致流程如下图所示:

另外,在分析代码原理之前,我们先要理解一下MeasureSpec。MeasureSpec在很大程度上决定了一个View的尺寸规格。在测量的过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽和高。

MeasureSpec是一个32位的int值,高2位代表SpecMode,表示测量模式;低30位代表SpecSize,是指在某种测量模式下的规格大小。SpecMode有三种类型:

UNSPECIFIED

父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。

EXACTLY

父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。

AT_MOST

父容器指定了一个可用大小SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

 

一、measure过程

如果这个View只是一个View,那么通过measure方法就可以完成测量过程;如果是一个ViewGroup,除了要测量自身之外,还会遍历调用所有子View的measure方法,各个子元素再递归去执行这个流程。

View的measure方法是一个final的方法,也就是子类无法重写这个方法。在measure方法中会调用View的onMeasure方法,这个方法是可以重写的方法。View的onMeasure方法如下所示:

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

在setMeasuredDimension() 里最终调用了setMeasureDimensionRaw。用来将最终测量的宽、高的值存入本View的成员变量中。

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

接下来我们回头看看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;
    }

这个方法虽然有三个case,但是事实上只有两种情况,第一种是在UNSPECIFIED时;第二种是在AT_MOST和EXACTLY时。

先来看在UNSPECIFIED情况,在这种情况下直接返回传入这个方法的第一个参数,我们看一下第一个参数的由来

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

如果View没有设置背景,那么返回的是mMinWidth,而这个值对应于android:minWidth这个属性所指的值,如果不指定这个属性,它的默认值为0。如果设定了View的背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()),即android:minWidth和背景的最小宽度中两者的最大值。

接下来是第二种情况,在AT_MOST和EXACTLY时,直接返回measureSpec的specSize的值。

由上述内容可以看出来,View的宽、高由specSize决定,所以直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。因为在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽高等于specSize。在这种情况下View的specSize就是parentSize,即父容器中剩余可用空间大小,这种布局效果和使用match_parent完全一致。解决这个问题的方法只需要在wrap_content时指定一个内部默认的宽、高值即可,这个值可以灵活指定。TextView、ImageView针对wrap_content情形在onMeasure方法中均做了特殊处理。下面是示例

 

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历调用子View的measure方法,各个子View再去递归执行这个过程。和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的过程中,会遍历所有的子View,并调用measureChid方法,接下来我们看一下这个方法:

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

这个方法的意思就是获取子View的LayoutParams,然后再通过getChildMeasureSpec方法来创建子元素的MeasureSpec,接着讲MeasureSpec直接传递给子View的Measure方法来进行测量。

ViewGroup没有直接定义测量的具体过程,这是因为每一个ViewGroup都有其特有的测量过程。接下来我们分析一下LinearLayout的Measure的过程。

LinearLayout的onMeasure方法

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

这个方法就是以布局方向将测量过程分离开。我们看一下纵向布局下的测量过程measureVertical方法的主要逻辑:

for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            ...省略处处理了child为null,为GONE等几种情况的测量
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // 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();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

在当前的heightMode为AT_MOST的情况下。会对每一个子View执行measureChildBeforeLayout方法,这个方法内部会调用子View的measure方法,这样每个子元素就开始依次进入measure过程。并且会通过mTotalLength这个变量来存储LinearLayout在竖直方向的高度,每测量一个子View,mTotalLength就会增加。当子元素测量完毕之后,LinearLayout会测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程有所不同,具体来说,如果它的布局中高度采用的是match_parent或者具体数值,那么它的测量过程和View一致,即高度为specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度的总和,但是仍然不能超过父容器的剩余空间,同时还要考虑在竖直方向的padding。

measure完成后,通过getMesuredWidth/Height方法就可以获取View的测量宽\高。

 

二、Layout过程

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。先看一下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);

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

layout大致流程:首相会通过setFrame方法设置View的四个顶点的位置,即初始化mLeft、mRight、mTop、mBotton这四个值,View的这四个顶点一旦确定下来,那么View在父容器中的位置也就确定下来了。接着会调用onLayout方法,这个方法的用途是父容器确定子View的位置。

在View中,onLayout方法为空方法,因为View是没有子View的。

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

ViewGroup中layout方法重点也是调用了父类的layout方法

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

ViewGroup中的onLayout是一个抽象的方法,迫使其子类必须实现这个方法。

    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

接下来我们重点看一下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);
        }
    }

我们还是看竖直布局的layoutVertical中的重点部分

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

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

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

 这个逻辑主要是遍历所有子View,根据你当前的显示情况,Gravity设置情况等计算子View的位置,然后调用setChildFrame方法来为子View指定对应的位置。其中childTop会逐渐增大,也就是下面的子View会方法更考下的位置。看一下setChildFrame方法:

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

这个只是调用了子View的layout方法。如果这个child是一个View,那么通过layout的方法就可以设置好它的位置,如果它是一个ViewGroup就会继续调用onLayout设置其子View的位置。这样一层层传递下去就可以完成整个View树的layout过程。最终将位置信息保存成当前View的成员信息。

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

 

draw过程

draw过程是一个比较简单的过程,主要作用就是将View绘制到屏幕上。

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

以上是源码中的注解,View的绘制过程主要遵循以下步骤:

1.绘制背景。由drawBackground方法完成,如果未设定Drawable对象,则会直接返回。

2.绘制自身内容。由onDraw方法完成。

    protected void onDraw(Canvas canvas) {
    }

View的onDraw是空方法,所以自定义的view都要实现这个方法。

3.绘制子视图。由dispatchDraw方法完成。View的dispatchDraw是空方法,ViewGroup的dispatchDraw方法有具体的实现,主要是调用子视图的draw方法,这样只要最顶层的View调用了draw方法,底层的所有View的draw方法都可以被调用。下面是dispatchDraw的一部分代码,主要是遍历了所有的子View,并通过drawChild方法来调用子View的draw方法。

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

            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);
            }
        }
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

4.绘制装饰。主要是foreground和滚动条。

 

此文仅为学习笔记

参考:《Android开发艺术探索》

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值