View的工作原理

来源:互联网 发布:jersey json参数 编辑:程序博客网 时间:2024/04/27 17:11

1、初识ViewRoot和DecorView

在正式介绍View的三大流程之前,我们必须先介绍一些基本概念,这样才能更好地理解View的Measuer、layout和draw的过程,本节主要介绍ViewRoot和DecorView的概念。

ViewRoot对应于ViewRootImpl类,它是链接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程可参看如下代码:

root = new ViewRootImpl(view.getContext(), display);root.setView(view, wparams, panelParentView);

view的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw负责将view绘制在屏幕上。针对performTraversals的大致流程,可用下图来表示:
这里写图片描述
如图,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有本质的区别。
measure过程决定了View的宽和高,measure完成以后,可以通过getMeasureWdith和getMeasureHeight方法来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高,但是特殊情况除外,这点在后面会说明。Layout过程决定了View的四个顶点的坐标和实际的View的宽/高,完成以后,可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到View的最终宽/高。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

2、理解MeasureSpec

为了更好的理解View的测量过程,我们还需要理解MeasureSpec,从名字上来看MeasureSpec看起来像“测量规格”或者“测量说明书”,不管怎么翻译,他看起来都好像是或多或少的决定了View的测量过程。通过源码可以发现,MeasureSpec的确参与了View的measure过程。那MeasureSpec到底是干什么的呢?确切来说MeasureSpec很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中能,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽/高。上面提到过这里的宽/高是测量的宽/高,不一定等于View最终额宽/高。MeasureSpec看起来有点复杂,其实他的实现是很简单的,线面细细的分析MeasureSpec。

2.1MeasureSpec

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode指的是测量模式,而SpecSize指的是在某种测量模式下的规格大小。
SpecMode有三类,每一类都表示特殊的含义,如下所示:

UNSPECIFIED 父容器不对View有任何的限制,要多大有多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。他对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST 父容器指定了一个可用大小,即SpecSize,View的大小不能大于这个值,具体是什么值,要看不同View的具体实现。他对应于LayoutParams中的wrap_content。

2.2 MeasureSpec和LayoutParams的对应关系

上面提到,系统内部是通过MeasureSpec来进行View的测量,但是在恒昌情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。另外,对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定的,MeasureSpec一旦确定后,onMeasure中就可以确定VIew的测量宽/高。
需要说明的是,当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式,并且其大小遵循LayoutParams中的大小。当View的宽/高是match_parent时,如果父容器是精准模式,那么View也是精准模式并且其大小就是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准模式还是最大化,VIew的模式总是最大化并且大小不能超过父容器的剩余空间。
UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般来说我们不需要关注此模式。

只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速的确定出子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。

这里写图片描述

3、 View的工作流程

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

3.1 measure过程

3.1.1View的measure过程

View的measure过程由其measure方法来wanchengmeasure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中回去调用View的onMeasure方法,因此只需要看onMeasure的实现即可,View的onMeasure方法如下所示。

protected void onMeasure(int widthMesaureSpec, int heightMeasureSpec) {    setMeasureDimension(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这个方法逻辑很简单,对于我们来说,我们只需要看AT_MOST和EXACTLY这两种情况。简单的理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要多次提到测量几乎所有情况下VIew的测量大小和最终确定大小是相等的。
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为getSuggestedMinimumWidth和getSuggestedMiniMumHeight这两个方法的返回值,看一下他们的源码:

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

这里我们只分析getSuggestedMinimumWidth方法的实现,getSuggestedMinimumHeight和它的实现原理是一样的。从getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为androi:minWidth属性所指定的值。这个属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth())。mMinWidth的含义我们已经知道了,那么mBackground.getMinimumWidth()是什么呢?我们看一下Drawable的getMinimumWidth方法,如下所示:

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

可以看出getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则返回为0.那么Drawable在什么情况下有原始宽度呢?这里先举个例子说明一下,ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高。
再总结一下上面的逻辑:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽/高。

从getDefaultSize方法的实现来看,View的宽/高由SpecSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在不居中使用wrap_content就相当于使用match_parent。为社么呢?
从上述代码中我们知道,如果View在布局中使用wrap_content,那么他的specMode是AT_MOST模式,在这种情况下,他的宽/高等于specSize,这种情况下View的specSize是parentSize,而parentSize是父容器中当前剩余空间大小,这种效果和在布局中使用match_parent完全一致,如何解决这个问题呢?也很简单,代码如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);    if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {        setMeasuredDimenssion(mWidth, mHeight);    } else if(widthSpecMode == MeasureSpec.AT_MOST) {        setMeasuredDimenssion(mWdith, heightSpecSize);    } else if(heightSpecMode == MeasureSpec.AT_MOST) {        setMeasuredDimenssion(widthSpecSize, mHeight);    }}

在上面代码中,我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高即可。对于非wrap_content情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果看TextView、ImageView等的源码就可以知道,针对wrap_content情形,他们的onMeasure放大均做了特殊处理。

3.1.2 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,然后再通过getChildMeasureSpec来创建资子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getChildMeasureSpec的工作过程已经在上面进行了详细分析。

我们知道ViewGroup没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程onMeasure方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法进行统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致他们在测量细节各不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因此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);        }    }

上述代码很简单,我们选择一个来看一下,比如选择查看竖直布局的LinearLayout的测量过程,即measureVertical方法,mewsureVertical的源码比较长,下面只描述其大概逻辑,首先看一段代码:

