ViewRoot和DecorView

  • ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

  • 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过程,如此反复完成了整个View数的遍历。

  • measure过程决定了View的宽/高,完成后可通过getMeasuredWidth/getMeasureHeight方法来获取View测量后的宽/高。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTop、getBotton、getLeft和getRight拿到View的四个定点坐标。Draw过程决定了View的显示,完成后View的内容才能呈现到屏幕上。

  • DecorView作为顶级View,一般情况下它内部包含了一个竖直方向的LinearLayout,里面分为两个部分(具体情况和Android版本和主题有关),上面是标题栏,下面是内容栏。在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。


理解MeasureSpec

MeasureSpec

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

  • 为了方便操作其提供了打包和解包的方法,一组SpecMode和SpecSize可以打包为MeasureSpec,MeasureSpec也可以解包为一组SpecMode和SpecSize,这里的MeasureSpec是指其所代表的int值

  • MpecMode有三类:

    1. UNSPECIFIED 父容器不对View进行任何限制,要多大给多大,一般用于系统内部
    2. EXACTLY 父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式。
    3. AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,不同View实现不同,对应LayoutParams中的wrap_content。

    MeasureSpec和LayputParams的对应关系

    • 对于DecorView,它的MeasureSpec由Window的尺寸和其自身的LayoutParams来共同确定,对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams来共同确定。

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

    • 这里参数中的padding是指父容器的padding,这里是父容器所占用的空间,所以子view能使用的空间要减去这个padding的值。同时这个方法内部其实就是根据父容器的MeasureSpec结合子view的LayoutParams来确定子view的MeasureSpec

      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
      public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      int specMode = MeasureSpec.getMode(spec);
      int specSize = MeasureSpec.getSize(spec);

      int size = Math.max(0, specSize - padding);

      int resultSize = 0;
      int resultMode = 0;

      switch (specMode) {
      // Parent has imposed an exact size on us
      case MeasureSpec.EXACTLY:
      if (childDimension >= 0) {
      resultSize = childDimension;
      resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
      // Child wants to be our size. So be it.
      resultSize = size;
      resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
      // Child wants to determine its own size. It can't be
      // bigger than us.
      resultSize = size;
      resultMode = MeasureSpec.AT_MOST;
      }
      break;

      // Parent has imposed a maximum size on us
      case MeasureSpec.AT_MOST:
      if (childDimension >= 0) {
      // Child wants a specific size... so be it
      resultSize = childDimension;
      resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
      // Child wants to be our size, but our size is not fixed.
      // Constrain child to not be bigger than us.
      resultSize = size;
      resultMode = MeasureSpec.AT_MOST;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
      // Child wants to determine its own size. It can't be
      // bigger than us.
      resultSize = size;
      resultMode = MeasureSpec.AT_MOST;
      }
      break;

      // Parent asked to see how big we want to be
      case MeasureSpec.UNSPECIFIED:
      if (childDimension >= 0) {
      // Child wants a specific size... let him have it
      resultSize = childDimension;
      resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
      // Child wants to be our size... find out how big it should
      // be
      resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
      resultMode = MeasureSpec.UNSPECIFIED;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
      // Child wants to determine its own size.... find out how
      // big it should be
      resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
      resultMode = MeasureSpec.UNSPECIFIED;
      }
      break;
      }
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
      }

    • 普通View的MeasureSpec创建规则


View的工作流程

measure过程

View的measure

  • 一般我们只需要看MeasureSpec.AT_MOST和MeasureSpec.EXACTLY两种情况,这两种情况返回的result其实都是measureSpec中取得的specSize,这个specSize就是View测量后的大小,这里之所以是View测量后的大小,是因为View的最终大小是在layout阶段确定的,所以要加已区分,一般情况下View测量大小和最终大小是一样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//里面有一个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;
}

  • View的宽高由specSize决定,如果我们通过继承View来自定义控件需要重写onMeasure方法,并设置WRAP_CONTENT时的大小,否则在布局中使用WRAP_CONTENT相当于使用MATCH_PARENT

