Android 开发艺术探索笔记-4.View的工作原理
cfanr Lv4

4.1初识ViewRoot和DecorView

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

1
2
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparam, panelParentView);

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上,流程如图:

measure过程决定了view的宽高,通常情况下这个宽高都等同于view最终的宽高。layout过程决定了view的四个顶点的坐标和view实际的宽高,通过getWidth和getHeight方法可以得到最终的宽高。draw过程决定了view的显示。

DecorView其实是一个FrameLayout,内部包含一个竖直方向的LinearLayout,它里面有上面的标题栏和下面的内容栏(id为android.R.id.content)两部分。

4.2理解MeasureSpec

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

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大;
  • EXACTLY:父容器已经检测出View所需要的精确大小,View的最终大小就是SpecSize的指定值。LayoutParams中的match_parent和具体值就是这种模式;
  • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,对应于LayoutParams的wrap_content。

2.MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定view的MeasureSpec,从而进一步确定view的宽高。对于DecorView,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来决定;对于普通view,它的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。

3.普通view的MeasureSpec的创建规则

当view采用固定宽高时,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式,并且大小是LayoutParams中的大小。
当view的宽高是match_parent时,如果父容器的模式是精确模式,那么view也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,那么view也是最大模式,并且大小是不会超过父容器的剩余空间。
当view的宽高是wrap_content时,不管父容器的模式是精确模式还是最大模式,view的模式总是最大模式,并且大小不超过父容器的剩余空间。

4.3View的工作流程

1.measure过程
原始View,直接measure就完成测量过程,而ViewGroup,除了完成自己的测量过程,还需要遍历调用所有子元素的measure方法。具体测量方法代码略。

view的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个view已经测量完毕了。如果view还没有测量完毕,那么获得的宽高就都是0。下面是四种解决该问题的方法:

  • Activity/View # onWindowFocusChanged方法
    onWindowFocusChanged方法表示view已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没问题的。这个方法会被调用多次,当Activity继续执行或者暂停执行的时候,这个方法都会被调用。
  • view.post(runnable)
    通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也已经初始化好了。
  • ViewTreeObserver
    使用ViewTreeObserver的众多回调方法可以完成这个功能,比如使用onGlobalLayoutListener接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,onGlobalLayout方法将被回调。伴随着view树的状态改变,这个方法也会被多次调用。
  • view.measure(int widthMeasureSpec, int heightMeasureSpec)
    通过手动对view进行measure来得到view的宽高,这个要根据view的LayoutParams来处理:
    match_parent:无法measure出具体的宽高;
    精确值:例如100px
    1
    2
    3
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec, heightMeasureSpec);
    wrap_content:如下measure,设置最大值
    1
    2
    3
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
    view.measure(widthMeasureSpec, heightMeasureSpec);

2.layout过程
layout方法的大致流程:先通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,View在父容器的位置就确定了;接着调用onLayout方法,用于父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。

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

  • 绘制背景background.draw(canvas)
  • 绘制自己(onDraw)
  • 绘制children(dispatchDraw):dispatchDraw会遍历调用所有子元素的draw方法,使draw事件一层层地传递下去。
  • 绘制装饰(onDrawScrollBars)

View有一个特殊的方法setWillNotDraw, 如果一个View不需要绘制任何内容,设置这个标记位为true后,系统会做相应优化。对于自定义控件继承ViewGroup,本身不具备绘制功能,可以开启这个标识。

4.4自定义View

1.继承View重写onDraw方法,需要自己支持wrap_content,并且padding也要自己处理。继承特定的View例如TextView不需要考虑。
2.继承ViewGroup需要派生特殊的Layout
3.尽量不要在View中使用Handler,View内部有提供post系列方法可以替代Handler
4.View中如果有线程或者动画,需要在onDetachedFromWindow方法中及时停止。
5.带有滑动嵌套时,处理好滑动冲突

The end.