View的三大流程:
测量流程、布局流程、绘制流程。
View常见的回调方法:
构造方法、onAttach、onVisibilityChanged、onDetach
一、ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。
在ActivityThread中,当Activity对象被创建后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl和DecorView建立关联。
View的绘制流程是从ViewRoot的performTraversals开始的。
Measure用来测量View的宽、高;
layout用来确定View在父容器中的放置位置;
draw负责将View绘制在屏幕上。
二、MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode代表测量模式,而SpecSize是指在某种测量模式下的规格大小。
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽/高。
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,有打包和解包方法。
SpecMode有三类,每一类都表示特殊的含义:
UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部或者用户需要手动测量View的大小。
EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小是SpecSize所指定的值,它对应于LayoutParams中的match_parent和具体的数值。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现,对应于LayoutParams中的wrap_content.
三、View的工作流程
view的工作流程主要是指measure、layout、draw这三大流程,即测量、布局、绘制。
其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。
measure过程,如果只是一个原始的View,那么通过measure方法就完成了其测量过程;如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。
measure过程产生的大小为测量后的大小,因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分,但几乎所有情况下View的测量大小和最终大小是相等的。
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法有会被调用。layout方法确定View本身的位置,而onLayout方法确定所有子元素的位置。
通过以下四种方法来获取View的宽/高
1.Activity/View/onWindowFocusChanged
2.view.post(runnable)
3.ViewTreeObserver
4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。
在某些情况下,View需要多次measure才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高和最终宽/高还是相同的。
Draw过程
draw的作用是将View绘制到屏幕上面。View的绘制过程蹲循以下几步:
1.绘制背景background.draw(canvas)。
2.绘制自己(onDraw)。
3.绘制children(dispatchDraw)。
4.绘制装饰(onDrawScrollBars)。
setWillNotDraw这个方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。
四、自定义View
自定义View是一个综合的技术体系,它涉及View的层次结构、事件分发机制和View的工作原理等细节。
自定义View的分类
1.继承View重新onDraw
主要是实现一些不规则的图形。
重写onDraw方法。采用这种方式需要自己支持wrap_content, 并且padding也需要自己处理。
2.继承ViewGroup派生特殊的Layout
主要用于实现自定义的布局,除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局之外,我们需要重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,就可以采用这个方法来实现。需要适当地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
3.继承特定的View(比如TextView)
一般用于拓展某种已有的View的功能,比如TextView,这种方法比较容易实现,这种方法不需要自己支持wrap_content和padding等。
4.继承特定的ViewGroup(比如LinearLayout)
当某种效果看起来是几种View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。这种方法和方法2的区别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
自定义View须知
1.让View支持wrap_content
直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。
从getDefaultSize方法的实现来看,View的宽/高由specSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent. 原因:如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。这种效果和在布局中使用match_parent完全一致。
解决这个问题的方法:我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高即可。
2.如果有必要,让你的View支持padding
这是因为直接继承View的控件,如果不在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变得不可见时,我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
5.View带有滑动嵌套情形时,需要处理好滑动冲突。
自定义View的思想:
首先View的弹性滑动、滑动冲突、绘制原理。
在面对新的自定义View时,要能够对其进行分类并选择合适的实现思路。