ViewGroup的measure

  • ViewGroup是一个抽象类,它没有重写View的onMeasure方法,而是自己提供了一个measureChildren方法

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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);
    }
    }
    }

    里面会对子元素进行遍历,然后调用measureChild方法去测量每一个子元素的宽高

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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接着将其传递给view的measure方法进行测量

  • ViewGroup并没有去定义测量的具体过程,这是因为ViewGroup是一个抽象类,其onMeasure方法需要各个子类去具体实现,因为每个ViewGroup子类有不同的布局特性,这导致他们的测量细节各不相同,因此ViewGroup无法做到统一实现

  • 有时候onMeasure中拿到的测量宽高可能是不准确的,比较好的习惯是在onLayout中去获取View的测量宽高和最终宽高

  • 在Activity中,在onCreate,onStart,onResume中均无法正确获得View的宽高信息,这是因为measure和Activity的生命周期是不同步的,所以很可能View没有测量完毕,获得的宽高是0,有四种解决该问题的方法:

    1. Activity/View#onWindowFocusChanged

      • onWindowFocusChanged这个方法的含义:View已经初始化完毕,宽高都已经准备好了,,这个时候去获取宽高都是没有问题的,需要注意的是这个方法会被多次调用,当Activity窗口得到焦点和失去焦点时均会被调用,如果频繁地进行onResume和onPause那么这个方法就会频繁地被调用

        1
        2
        3
        4
        5
        6
        7
        8
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
        int width = myView.getMeasuredWidth(); // 此时测量已完成,可以拿到值
        int height = myView.getMeasuredHeight();
        }
        }
    2. view.post(runnable)

      • 通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化完成

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        protected void onStart(){
        super.onStart();
        view.post(new Runnable() {
        @Override
        public void run() {
        // 此时 View 已经关联到 Window,且完成了测量和布局
        int width = myView.getWidth();
        int height = myView.getHeight();
        Log.d("TAG", "View 的宽高是: " + width + " x " + height);
        }
        });
        }
    3. ViewTreeObserver

      • 使用ViewTreeObserver这个的众多回调也可以实现这个功能,比如使用OnGlobalLayoutListener这个接口,当 View 树的状态发生改变或者View树内部的View的可见性发生改变时,此方法发生回调,因此是获取view宽高的好时机,需要注意的时,伴随着View树状态改变此方法会被多次调用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        protected void onStart(){
        super.onStart();
        // 1. 获取观察者并添加监听
        ViewTreeObserver observe=view.getViewTreeObserver();
        observe.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
        // 2. 此时可以百分之百拿到准确的宽高
        int width = view.getWidth();
        int height = view.getHeight();

        // 3. 【极其重要】及时移除监听
        // 因为任何微小的布局变化(如键盘弹出)都会再次触发此方法,不移除会造成性能浪费甚至死循环
        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        }
        });
        }
    4. view.measure(int widthMeasureSpec,int heightMeasureSpec)

      • 通过手动对View进行measure,需要根据LayoutParams分情况处理

        1. **match_parent:**直接放弃,无法measure出宽高

        2. 具体数值:

          1
          2
          3
          int widthSpec = MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
          int heightSpec = MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
          view.measure(widthSpec, heightSpec);
        3. wrap_content:

          1
          2
          3
          4
          // 使用 AT_MOST 模式,大小传入屏幕能提供的最大值(移位运算后的最大值)
          int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);//1<<30即2的30次方
          int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
          view.measure(widthSpec, heightSpec);

总结

  1. measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。

  2. MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

    1
    2
    3
    MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
    MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
    MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;
  3. View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

  4. 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

  5. ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

  6. 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

  7. View的布局大小由父View和子View共同决定。

  8. 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

layout过程

layout

  • Layout作用是ViewGroup用来确定子元素位置的,ViewGroup的位置确定后,它在onLayout中会遍历所有的子元素并调用子元素layout方法,子元素layout方法中又会调用onLayout方法,View的layout方法确定自身的位置,而onLayout方法确定所有子元素的位置
  • layout方法的大致流程如下:首先会通过setFrame方法来确定mLeft;mTop;mBottom; mRight;只要这四个点一旦确定,那么View在父容器中的位置就确定了,接着会调用onLayout方法,该方法目的是父容器来确定子元素的位置,无论是View还是ViewGroup都没有实现onLayout方法

总结

  1. layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。
  2. View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
  3. measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
  4. 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。
  5. 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

draw过程

draw

  • 步骤

    1. 绘制背景background.draw(canvas)
    2. 绘制自己(onDraw)
    3. 绘制children(dispatchDraw)
    4. 绘制装饰(onDrawScrollBars)
  • View的绘制过程的传递是通过dispatchDraw实现的,dispatchdraw会遍历调用所有子元素的draw方法。如此draw事件就一层一层的传递下去。

总结

  1. 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
  2. View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
  3. View的绘制是借助onDraw方法传入的Canvas类来进行的。
  4. 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
  5. 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

自定义View

自定义View的分类

  1. 继承View重写onDraw方法
  • 这种方法主要用于实现一些不规则的效果,这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则的图形,很显然这需要通过绘制的方法来实现,重写onDraw 方法,采用这种方式需要自己支持wrap_content,并且padding也是需要自己处理。
  1. 继承ViewGroup派生特殊的Layout
    • 这种方法主要用于实现自定义的布局,出了LinerarLayout、RelativeLayout、FrameLayout 这几种系统的布局之外,我们重新定义一种新布局、当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现,采用这种方式稍微复杂一些,需要合适的处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
  2. 继承特定的View(比如TextView ImageVIew)
    • 这种方法比较常见,一般适用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现,这种方法不需要自己支持 wrap_content 和 padding等;
  3. 继承特定的VIewGroup(比如LinearLayout)
    • 这种方法比较常见,当某种效果看起来很像几个View组合在一起的时候,可以 采用这种方式来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程,需要注意这种方法和第二种方法的区别,一般来说方法二种实现的效果方法四中也可以实现,两者的主要差别在于方法二更接近View的底层。

自定义View注意事项

  1. 让View支持wrap_content
    • 因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content中做一些的特殊处理,那么当外界在布局中使用wrap_content 时就无法达到预期的效果。
  2. 如果有必要,让View支持padding
    • 如果不在draw方法中处理padding,那么padding属性是无法起作用的,另外,直接继承自ViewGroup 的控件需要在onMeasure 和onLayout中考虑padding和子元素的margin对其造成影响,不然将导致padding和子元素的margin失效。
  3. 尽量不要再View中使用Handler
    • 因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非自己很明确的要使用Handler来发送消息。
  4. View中如果有线程或者动画,需要及时停止
    • 如果有线程或者动画需要停止时候,那么onDetachedFromWindow是一个很好的时机,当包含View的Activity退出或者当前VIew被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动的时候,View的onAttachedToWindow方法会被调用,同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
  5. View带有滑动嵌套情形时,需要处理好滑动冲突
    • 如果有滑动冲突的时候,那么要合适的处理滑动冲突,否则将会严重影响View的效果。