(本文图片有借鉴,内容为读《Android艺术与探索》有感,如有错误欢迎指出)
View绘制机制分为measure()测量大小,layout()确定位置,draw()绘制三个过程。
Measure过程:
MeasureSpec:
是一个大小和模式的组合值,32位整型。前两位为mode,后30位为size。
MeasureSpec的模式:
- UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大
- EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
- AT_MOST:子容器可以是声明大小内的任意大小
测量过程是先从子view开始测量,结果出来后在测量自己的逐层向上测量,但测量时计算大小的过程则是拿着父view的MeasureSpec,再结合子view的LayoutParams计算子view的MeasureSpec,再逐层向下计算。
所以相应的排列组合如下:
父view的MeasureSpec | 子view的Layout_XXX | 子view的MeasureSpec |
---|---|---|
EXACTLY大小确定 | match_parent:充满整个父view,所以大小也是确定的 | size = 父view的大小,mode = EXACTLY |
wrap_content:根据自己大小进行调整,但因为父view是固定的,因此最大不能超过父View | size = 暂定父view的大小,mode = AT_MOST 后在实现对自己大小的测量 | |
确定的值(如20dp):不管父view是怎样的,都固定大小 | size = 固定大小,mode = EXACTLY | |
AT_MOST大小不能超过size的值 | match_parent:充满整个父view,父view大小不确定,因此也不确定,最大是父view的大小 | size = 暂定父view的大小,mode = AT_MOST 虽然写的是match_parent但因为父view不确定所以大小也是不确定的 |
wrap_content:根据自己大小进行调整,因为父view子view大小都是不固定的,但能确定的是不会超过父容器 | size = 暂定父view的大小,mode = AT_MOST 后在实现对自己大小的测量 | |
确定的值(如20dp):不管父view是怎样的,都固定大小 | size = 固定大小,mode = EXACTLY | |
UNSPECIFID未制定大小,没有任何束缚和约束 | match_parent:充满整个父view,父view大小没有要求,所以子view也没有任何约束,想多大就多大,size值失去意义,一般直接设成0 | size = 0,mode = UNSPECIFIED |
wrap_content:根据自己大小进行调整,同上 | size = 0,mode = UNSPECIFIED | |
确定的值(如20dp):不管父view是怎样的,都固定大小 | size = 固定大小,mode = EXACTLY |
由此判断子mode的流程可以总结为如下:
p.s. 当view采用固定宽高时,不管父容器MeasureSpec是什么,view的MeasureSpec都是精准模式且大小遵需layoutParams中的大小
对于View的默认值测量时比较容易。根据父容器的状态设定默认值的大小。
但对于其他View的派生类(如TextView、Button、ImageView等)都对onMeasure方法进行重写。会先去测量文字和图片的的高度等拿到contant这个高度,然后再根据mode设定view真正的大小。然后在所有的子view测量完成后,调用setMeasuredDimension设置自己的宽高。(不同view设定方式不同,frameLayout可能是最大子view大小,linearLayout可能是高度的叠加等。)
整个view的结构。其中包含view和textView的linearLayout是用户所添加的布局。
代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:background="@android:color/holo_blue_dark"
android:paddingBottom="70dp"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/material_blue_grey_800"
android:text="TextView"
android:textColor="@android:color/white"
android:textSize="20sp" />
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@android:color/holo_green_dark" />
</LinearLayout>
总结构:
那么View的绘制是从哪里开始的呢?
我们知道每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口,每个Window都对应着一个View和一个ViewRootImpl,
Window和View通过ViewRootImpl来建立联系,对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带,
绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程的。
Layout过程
performTraversals在DecorView.measure计算出大小后,执行Layout来确定具体放在哪个位置。 传入参数为left,top,right,bottom四个位置(因此可不measure就确定view的大小。如right-left)
draw过程
draw的递归流程:
draw 过程中一共分成7步,
第一步:drawBackground(canvas): 作用就是绘制 View 的背景。
第二步:如果必要,保存canvas的layers
第三步:onDraw(canvas) :绘制 View 的内容。View 的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。
第四步:dispatchDraw(canvas):遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup 里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View 的绘制过程,基本满足需求。
第五步:如果必要,绘制渐变效果重启layers
第六步:onDrawForeground(canvas):对前景色跟滚动条进行绘制。
第七步:drawDefaultFocusHighlight(canvas):绘制默认焦点高亮