Android View(四)——View的工作原理

一.初识ViewRoot 和 DecorView

  • DecorView
    DecorView是整个Window界面的最顶层View。DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。
  • ViewRoot
    ViewRoot是连接WIndowManager和DecorVIew的纽带。View的三大流程(measure layout draw)都是通过ViewRoot完成的。在ActivityThread中,Activity创建后,DecorView会被添加到Window中,同时创建ViewRootImpl,然后将DecorView与ViewRootImpl建立关联(通过ViewRoot的setView方法)。
     

一张图看明白ViewRoot 和 DecorView

在这里插入图片描述

二.View的工作流程概述

View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来

  • measure方法用于测量View的宽高
  • layout方法用于确定View在父容器中的位置
  • draw方法负责把View绘制在屏幕上

大致流程图如下:

在这里插入图片描述
理解:
      
performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。
      
同理,performLayout 和 performDraw 的传递流程和 performMeasure 是类似的,唯一不同的是,performDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

View绘制过程中的一些方法介绍

方法 作用 被赋值的时间
getMeasuredWidth/getMeasureHeight 获取View测量后的宽/高 Measure完成以后
getTop/getBottom/getLeft/getRight VIew四个顶点的坐标 layout完成以后
getWidth/getHeight 拿到View的最终宽/高 layout完成之后

三.理解MeasureSpec

1.MeasureSpec是干什么的?

MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说很大程度上是因为这个过程还受父容器的影响。在测量过程中,系统会将View的LayoutParams根据父容器所施加的转换规则转换为对应的MeasureSpec,然后再根据这个MeasureSprc来测量出View的宽/高。

2.MeasureSpec的定义

MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)。

