重点:View的底层工作原理,View的测量流程、布局流程和绘制流程。自定义View选择最适当的需要的方式,继承view,viewGroup以及现有的系统控件。
1 初识ViewRoot和DecorView
ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的measure、layout和draw都是通过ViewRoot来完成的。在ActivityThread中,activity对象被创建完毕,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。view的绘制流程从ViewRoot中的performTraversals方法开始,经过measure、layout和draw三个过程,measure测量view的宽高、layout确定view在父容器的放置位置而draw负责将view绘制在屏幕上:
performMeasure ---------> measure ----> onMeasure
performLayout --------> layout ------> onLayout
performDraw ---------> draw -------> onDraw
三个方法在定义view中走一次,最后在onMeasure中对所有的子元素进行measure过程,接着子元素复制父容器的measure过程,DecorView作为顶级View(FrameLayout),包含2个LinearLayout,上面标题栏,下面内容栏,内容栏的id为content,所以出现了setContentView的指定布局方法。
2 理解MeasureSpec
测量规格,测量过程中,系统将View的LayoutParams根据父容器施加的规则转换为对应的MeasureSpec,在根据这个MeasureSpec来测量View的宽高。
2.1 MeasureSpec
MeasureSpec代表一个32位int值,高二位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小),SpecMode有三类,如下所示:
UNSPECIFIED:(不明)
父容器不对View有限制,要多大给多大,这种情况用于系统内部,表示一种测量的状态。
EXACTLY:(准确)
父容器检测出了View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值。对应LayoutParams中的match_parent和具体的数值。
AT_MOST(不能大于SpecSize,wrap_content)
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么要看不同View的具体实现,对应LayoutParams中的wrap_content.
2.2 MeasureSpec和LayoutParams的对应关系
测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MesureSpec,然后在根据这个MesureSpec确定View测量后的宽高。其中DecorView中的MesureSpec有窗口的尺寸和自身的LayoutParams共同确定;普通View,其MesureSpec由父容器的MesureSpec和自身的LayoutParams共同确定。
View采用的宽高模式 父容器的MesureSpec View的MesureSpec
dp/px 任何 EXACTLY并是LayoutParams大小
match_parent EXACTLY EXACTLY 并是父容器的剩余空间
match_parent AT_MOST AT_MOST并不会超过父容器的剩余空间
wrap_content 任何 AT_MOST并不会超过父容器的剩余空间
3 View的工作流程
即measure、layout和draw三个过程,测量、布局和绘制
measure -----------------> 确定View的测量宽高
layout ------------------> 确定View的最终宽高和四个顶点的位置
draw -------------------> 将View绘制到屏幕上
3.1 measure过程
view的measure就完成了测量过程,而viewgroup除了完成自己的测量过程外,还遍历调用子元素的measure(),各个子元素在递归去执行这个流程.
3.1.1 View的measure过程
View的mesure是一个final类型的方法,子类不能重写此方法,而View的measure()会调用onMeasure(),所以具体的看onMeasure()即可。getSuggestedMinimumWidth的逻辑:View没有设置背景,返回android:minWidth这个属性值,这个值可以为0;View设置了背景,则返回android:minWidth和背景的最小宽度这两者间的最大值,getSuggestedMinimumWidth的返回值就是View在UNSPECIFIED情况下测量的宽高.
3.1.2 ViewGroup的measure过程
ViewGroup除了完成自己的measure过程,还要遍历调用所有子元素的measure(),各个子元素递归的执行这个过程。ViewGroup是一个抽象类,没有重写onMeasure(),但是提供了一个measureChildren(),因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,所以每个ViewGroup中都有自己的measure()。
View的measure()和Activity的生命周期方法不是同步的,所以在onCreate()、onStart()和onResume()中无法保证能够measure到view的宽高,这里给出了四种方法解决这个问题:
1) onWindowFocusChanged: 在这里View已经初始化完毕,宽高准备好了,但是这个方法可能会执行多次,onResume和onPause会频繁的调用。
2)view.post(runnable): post将一个runnable投递到消息队列的尾部,等待Looper调用此runnable时,View已经初始化完毕。
3)ViewTreeObserver: 使用ViewTreeObserver的回调可完成这个功能,比如使用OnGloballLayoutListener这个接口,当View树状态发生改变或者View树内部的可见性发生改变时,onGlobalLayout()会被回调,这是获取view宽高的一个很好的时机,伴随View树的状态改变,onGloballLayout()会被调用多次。
4)view.measure(int widthMeasureSpec,int heightMeasureSpec):手动对View进行measure得到View的宽高,处理情况要根据View的LayoutParams来分:
match_parent : 直接放弃,需要知道parentSize,父容器的剩余空间,而无法知道parentSize的大小;具体数值(dp/px):宽高都是100dp;wrap_content,P192页.
3.2 layout过程
作用是ViewGroup用来确定子元素的位置,ViewGroup位置确定后,onLayout()遍历所有的子元素并调用layout(),layout()确定View本身的位置,onLayout()会调用所有子元素的位置。一个问题:View的测量宽高和最终宽高有什么区别?
View的getMeasuredWidth和getWidth的区别,在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的measure过程,最终宽高形成于View的layout过程,两者赋值的时机不同。
3.3 draw过程
作用是将View绘制到屏幕上,分为以下几步:
1) 绘制背景 background.draw(canvas) 2) 绘制自己(onDraw) 3)绘制children(dispatchDraw) 4)绘制装饰(onDrawScrollBars)
view的绘制通过dispatchDraw()实现的,dispatchDraw遍历所有子元素的draw(),就会一层层的传递下去,有一个setWillNotDraw() ,如果一个View不需要绘制任何内容,那设置这个标志位true后,系统进行相应的优化。默认情况下,View没有启动这个优化标记位,但ViewGroup会默认启动这个优化标记位。但对于实际开发的意义:当我们自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标志位从而便于系统进行后续的优化;知道一个ViewGroup需要通过onDraw()绘制内容时,需要显示的关闭WILL_NOT_DRAW这个标记位。
4 自定义View
自定义View是一个综合的技术体系,它涉及到了View的层次结构、事件分发机制和View的工作原理等技术细节。
4.1 自定义View的分类(4类)
1)继承View重写onDraw() 主要实现一些不规则的效果,不方便通过布局的组合方式来达到,需要静态或者动态的现实一些不规则的图形。就需要重写onDraw(),支持wrap_content,并且padding也需要自己处理。
2)继承ViewGroup派生特殊的layout 主要实现自定义的布局,除了Linearlayout、RelativeLayout、FrameLayout这几种系统的布局之外,重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可采用这种方式来实现,需要合适的处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
3) 继承特定的View 一般用于扩展已有的View,比如TextView,不需要自己支持wrap_content和padding等等。