目录
- 初识ViewRoot和DecorView
- 理解MeasureSpec
- View的工作流程
- 自定义View
初识ViewRoot和DecorView
View的绘制流程从ViewRoot的perfromTraversals方法开始,他经过measure,layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定view在父容器中的放置位置,draw负责将View绘制在屏幕上,针对perfromTraversals的大致流程,可以看图:
图中的perfromTraversals会依次调用perfromMeasure,perfromLayout,perfromDraw,他们分别完成顶级View的measure,layout和draw这三大流程,其中在perfromMeasure中会调用measure方法,在measure方法中又调用onMeasure,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure过程,接着子元素会重复父容器的measure过程,如此反复的完成了整个View树的遍历,同理,其他两个也是如此,唯一有点区别的是perfromDraw的传递过程是在draw反复中通过dispatchDraw来实现的,不过这并没有什么本质的区别。
measure过程决定了View的宽高,Measure完成之后可以通过getMeasureWidth和getMeasureHeight来获取View测量后的高宽,在所有的情况下塔几乎都是等于最终的宽高,但是特殊情况除外。
layout过程决定了view的四个顶点的坐标和实际View的宽高,完成之后,通过getTop,getLeft,getRight,getBottom来拿到View的四个顶点的位置,并可以通过getWight和getHeight方法来拿到View的最终宽高。
Draw决定了View的显示,只有draw方法完成了之后,view才会显示在屏幕上。
理解MeasureSpec
MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某个测量模式下的规格大小。
SpecMode有三类,每一类都有特殊的含义
- UNSPECIFIED
父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
- EXACTLY
父容器已经检测出View所需要的精度大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent,和具体的数值这两种模式。
- AT_MOST
父容器指定了一个可用大小,即SpecSize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现,它对应于LayoutParams中wrap_content。
MeasureSpec 和 LayoutParams 的对应关系
MeasureSpec不是唯一由layoutparams决定的,layoutparams需要和父容器一起决定view的MeasureSpec从而进一步决定view的宽高,对于顶级view(DecorView)和普通的view来说,MeasureSpec的转换过程有些不同,对于decorview,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams来共同决定,MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽高。
- LayouParams.MATCH_PARENT:精确模式,大小就是窗口的大小
- LayouParams.WRAP_CONTENT:最大模式,大小不定,但是不能超出屏幕的大小
- 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
对于普通View,其MeasureSpec 由父容器的MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和Viev本身不同的LayoutParams,View就可以有多种MeasureSpec。
当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpee都是精确模式,那么View也是精准模式并且其大小是父容器的剩余空间;
如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化,并且大小不能超过父容器的剩余空间。
View的工作流程
measure过程
View的measure过程
- measure
/** 开始测量一个View有多大,parent会在参数中提供约束信息,实际的测量工作是在onMeasure()中进行的,该方法会调用onMeasure()方法,所以只有onMeasure能被也必须要被override */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);
父布局会在自己的onMeasure方法中,调用child.measure ,这就把measure过程转移到了子View中。
- onMeasure
/** 具体测量过程,测量view和它的内容,来决定测量的宽高(mMeasuredWidth mMeasuredHeight )。该方法中必须要调用setMeasuredDimension(int, int)来保存该view测量的宽高。 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
子View会在该方法中,根据父布局给出的限制信息,和自己的content大小,来合理的测量自己的尺寸。
- setMeasuredDimension
/** 保存测量结果 */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);
当View测量结束后,把测量结果保存起来,具体保存在mMeasuredWidth和mMeasuredHeight中。
ViewGroup的measure过程
- measureChildren
/** 让所有子view测量自己的尺寸,需要考虑当前ViewGroup的MeasureSpec和Padding。跳过状态为gone的子view */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();
测量所有的子View尺寸,把measure过程交到子View内部。
- measureChild
/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding。 */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();
对每一个具体的子View进行测量。
- measureChildWithMargins
/** 测量单个View,需要考虑当前ViewGroup的MeasureSpec和Padding、margins。 */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();
对每一个具体的子View进行测量。但是需要考虑到margin等信息。
- getChildMeasureSpec
/** measureChildren过程中最困难的一部分,为child计算MeasureSpec。该方法为每个child的每个维度(宽、高)计算正确的MeasureSpec。目标就是把当前viewgroup的MeasureSpec和child的LayoutParams结合起来,生成最合理的结果。
比如,当前ViewGroup知道自己的准确大小,因为MeasureSpec的mode为EXACTLY,而child希望能够match_parent,这时就会为child生成一个mode为EXACTLY,大小为ViewGroup大小的MeasureSpec。
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension);
根据当前自身的状况,以及特定子View的尺寸参数,为特定子View计算一个合理的限制信息。
layout过程
Layout的作用是ViewGroup用来确定子元素的作用的,当ViewGroup的位置被确认之后,他的layout就会去遍历所有子元素并且调用onLayout方法,在layout方法中onLayou又被调用。
layout方法确定了View本身的位置,而onLayout方法则会确定所有子元素的位置。
layout的方法的大致流程如下,首先会通过一个setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mTop,mRight,mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接下来会调用onLayout方法,这个方法的用途是调用父容器确定子元素的位置。
draw过程
draw过程的作用是将View绘制到屏幕上面,View的绘制过程:
- 绘制背景background.draw(canvas)。
- 绘制自己(onDraw)。
- 绘制children(dispatchDraw)。
- 绘制装饰(onDrawScrollBars)。
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。
自定义View
自定义View的分类
- 继承View重写onDraw方法
- 继承ViewGroup派生出来的Layout
- 继承特定的View
- 继承特定的ViewGroup
自定义View的须知
- 让View支持warp_content
- 如果有有必要,让你的View支持padding
- 尽量不要在View中使用Handler
- View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
- View带有滑动嵌套时,需要处理好滑动冲突
参考资料:
《Android开发艺术探索》
Android开发艺术探索笔记
Android View框架的measure机制