Android中View测量、布局及绘制原理

来源:互联网 发布:网络海豚壁纸图片 编辑:程序博客网 时间:2024/05/15 23:13

一、View绘制的流程框架

这里写图片描述
View的绘制是从上往下一层层迭代下来的。DecorView–>ViewGroup(—>ViewGroup)–>View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。 
这里写图片描述

二、Measure流程

顾名思义,就是测量每个控件的大小。

调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用 
setMeasuredDimension()设定View的宽高信息,完成View的测量操作。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {}
  • 1
  • 2
  • 3

measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
  • 1
  • 2
  • 3
  • 4

由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec,heightMeasureSpec这两个参数信息怎么获得?

如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

MeasureSpec的确定

先介绍下什么是MeasureSpec? 
这里写图片描述 
MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。

其中,Mode模式共分为三类

UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值

AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。 
这里写图片描述

我们来看getChildMeasureSpec()的源码分析:

//作用:/ 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec//即子view的确切大小由两方面共同决定:父view的MeasureSpec 和 子view的LayoutParams属性 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {   //参数说明 * @param spec 父view的详细测量值(MeasureSpec)  * @param padding view当前尺寸的的内边距和外边距(padding,margin)  * @param childDimension 子视图的布局参数(宽/高)    //父view的测量模式    int specMode = MeasureSpec.getMode(spec);         //父view的大小    int specSize = MeasureSpec.getSize(spec);         //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)       int size = Math.max(0, specSize - padding);      //子view想要的实际大小和模式(需要计算)      int resultSize = 0;      int resultMode = 0;      //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小      // 当父view的模式为EXACITY时,父view强加给子view确切的值   //一般是父view设置为match_parent或者固定值的ViewGroup     switch (specMode) {      case MeasureSpec.EXACTLY:          // 当子view的LayoutParams>0,即有确切的值          if (childDimension >= 0) {              //子view大小为子自身所赋的值,模式大小为EXACTLY              resultSize = childDimension;              resultMode = MeasureSpec.EXACTLY;          // 当子view的LayoutParams为MATCH_PARENT时(-1)          } else if (childDimension == LayoutParams.MATCH_PARENT) {              //子view大小为父view大小,模式为EXACTLY              resultSize = size;              resultMode = MeasureSpec.EXACTLY;          // 当子view的LayoutParams为WRAP_CONTENT时(-2)              } else if (childDimension == LayoutParams.WRAP_CONTENT) {              //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST              resultSize = size;              resultMode = MeasureSpec.AT_MOST;          }          break;      // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)      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;      // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大    // 多见于ListView、GridView      case MeasureSpec.UNSPECIFIED:          if (childDimension >= 0) {              // 子view大小为子自身所赋的值              resultSize = childDimension;              resultMode = MeasureSpec.EXACTLY;          } else if (childDimension == LayoutParams.MATCH_PARENT) {              // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0              resultSize = 0;              resultMode = MeasureSpec.UNSPECIFIED;          } else if (childDimension == LayoutParams.WRAP_CONTENT) {              // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0              resultSize = 0;              resultMode = MeasureSpec.UNSPECIFIED;          }          break;      }      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87

关于getChildMeasureSpec()里对于子View的测量模式和大小的判断逻辑有点复杂; 

现总结如下表: 
这里写图片描述
规律总结:(以子View为标准,横向观察)

  • 当子View采用具体数值(dp / px)时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;

  • 当子View采用match_parent时,子View的测量模式与父容器的测量模式一致,若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间

  • 当子View采用wrap_parent时,无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。

对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

这里写图片描述

从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。

关于MeasureSpec和View的Measure过程还可以看这篇文章

三、Layout流程

测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

layout()作用:确定View本身的位置,即设置View本身的四个顶点位置 
源码分析如下:(仅贴出关键代码)

public void layout(int l, int t, int r, int b) {      // 当前视图的四个顶点    int oldL = mLeft;      int oldT = mTop;      int oldB = mBottom;      int oldR = mRight;      // setFrame() / setOpticalFrame():确定View的位置    // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回  (具体请看下面源码分析)      boolean changed = isLayoutModeOptical(mParent) ?            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);    //如果视图的大小和位置发生变化,会调用onLayout()    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {          // onLayout():确定该View所有的子View在父容器的位置        // 由于单一View是没有子View的,所以onLayout()是一个空实现(        onLayout(changed, l, t, r, b);    ...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。 setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

protected boolean setFrame(int left, int top, int right, int bottom) {    ...// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点// 即确定了视图的位置    mLeft = left;    mTop = top;    mRight = right;    mBottom = bottom;    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

protected void onLayout(boolean changed, int left, int top, intright, int bottom) {}
  • 1
  • 2
  • 3

如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

由此便完成了一层层的的布局工作。

四、Draw过程

View的绘制过程遵循如下几步:

①绘制背景 background.draw(canvas) 
②绘制自己(onDraw) 
③绘制Children(dispatchDraw) 
④绘制装饰(onDrawScrollBars)

从源码中可以清楚地看出绘制的顺序:

public void draw(Canvas canvas) {// 特别注意:// 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)// 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。// 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。    ...    /*     * 绘制过程如下:     *   1. 绘制view背景     *   2. 绘制view内容     *   3. 绘制子View     *   4. 绘制装饰(渐变框,滑动条等等)     */    int saveCount;    if (!dirtyOpaque) {          // 步骤1: 绘制本身View背景        drawBackground(canvas);    }        // 如果有必要,就保存图层(还有一个复原图层)        // 优化技巧:        // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过        // 因此在绘制的时候,节省 layer 可以提高绘制效率        final int viewFlags = mViewFlags;        if (!verticalEdges && !horizontalEdges) {        if (!dirtyOpaque)              // 步骤2:绘制本身View内容            onDraw(canvas);        //  View 中:默认为空实现        // ViewGroup中:自定义View时需要进行复写!!!!..        // 步骤3:绘制子View        dispatchDraw(canvas);       // 由于单一View没有子View,所以View 中:默认为空实现        ...        // 步骤4:绘制滑动条和前景色等等        onDrawScrollBars(canvas);        // we're done...        return;    }    ...    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

dispatchDraw()作用遍历子View并绘制

// 仅贴出重要代码// 特别注意:// ViewGroup中:由于 系统 已经为我们实现了该方法,所以我们一般都不需要重写该方法// View中默认为空实现(因为没有子View可以去绘制)protected void dispatchDraw(Canvas canvas) {    ......// 遍历子View    final int childrenCount = mChildrenCount;    ......    for (int i = 0; i < childrenCount; i++) {            ......            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||                    transientChild.getAnimation() != null) {              // 绘制视图              // 继续看下面源码分析                more |= drawChild(canvas, transientChild, drawingTime);            }            ......    }}// drawChild()源码分析protected boolean drawChild(Canvas canvas, View child, long drawingTime) {    // 最终还是调用了子 View 的 draw ()进行子View的绘制    return child.draw(canvas, this, drawingTime);}

阅读全文
0 0
原创粉丝点击