我们都知道 View 的绘制过程包括三个步骤,即 onMeasure()、onLayout()、onDraw()。我们要逐一对三个过程进行解析。
onMeasure()
作用是用于测量视图的大小。在 View 的 onMeasure() 方法中会接收两个参数 widthMeasureSpce 和 heightMeasureSpec,这两个值用于确定视图的宽度和高度的规格和大小。
MeasureSpec 的值由 specSize 和 specMode 共同组成,其中 specSize 记录的是大小,specMode 记录的是规格,specMode 由三种规格,如下:
EXACTLY
表示子视图的大小由 specSize 值来决定,系统默认会按照这个规则来设置子视图的大小,我们也可以按照自己的要求来设置成任意的大小
AT_MOST
表示子视图最多只能是 specSize 指定的大小,
UNSPECIFIED
表示我们可以按照自己的意愿设置成任意的大小,没有限制。但这种情况平时开发中很少用到。
widthMeasureSpec 和 heightMeasureSpec 这两个值是父视图经过计算传递给子视图的,那么父视图的数据又是从哪里来的呢?我们有必要去看下源码啦。
View 的绘制过程是从 ViewRootImpl 的 performTraversals()方法开始,代码如下:
|
|
在上述代码中,我们可以发现这里调用了 getRootMeasureSpec() 方法,这个方法获取 widthMeasureSpec 和 heightMeasureSpec 的值,这个方法接收两个参数,第一个参数是 window 的可使用值。第二个参数是 创建 ViewGroup 实例时布局文件中的值,它们都等于 MATCH_PARENT。getRootMeasureSpec() 方法得代码如下:
|
|
可以看到,这是使用 MeasureSpec.makeMeasureSpe() 方法返回一个 MeasureSpec,当 rootDimension 等于 MATCH_PARENT 时,MeasureSpec 的 specMode 是 EXACTLY。当 rootDimension 等于 WRAP_CONTENT 时,MeasureSpec 的specMode 是AT_MOST。并且 MATCH_PARENT 和 WRAP_CONTENT 时的 specSize 都是 windowSize,也就意味着根视图总是充满全屏的。
我们回到 performTraversals() 方法中,在这个方法中会调用 performMeasure() 方法,代码如下:
|
|
在这个方法中会调用 View 的 measure() 方法来测量视图的大小,View 的 measure() 方法代码如下:
|
|
上述代码中,measure() 方法是被final 修饰的,所以这个方法我们是不能重写的,然而 上面调用了 onMeasure() 方法,这个就是我们经常使用的方法,如下:
|
|
这个方法中默认会调用 getDefaultSize() 方法来获取视图的大小,如下:
|
|
这里的 measureSpec 是一直从 measure() 方法中传递过来的,然后调用 MeasureSpec.getMode() 方法来获取 specMode,调用 MeasureSpec.getSize() 方法来获取 specSize。接下来进行判断,如果 specMode 是 AT_MOST 和 EXACTLY,那么返回 specSize,如果 specMode 是 UNSPECIFIED ,那么返回的是 size。之后会在 onMeasure() 方法中调用 setMeasureDimension() 方法来设定测量的大小,到此为止,一次测量过程就结束啦。
当然,如果一个布局中包含了多个子视图,每个子视图的绘制都会经历一个 measure 过程,ViewGroup 中定义了一个 measureChildren() 方法来测量子视图的大小,如下:
|
|
这个方法首先会遍历当前布局下的所有子视图,然后逐一调用 measureChild() 方法来测量相应视图的大小,如下:
|
|
在这个方法中,调用 getChildMeasureSpec() 方法来计算子视图的 MeasureSpec 方法,计算的依据是布局文件中的 MATCH_PARENT、WRAP_CONTENT、paddingLeft、paddingRight 等值,在最后会调用 child.measure() 方法,这个方法就是我么上面分析的 View 的 measure()。也就是再把上面的流程再走一遍。
需要注意, 在 setMeasureDimension() 方法调用之后,我们才能使用 getMeasuredWidth() 和 getMeasuredHeight() 来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
通过上面的分析,我们知道视图大小的控制是由父视图、布局文件以及视图本身共同决定的,父视图会根据 specMode 决定 自视图的大小,而我们可以在布局文件中指定视图的大小,然后视图本身会对最终的大小进行决定。
到此为止,视图绘制过程的第一个阶段就完成啦。
onLayout()
这个方法的作用是确定视图的位置。ViewRootImpl 的 performTravels() 方法在执行完 performMeasure() 方法之后,会调用 performLayout() 方法,如下:
|
|
在这个方法里,调用了 View 的 layout() 方法,它的四个参数分别是左、上、右、下的坐标,那么我们来看一下 layout() 方法,如下:
|
|
在上述方法中,首先会调用 isLayoutModeOptical() 方法来判断父控件是 View 还是 ViewGroup,如果是 ViewGroup,那么就执行 setOpticalFrame() 方法,如果是 View,那么就执行 setFrame() 方法,通过观察我们发现 setOpticalFrame() 方法的也是调用 setFrame() 方法,因此是通过调用 setFrame() 方法来判断视图的大小是否发生过改变,来决定是否要对当前视图进行重绘。接下来会调用 onLayout() 方法,我们跟进去瞧瞧,原来 onLayout() 方法是空方法,如下:
|
|
从上面的注视可以知道,onLayout() 方法是 View 的子类必须重写的一个方法,这是因为 onLayout() 方法的作用是确定视图在布局中的位置,那么这个工作应该由布局来完成,即父视图决定子视图的显示位置。
在 onLayout() 方法结束之后,我们就可以用 getWidth() 方法和 getHeight() 方法来获取视图的宽高啦,有的朋友可能分不清 getWidth() 方法和 getMeasureWidth() 方法,那么我么来详细说一下,
- getMeasureWidth() 方法是在 measure() 过程结束后就可以获得到,它的值是经过 setMeasureDimension() 方法来设置的。
- getWidth() 方法是在 Layout() 方法中获得的,它的值是通过视图右边的值减去左边的值计算出来的。
到此为止,View 绘制的第二阶段我们就分析完毕啦。
onDraw()
它的作用是对视图进行绘制。View 的 draw() 方法如下,
|
|
可以看到,View 的绘制需要经过六步,但一般从第二步和第五步我们很少用到。首先,
- 第一步是对视图的背景进行绘制。
- 第三步是绘制内容,调用 onDraw() 方法,它的里面是一个空实现,我们必须重写该方法。
- 第四步是对当前视图的所有子视图进行绘制,我们发现 dispatchDraw() 方法也是一个空方法,但 ViewGroup 的 dispatchDraw() 方法就会有具体的实现。
- 第六步的作用是画滚动条,调用 onDrawForeground () 方法,在这个方法里会调用 onDrawScrollIndicators() 方法 和 onDrawScrollBars() 方法去画滚动条。
onDraw() 是我们根据具体的需求需要自己手动重写的,根据不同的视图绘制不同的内容。
到此为止,我们的 onDraw() 方法就分析完毕啦。