//MeasureSpec源码——部分重点代码
		private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
      	
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
       
        public static final int EXACTLY     = 1 << MODE_SHIFT;
		
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //将 size和mode打包成一个MeasureSpec
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
    
    
            if (sUseBrokenMakeMeasureSpec) {
    
    
                return size + mode;
            } else {
    
    
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
       //MeasureSpec解包出mode
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
    
    
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        //MeasureSpec解包出size
        public static int getSize(int measureSpec) {
    
    
            return (measureSpec & ~MODE_MASK);
        }       

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存分配,为了方便操作,其提供了打包和解包方法。
      
SpecMode有三类:

类别 说明 使用时机
UNSPECIFIED 父容器不对View有任何限制,要多大给到大 这种情况一般用于系统内部,表示一直测量出状态
EXACTLY 父容器已经检测出View的精确的大小,这时候View的最终大小就是SpecSize确定的值 对应LayoutParams中的match_parent和具体数值这两种模式
AT_MOST 父容器指定一个可用大小即SpecSize,View的大小不能大于这个值 对应LayoutParent中的wrap_content

      

3.MeasureSpec和LayoutParams的对应关系

在View测量的时候,系统会将LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽\高。
      
另外,对于顶级View(DecorView)和普通View来说,MeaSureSpec的转换过程略有不同。

3.1 顶级View(DecorView)

对于DecorView,其MeasureSpec由窗口的尺寸和自身的LayoutParams来共同决定。
      
源码分析:

/*
ViewRootImpl.measureHierarchy其中的一段代码
	这段代码展示了DecorView的MeasureSpec创建过程
	desiredWindowWidth,desiredWindowHeight是屏幕尺寸
*/
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

/*
ViewRootImpl.getRootMeasureSpec源码
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    
    
        int measureSpec;
        switch (rootDimension) {
    
    

        case ViewGroup.LayoutParams.MATCH_PARENT: //精确模式,大小就是窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT: //最大模式,大小不定,但不能超酷窗口大小
           
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default: //精确模式,大小为LayoutParams中指定的大小
           
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

3.2 普通View

View的measure过程由ViewGroup传递而来
      
源码分析:

/*
ViewGroup.measureChildWithMargins方法
改方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec
方法得到子元素的MeasureSpec
*/
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
    
    
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
		/*
		获得子元素的MeasureSpec,可以看出子元素MeasureSpec的确定不止和父元素的MeasureSpec,
		自身的LayoutParams有关,还和View的margin和padding有关
		
		*/
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

/*
ViewGroup.getChildMeasureSpec方法
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    
    
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec); //父容器的specSize
		/*
		padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的
		尺寸减去padding
		*/
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
    
    	//父容器的specMode
       //可以看出父容器的specMode不同,结果不同
        case MeasureSpec.EXACTLY:	
            if (childDimension >= 0) {
    
    		//childLayoutParams是具体数值,dp/px
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
    
    //childLayoutParams是MATCH_PARENT
               
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
    
    //childLayoutParams是WRAP_CONTENT
                
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

       
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
    
    
            
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
    
    
              
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
    
    
               
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
    
    
               
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
    
    
               
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
    
    
               
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
       
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

将上述代码转换为表:
在这里插入图片描述
说明:

  • 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View都采用精确模式(EXACTLY),大小遵循Layoutparams中的大小。
  • 当View的宽高是match_parent时,如果父容器是精准模式,那么View也是精准模式并且其大小时父容器的剩余空间。
  • 如果父容器是最大模式(AT_MOST),那么View也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当View的宽高是wrap_content时,不管父容器的模式是最大还是精准,View的模式总是最大并且其大小不会超过父容器的剩余空间。

四.View的工作流程和源码分析

View的三大流程如下

  • measure:测量,决定了View的宽度和高度
  • layout:布局,决定了View最终的宽高及四个顶点的位置
  • draw:绘制,将View绘制到屏幕上

1.measure过程

measure确定Veiw的测量宽/高。普通的View通过measure方法后就完成了它的测量过程,而ViewGroup除了自己的测量过程外,还会遍历所有子元素的measure方法,子元素再递归执行,才能完成

      

1.1 View的measure过程

View的measure过程由measure方法来完成,measure方法是一个final方法,不能重写,它会调用VIew的onMeasure方法。onMeasure方法中会调用getDefaultSize方法,而getDefault方法中又会调用getSuggestedWidth和getSuggestedHeight方法。

/*
View.onMeasure
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
// setMeasuredDimension会设置宽/高的测量值
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

/*
 View.getDefaultSize方法
 //getDefaultSize方法所返回的就是测量后的View的大小。
*/
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;
    }

我们接着看到getSuggestedWidth和getSuggestedHeight方法

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

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

它在没有指定background的情况下,返回的是minSize这一属性对应的值,而在指定了背景的情况下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法对应的值

这两个方法在Drawable有原始宽度的情况下返回原始宽度,否则返回0

总结:从getDefaultSize方法可以看出,View的宽高由specSize决定。
      
问题:如果View在布局中使用Wrap_content,那么他的specMode就是AT_MOST,specSize就是parentSize(父容量中目前可使用的大小),而此时View的宽高由specSize决定。所以View的宽高就等于父容量中目前可使用的大小,效果和match_parent完全一致
      
解决:直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,具体的可参考下面的代码

/*
给View指定一个默认宽高。在wrap_content时设置此宽高即可。而非wrap_content我们只需要沿用系统宽高即可。
*/
@Override
    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){
    
    	//在wrap_content时设置此宽高
            setMeasuredDimension(mWidth, mHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
    
    
            setMeasuredDimension(mWidth, heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
    
    
            setMeasuredDimension(widthSpecSize, mHeight);
        }

    }

1.2 ViewGroup的measure过程

ViewGroup除了完成自己的measure过程,还会遍历调用子元素的measure方法,然后子元素再次递归执行,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时,会遍历子元素,调用measureChild方法对子元素进行measure。

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    
    
            //取出子元素的LayoutParams
        final LayoutParams lp = child.getLayoutParams();
		/*通过getChildMeasureSpec方法创建子元素MeasureSpec
		这个方法上面有过分析
		*/
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
		//传递给View的measure方法进行测量。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

ViewGroup没有定义测量具体过程,因为它是个抽象类。具体的测量过程的onMeasure方法需要子类来实现。以LinearLayout为例:

LinearLayout.onMeasure
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        if (mOrientation == VERTICAL) {
    
    
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
    
    
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
//选择measureVertical继续分析
//LinearLayout.measureVertical
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    
    
      ......
        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
    
    
            final View child = getVirtualChildAt(i);
            .......
            //这个方法内部会调用子元素法measure方法,对子元素进行measure过程
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
    
    
                   
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                //mTotalLength存储LinearLayout初步高度。每测量一个子元素,mTotalLength就会增加。
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

             

          ........

系统会遍历子元素并对每个子元素调用measureChildBeforeLayout方法。这个方法会调用子元素的measure方法。这样子元素就开始进入measure过程。并且系统会计算子元素的高度并存放在mTotalHeght中
      
子元素测量完毕后,LinearLayout会测量自己的大小:

 // Add in our padding
            mTotalLength += mPaddingLeft + mPaddingRight;
           
        maxHeight += mPaddingTop + mPaddingBottom;

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

        setMeasuredDimension(widthSizeAndState | (childState&MEASURED_STATE_MASK),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        (childState<<MEASURED_HEIGHT_STATE_SHIFT)));

对于竖直的LinearLayout来说,它在水平方向测量过程遵循View的测量过程,而竖直方向,如果在布局中采用match_parent或具体值,则和View的测量过程一致,如果采用的是wrap_content,则它的高度是所以子元素占用的高度的和。但仍然不会超过它的父容器的剩余空间。它的最终高度还需要考虑它在竖直方向的padding。
      
进一步看源码:

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

View的measure过程是十分复杂的,在measure完成后,通过getMeasuredWidth或getMeasuredHeight方法即可正确获得View的测量宽/高

注意,在某些极端情况下,系统可能需要多次measure才能拿到最终的测量宽高。此时的测量结果是不准确的。因此最好在onLayout中去获取View的测量宽高/最终宽高

1.3 onCreate等方法中无法获取正确宽高的解决方法

  1. Activity/View 的 onWindowFocusChanged
    onWindowFocusChanged中,VIew已经初始化完毕了,此时获取宽高是没问题的。但需要注意的是,onWindowFocusChanged会被调用多次。当Activity窗口得到焦点和失去焦点时都会被调用。如果频繁进行onResume和onPause,则它会被频繁调用
    代码:
@Override
    public void onWindowFocusChanged(boolean hasFocus) {
    
    
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
    
    
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight()}
    }
  1. view.post(runnable)
    通过post可以将runnable投递到消息队列的尾部,然后等待Looper调用runnable的时候,View也初始化好了。典型代码如下。