// 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).                measureChildBeforeLayout(                       child, i, widthMeasureSpec, 0, heightMeasureSpec,                       totalWeight == 0 ? mTotalLength : 0);                if (oldHeight != Integer.MIN_VALUE) {                   lp.height = oldHeight;                }                final int childHeight = child.getMeasuredHeight();                final int totalLength = mTotalLength;                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +                       lp.bottomMargin + getNextLocationOffset(child));          }

从上面这段代码可以看出,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等,当子元素测量完毕后,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会根据子元素的情况来测量自己的大小,针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向上的测量则和View的测量有所不同。具体来说是指,如果他的布局中高度采用的是match_parent或者具体数值,那么他的测量过程和View一致,即高度为specSize;如果他的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,当然它的最终高度还需要考虑其在竖直方向的padding,这个过程可以进一步参看如下代码:

/**     * Utility to reconcile a desired size and state, with constraints imposed     * by a MeasureSpec.  Will take the desired size, unless a different size     * is imposed by the constraints.  The returned value is a compound integer,     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting     * size is smaller than the size the view wants to be.     *     * @param size How big the view wants to be     * @param measureSpec Constraints imposed by the parent     * @return Size information bit mask as defined by     * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.     */    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {        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:            if (specSize < size) {                result = specSize | MEASURED_STATE_TOO_SMALL;            } else {                result = size;            }            break;        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result | (childMeasuredState&MEASURED_STATE_MASK);    }

View的measure过程是三大流程中最复杂的一个,measure完成以后通过getMeasuredWidth/Height方法可以正确的获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情况下,在onMeasure方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

上面已经对View的measure过程进行了详细的分析,现在考虑一种情况,比如我们想在Activity刚启动的时候就做一件任务,但是这一件任务需要获取某个VIew的宽/高。而实际上在onCreate、onStart、onResume中均无法正确得到某个VIew的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽/高就是0。那有没有什么办法能解决这个问题呢?必须有啊!下面给出四种解决方案:
(1) Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没有问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁的进行onResume和onPause,那么onWindowFocusChanged也会被频繁的调用。典型的获取宽高代码如下:

 @Override public void onWindowFocusChanged(boolean hasFocus) {     super.onWindowFocusChanged(hasFocus);     if (hasFocus) {         int width = view.getMeasuredWidth();         int height = view.getMeasuredHeight();     } }

(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用次runnable的时候,View也已经初始化好了。典型的代码如下:

    protected void onStart() {        super.onStart();        view.post(new Runnable() {            @Override            public void run() {                 int width = view.getMeasuredWidth();                 int height = view.getMeasuredHeight();            }        });    }

(3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListerner这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽/高一个很好的时机。需要注意的是,伴随着View树状态改变等,onGlobalLayout会被调用多次。典型代码如下:

    protected void onStart() {        super.onStart();        ViewTreeObserver observer = view.getViewTrssObserver();        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {            @Override            public void onGlobalLayout() {                      view.getViewTrssObserver().removeGlobalOnLayoutLIstener(this);                 int width = view.getMeasuredWidth();                 int height = view.getMeasuredHeight();            }        });    }

(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
match_parent:
直接放弃,无法measure出具体的宽/高。原因很简单,根据View的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
具体的数值(dp/px)

。。。

比较复杂,不推荐使用

3.2 layout过程

layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,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);            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的四个顶点的位置,即初始化mLeft、mRight、mTop、mBottom这四个值,View的四个顶点一旦确定,那么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);        }    }

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

这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放在靠下的位置,这刚好符合竖直方向的LinearLayout的特性。至于setChildFrame,它仅仅是调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定为以后就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层的传递下去就完成了整个View树的layout过程。
setChildFrame方法的实现如下:

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

我们注意到,setChildFrame中的width和height实际上就是子元素的测量宽/高,从下面的代码可以看出这一点:

    final int childWidth = child.getMeasuredWidth();    final int childHeight = child.getMeasuredHeight();    setChildFrame(child, childLeft, childTop + getLocationOffset(child),                        childWidth, childHeight);

而在layout方法中会通过setFrame去设置子元素的四个顶点的位置,在setFrame中有如下几句赋值语句,这样一来子元素的位置就确定了:

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

那现在我们看下View的测量宽/高和最终宽/高有什么区别?这个问题可以具体为:View的getMeasuredWidth和getWidth这两个方法有什么区别,为了回答这个问题,我们首先看下getWidth方法的具体实现:

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

从getWidth的源码再结合mLeft、mRight、mTop、mBottom这四个变量的赋值过程来看,getWidth方法返回值刚好就是View的测量宽度,经上述分析,我们可以回答这个问题了:在View的默认实现中,View的测量宽/高和最终的宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终的宽/高形成于VIew的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些,因此,在日常开发中,我们可以认为View的测量宽/高就等于最终的宽/高,但是确实存在某些特殊情况不一致,下面举例说明:

public void layout(int l, int t, intr, int b) {    super.layout(l, t, t+100, b+100);}

上述代码会导致在任何情况下View的最终宽/高总是比测量宽/高大100px,尽管这样写没有什么实际意义。

3.3 draw过程

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

  • 绘制背景background(canvas)
  • 绘制自己(onDraw)
  • 绘制children(dispatchDraw)
  • 绘制装饰(onDrawScrollBars)

这一点通过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);            // Step 6, draw decorations (scrollbars)            onDrawScrollBars(canvas);            if (mOverlay != null && !mOverlay.isEmpty()) {                mOverlay.getOverlayView().dispatchDraw(canvas);            }            // we're done...            return;        }        ...   }

View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有的子元素的draw方法,如此draw事件就一层一层的传递下去。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);    }

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

0 0
原创粉丝点击