View的工作原理——三大流程(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sliverbullets/article/details/81627621

View的工作流程

View的工作流程主要指:measure、layout、draw这三大流程。
这里写图片描述
View的绘制是从上往下一层层迭代下来的。DecorView–>ViewGroup(—>ViewGroup)–>View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。
这里写图片描述
为方便笔记,上面参考于此链接

Measure过程

一.View的measure过程

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

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

可以看出上述代码,通过setMeasuredDimension在设置View的宽/高,它的参数又调用了getDefauSize方法,所以我们看getDefauSize这个方法:

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

1.可以看出,它是根据measureSpec分解出specSize和specMode两个值,但是又看了一下case的值,UNSPECIFIED这个我们前面说过,一般不管,所以我们你只需要看AT_MOST和EXACTLY,然后他们的处理方式一样,返回了specSize的值,而这个specSize就是View测量后的大小。

2.为什么说是View测量后的值?是因为Veiw的最终大小是在layout阶段确定的,所以这里要加以区分。但其实几乎所有情况下View的测量大小和最终大小是相等的。后面会说特殊情况。

3.还有一个结论:直接继承View的自定义控件需要重写onMeasure方法中的warp_content,否则使用wrap_content和使用match_partent的效果是一样的,这个从上面方法和前面的表可以看出。

解决方案:

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);    
    if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){      
        setMeasuredDimension(mWidth,mHeight);   
    }else if(widthSpecMode == MeasureSpec.AT_MOST){     
        setMeasuredDimension(mWidth,heightSpecSize);
    }else if(heightSpecMode == MeasureSpec.AT_MOST){    
        setMeasuredDimension(widthSpecSize,mHeight);    
    }
}

在上面代码中,我们只需要在View指定为warp_content时设置一个默认内部宽/高(mWidth,mHeight)。至于这个默认宽/高大小如何指定,没有固定的依据,需要灵活指定。可以查看TextView和ImageView源码查看它们的解决方式。由于getDefaultSize的参数又是getSuggestedMinimumWidth()方法返回的,所以看一下这个方法:

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

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

很好理解,这里就是看View是否有设置Background,如果设置了,就选mMinHeight和 mBackground.getMinimumHeight()中最大的那个值返回,mMinHeight其实就是android:minHeight这个属性所指的值,如果不指定该属性,则默认为0。
*举个场景例子:当时写计算器的时候,给Button设置背景后导致Button之间默认的空隙没有了,注意是默认的,自己设置的不影响。*
而后者getMinimHeight这个方法又返回的是什么呢?看下面代码:

public int getMinimumHeight() {        
    final int intrinsicHeight = getIntrinsicHeight();        
    return intrinsicHeight > 0 ? intrinsicHeight : 0;   
}

可以看出,如果intrinsicHeight > 0就返回intrinsicHeight,否则返回0,intrinsicHeight就是Drawable的原始高度,前提是这个Drawable有原始高度。那么什么时候会有原始高度呢?如:ShapeDrawable就没有原始宽/高,BitmapDrawable就有原始宽/高。
还有一点需要注意:getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPCIFIED情况下的测量宽/高。

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

可以看出,它先取出了子元素的Layoutparams,之前说过的LayoutParams会影响MS的创建,然后通过getChildmeasureSpec来创建子元素的MeasureSpec,接着将MS直接传递给了View的measure方法。由于ViewGroup是一个抽象类,所以并没有定义其测量的具体过程,其测量过程的onMeasure需要各个子类去具体实现,比如LinearLayout、RelativeLayout这两者的布局显著不同,因此ViewGroup无法做统一实现。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程:

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

上面代码很简单,根据LinearLayout的Orientation属性来调用不同的测量过程,比如竖直布局的大概逻辑:

//部分代码
 // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            nonSkippedChildCount++;
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

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

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

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            /**
             * If applicable, compute the additional offset to the child's baseline
             * we'll need later when asked {@link #getBaseline}.
             */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

从上面这段代码可以看出来,系统会遍历子元素并对子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法(它内部调用measureChildWithMargins方法,这个方法会调用measure方法),这样各个子元素就进入了measure过程,并且系统通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度,每测量一个子元素,mTotallLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,如下:

  // Add in our padding        
  mTotalLength += mPaddingLeft + mPaddingRight;        
  int widthSize = mTotalLength;        
  // Check against our minimum width        
  widthSize = Math.max(widthSize, getSuggestedMinimumWidth());        
  // Reconcile our calculated size with the widthMeasureSpec        
  int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);        
  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),                heightSizeAndState);

上面代码可以看出,当子View测量完毕后,LinearLayout会根据子元素的情况来测量自己,对于竖直的LinearLayout来说,他在水平方向的测量遵守Veiw的测量,在竖直方向,如果他的布局中高度采用的是match_parent或者具体数值则遵守View的测量即精准模式,高度为specSize;但如果它的布局采用的格式warp_content,则它的高度就等于所有子元素所占用的高度总和及它在竖直方向的padding,但是仍然不能超过他的父容器的剩余空间。这个过程如下:

 //这个size是 maxHeight/maxWidth,即最大高/宽  
  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);
    }

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

一种情况:我们想在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽/高。

错误想法:
在onCreate、onResume里面获取某个View的宽/高事实上,在onCreate、onStart、onResume中均不能获取到正确的值,因为View的measure过程和Activity的生命周期不是同步的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了。