@Override
    protected void onStart() {
    
    
        super.onStart();
        view.post(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }

  1. ViewTreeObserer
    用ViewTreeObserver的回调可以完成这个功能,如OnGlobalLayoutListener接口。当View树的状态改变或者内部的View可见性改变时,它都会被回调。这是获取View的宽高的很好的时机。需要注意的是,随着View树的状态改变等,它会被调用多次
@Override
    protected void onStart() {
    
    
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    
    
            @Override
            public void onGlobalLayout() {
    
    
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width= view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }
  1. view.measure(int widthMeasureSpec, int heightMeasureSpec)
    通过手动对View进行measure来得到宽高,过程太繁琐,这里不展示

2.layout过程

Layout的作用是ViewGroup来确定子元素的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子元素并调用其layout发布方法

我们首先看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;
		/*
		setFrame:设定View四个顶点的位置(初始化mLeft,mRight,mTop,mBottom)
		*/
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    
    
        //父容器确定子元素的位置,但View和ViewGroup中均没有真正实现。因为其具体实现和布局有关
            onLayout(changed, l, t, r, b);    

首先,通过setFrame方法设定View四个顶点的位置(初始化mLeft,mRight,mTop,mBottom)。四个顶点一旦确定,则在父容器中的位置也确定了,接着便会调用onLayout方法,来让父容器确定子容器的位置。onLayout同样和具体布局有关,因此View和ViewGroup均没有实现onLayout方法。
      
我们借助LinearLayout的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.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);
            }
        }
    }

它会遍历所有子元素并调用setChildFrame来为子元素指定对应位置。其中childTop会逐渐增大,也就是后面的子元素会放置在靠下的位置。至于setChildFrame,它仅仅调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定位后,通过onLayout方法调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置。这样就完成了View树的layout过程。

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

根据layoutVertical的代码可以看出,这里的width和height就是子元素的测量宽高。而在layout方法中会通过setFrame来设置子元素四个顶点位置。在setFrame方法中有如下几句赋值语句,就确定了子元素的位置

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

View的测量宽\高和最终宽\高有什么区别

即getWidth()( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?

看一下他们的源码

// 获得View测量的宽 / 高
  public final int getMeasuredWidth() {
    
      
      return mMeasuredWidth & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredWidth
  }  

  public final int getMeasuredHeight() {
    
      
      return mMeasuredHeight & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredHeight
  }  

// 获得View最终的宽 / 高
  public final int getWidth() {
    
      
      return mRight - mLeft;  
      // View最终的宽 = 子View的右边界 - 子view的左边界。
  } 

  public final int getHeight() {
    
      
      return mBottom - mTop;  
     // View最终的高 = 子View的下边界 - 子view的上边界。
  }   

由于之前的mLeft、mRight都是由Measure过程赋值过来来看,它的返回值其实就是测量宽度。不过测量宽高形成在Measure过程,实际宽高形成于Layout过程。
      
在日常开发中,我们可以认为View的测量宽高与实际宽高是相等的。
      
但是如果我们故意重写了onLayout方法,可能就会导致两个宽高不等
      

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

3.draw过程

draw过程是将View绘制到屏幕上,它遵循下面几步:

  1. 绘制背景 background.draw(canvas);
  2. 绘制自己 onDraw
  3. 绘制children(dispathDraw)
  4. 绘制装饰(onDrawScrollBars)

View绘制过程的传递是通过dispatchDraw实现的。dispatchDraw会遍历调用所有子元素的draw方法。这样draw事件就一层层传递了下来。它有个比较特殊的setWillNotDraw方法。
      
源码如下:

/
public void setWillNotDraw(boolean willNotDraw) {
    
    

    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);

}


如果一个View不需要绘制任何内容,在我们设定这个标记为true后,系统就会对其进行相应优化。一般View没有启用这个标记位。但ViewGroup是默认启用的。

应用场景:

  • setWillNotDraw参数设置为true:当自定义View继承自 ViewGroup 、且本身并不具备任何绘制时,设置为 true 后,系统会进行相应的优化。
  • setWillNotDraw参数设置为false:当自定义View继承自 ViewGroup 、且需要绘制内容时,那么设置为 false,来关闭 WILL_NOT_DRAW 这个标记位。

猜你喜欢

转载自blog.csdn.net/haazzz/article/details/114549369