解决方法:
①Activity/View#onWindowChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了。注意:这个方法会被调用好多次,当Activity的窗口得到、失去焦点的时候均会被调用一次。即Acitvity继续执行或暂停执行都会调用这个方法。

典型代码:

public void onWindowFocusChanged(boolean hasFocus){
    super.onWidowFocusChanged(hasFocus);    
    if(hasFocus){       
        int width = view.getMeasureWidth();     
        int height = view.getmeasureHeight();   
     }
}

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

protected void onStart(){   
    super.onStart();    
    view.post(new Runnable(){       
        @Ovrride        
        public void run(){          
        int width = view.getMeasureWidth();         
        int height = view.getMeasureHeight();       
        }   
    });
}

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

protected void onStart(){   
    super.onStart();    
    ViewTreeObserver observer = view.getViewTreeObserver();    
    observer.addOnGloballLayoutListener(new OnGloballLayoutListener(){      
        @SuppreverWarnings("deprecation")        
        @Ovrride        
        public void onGloballLayout(){          
            view.getViewTreeObserver().removeGloballOnLayoutListener(this);
            int width = view.getMeasureWidth();         
            int height = view.getMeasureHeight();       
        }   
    });
}

④view.measure(int widthMeasureSpec,int heightMeasureSpec)通过手动对View进行measure来得到View的宽/高,这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
Ⅰ. match_parent由于无法measure出具体的宽/高,所以不可用。根据上面的表图知道,当为match_parent时要知道父容器的剩余空间parentSize,而这个时间我们无法知道parentSize,故不可取。
Ⅱ.具体的数值(dp/px)比如宽/高都是100px,如下,measure:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

Ⅲ.wrap_content如下measure:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1<< 30) -1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeaasureSpec( (1<< 30) -1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec); 

这里(1<< 30)-1就是MS的specSize的那30位二进制,其大小为2^30-1,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。关于view measure两种错误用法:

//第一种:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1,MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec,heightMeasureSpec);
//第二种
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 

首先违背了系统内部的实现规范(无法通过错误的MS去得出合法的SpecMode,从而导致measure过程出错,对比正确用法),其次不能保证一定能measure出正确的结果。

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

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

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

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

layout流程:首先会通过setFrame(setOpticalFrame是对子元素的)方法来设定Veiw的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,Veiw的四个顶点一旦确定,那么Veiw在父容器的位置也就确定了;接着会调用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);
        }
    }

然后我们看竖直方向的:

void layoutVertical(int left, int top, int right, int bottom) {
        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;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               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) {
                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);
            }
        }
    }

可以看到,此方法会便利所有的子元素并调用setChildFrame方法(它仅调用了子元素的layout方法)来为子元素指定对应的位置,其中childTop会逐渐增大,就是说遍历过程中后面的元素会在前一个元素的下面,这符合LinearLayout竖直的属性。父容器在layout方法中完成自己的定位后,就通过onLayout方法去调用子元素的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);
}

从上面可以得到,子元素的测量宽/高是width,height,从下面代码可以看出:

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

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

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

下面我们来回答一下之前我们的一个问题:View的测量宽/高和最终宽/高有什么区别?
这个问题可以具体为getMeasuredWidth和getWidth这两个方法有什么区别?
为什么说是这两种方法因为上面的setChildFrame方法用的宽就是getMeasuredWidth这个方法返回的值,而getWidth我们看源码就知道了。
我们来看一下getWidth的实现:

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

我们知道mRight,mLeft是View的四顶点位置中的,所以这个方法返回的值刚好是View的宽度。
我们现在来回答一下这个问题:
在View默认实现中(也就是调用setChildFrame方法而这个方法中的参数又调用getMeasuredWidth作为child的width),View的测量宽/高和最终宽/高(getWidth)是相等的,只不过形成的时机不一样,前者在View的measure过程,后者在View的layout过程。我们日常开发中,可以认为这两者是相等的,但总是有一些特殊情况会导致两者不相等。
如:①重写View的layout,如下

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

这样会导致最终值比测量值的宽高总大100px。虽然无意义,但证明这两者可以不相等。
②在某些情况下,View会多次测量,测量值可能不同,最终宽/高和前几次测量值会不同,但最终测量值和最终宽/高相等。

draw过程
它的作用是将View绘制到屏幕上面。
View的绘制过程如下:
①绘制背景backgrund.draw(canvas)
②绘制自己(onDraw)
③绘制children(dispatchDraw)
④绘制装饰(onDrawScroBars)

这个步骤通过源码也可以看出来:
虽然比较长,但真的好好看不难理解。

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

        /*
         * 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) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } 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的绘制过程的传递是通过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);
    }

从注释可以看出来 翻译:“如果该视图本身不进行任何绘图,则设置此标志以允许进一步优化。默认情况下,此标志不会在视图上设置,但可以在ViewGroup等视图子类上设置。通常,如果您重写{@link #onDraw(android.graphics.Canvas)},您应该清除这个标志”。
ViewGroup默认启用这个标志,这个标志对我们开发者的意义在于:当我们的自定义控件继承于ViewGroup并且其自身不具备绘制功能时,就开启这个标志位从而便于系统进行后期的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时我们要显示关闭这个标志位。

猜你喜欢

转载自blog.csdn.net/sliverbullets/article/details/81627621