参考:
- 图解 View 测量、布局及绘制原理
- 关于 View 的三大流程与自定义一些方法的总结
- Android View 的工作流程
- 源码解析 Android 中 View 的 measure 量算过程
- Android 应用层 View 绘制流程与源码分析
前言
要彻底的掌握自定义View, 当然要理解 View 的测量、布局及绘制原理, 现在才发现以前写的那些自定义View其实也只是一知半解, 所以开一篇博客特意梳理总结一下.
View的绘制流程
首先看一下Activity的架构图
我们的Activity布局就在这个ContentView里面
看一下用Layout Inspector探测的布局
在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中, 而内容栏的id是content, 我们的布局的确加到了id为content的FrameLayout中.
如何得到DecorView?可以这样:
1 | View decorView = activity.getWindow().getDecorView(); |
如何得到content?可以这样:
1 | FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content); |
如果要得到我们设置的View, 就可以这样
1 | View contentView = content.getChildAt(0); |
ViewRoot对应于ViewRootImpl类, 它是连接WindowManager和DecorView的纽带, View的三大流程均是通过ViewRoot来完成的. View的绘制流程是从ViewRootImpl的performTraversals方法开始的, 它经过measure、layout和draw三个过程才能最终才将一个View画出来.
measure确定View的测量宽/高, layout确定View的最终宽/高和四个顶点的位置, draw将View绘制到屏幕上.
View的绘制流程大概能够以下面这张图表示
performTraversals会依次调用performMeasure, performLayout和performDraw三个方法, 这三个方法依次分别完成顶级View的measure, layout和draw这三大流程, 其中在performMeasure中会调用measure方法, 在measure中又会调用onMeasure方法, 在onMeasure方法中则会对所有的子元素进行measure过程, 这个时候measure流程就从父元素传递到子元素了, 这样就完成了一次measure过程.接着子元素会重复父容器的measure过程, 如果反复就完成了整个View树的遍历.同理performLayout和performDraw的传递路程和performMeasure是类似的, 唯一不同的是, performDraw的传递是在draw方法中通过dispatchDraw来实现的, 不过这并没有本质区别.
我们还是简略的看一下源码吧.
这是performTraversals方法
measure流程
这是在performTraversals里的performMeasure方法
刚才说到performTraversal会调用顶级View的方法, 在这里打断点可知, 这个顶级View就是DecorView.
我们再跳转到View的measure()方法里
可以看到高亮处, onMeasure方法被调用了, 我们跳转到onMeasure方法看看.
可以看到onMeasure中有一个默认实现, 但是在写自定义View的时候, 我们很多时候都会重写这个onMeasure方法
理解MeasureSpec
我们看到onMeasure方法中有两个参数, widthMeasureSpec和heightMeasureSpec, 那么MeasureSpec是什么意思呢?
MeasureSpec翻译过来叫”测量说明书”.
MeasureSpec代表一个32位的int值, 高2位代表SpecMode, 低30位代表SpecSize.
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配, 为了方便操作, 提供了打包和解包的方法.我们自己写一个类测试一下:
1 | public class MeasureSpecTest { |
输出结果:
1 | modeMask = 11000000000000000000000000000000 |
可以看到 EXACTLY是01后面跟30个0, AT_MOST是10后面跟30个0, UNSPECIFIED是00后面跟30个0.具体的计算我就不说了, 其实也很简单, 主要涉及左移, 非, 以及最重要的通过来按位与来屏蔽某些位的操作.
下面来说说SpecMode的含义:
- UNSPECIFIED
父容器不对View有任何限制, 要多大给多大, 这种情况一般用于系统内部, 表示一种测量状态(listView和scrollView等) - EXACTLY
父容器已经检测出View所需要的精确大小, 这个时候View的最终大小就是SpecSize所指定的值.它对应于LayoutParams中的match_parent和具体的数值这两种模式 - AT_MOST
父容器指定了一个可用大小即SpecSize, View的大小不能大于这个值, 具体是什么值要看不同View的具体实现, 它对应于LayoutParams中的wrap_content
我们再回过头去看onMeasure方法, onMeasure中的两个参数widthMeasureSpec和heightMeasureSpec都是从上级View传递进来的. 通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。
那么MeasureSpec又是如何确定的呢?
对于 DecorView,其确定是通过屏幕的大小,和自身的布局参数 LayoutParams。
我们看一下ViewRootImpl中传递给DecorView的layoutParams是如何确定的
这个lp.with中的lp是一个WindowManager.LayoutParams, mWidth就是窗口的大小了
我们在看看getRootMeasureSpec()方法
1 | private static int getRootMeasureSpec(int windowSize, int rootDimension) { |
对于其他 View(包括 ViewGroup),系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec, 然后再根据这个measureSpec来测量出View的宽/高.
关于这里我们看一下ViewGroup的getChildMeasureSpec方法
1 | public static int getChildMeasureSpec(int spec, int padding, int childDimension) { |
可以看到, 这个方法的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec. 将结论整理成表格就是这样:
一般在自定义View时,当View的LayoutParams的布局格式是wrap_content,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。这是为什么呢?我们看一下View的onMeasure()方法
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
看一些getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
1 | public static int getDefaultSize(int size, int measureSpec) { |
从上面的表格可知, 当View的布局格式是wrap_content时, View的specMode是AT_MOST, 而这里, case MeasureSpec.AT_MOST时, 得到的宽度是specSize, 再从上面的表格可知, 当View的布局格式是wrap_content时, View的specSize是parentSize, 就是父容器当前剩余的空间大小.这种效果和使用match_parent完全一致, 所以需要我们需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。
下面看看ViewGroup中的measure流程, 以RelativeLayout举例, 在它的onMeasure()方法中:
再跟踪这个measureChild方法
可以看到最终调用了child.measure()方法, 也就是View的measure()方法, 这是测量流程就从当前RelativeLayout传递到子元素了.
看一下测量的完整流程, 需要说明的是这个measureChildre方法并不是必须调用的, 它只是ViewGroup提供的一个方法, 可以用它来方便的测量子View, 就不用自己再去写循环遍历测量的逻辑了.
在完全理解了View的测量流程之后下面提两个问题
- getMeasuredHeight()和getHeight()有什么区别?
- 在Activity的生命周期中, 因为View还没有测量完毕, 我们无法直接得到View的宽高, 有一种解决方法是手动对View进行测量, view.measure(0,0), 然后就能获得宽高了, 这是为什么呢?
首先解决第一个问题, 我们看一下getMeasureHeight()的源码
1 | /** |
可以看到注释写的是返回View的原始的测量高度
再看看getHeight()的源码
1 | /** |
返回的是View的mBottom减去mTop, 而mBottom和mTop是在onLayout()确定的.现在我就直接说结论了:
- getMeasuredHeight方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定, 是View的测量高度
- getHeight方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的, 是View的最终高度
- 一般情况下在onLayout方法中使用getMeasuredHeight方法,而在除onLayout方法之外的地方用getHeight方法
- 一般情况下getMeasureHeight和getHeight是相等的
所以不要再相信网络流传的那种所谓的”getHeight是获取控件在屏幕中的高度, getMeasureHeight是获取控件的完整高度”那种说法了, 要么实践, 要么看源码.
可以看这一篇 Android开发之getMeasuredWidth和getWidth区别从源码分析
关于第二个问题, 这里的情况比较复杂, 我们先看一下View.measure()方法
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { |
这里在measure()方法中会进入onMeasure()方法
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
因为我们传入的widthMeasureSpec和heightMeasureSpec都是0, 所以这里的widthMeasureSpec和heightMeasureSpec也都是0, 我们看一下getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
这个方法
1 | public static int getDefaultSize(int size, int measureSpec) { |
这边传入的size是getSuggestedMinimumWidth(), 一般我们如果没有设置backgroud或者minwidh的话, 这个size就是0.因为传入的measureSpec是0, 所以解包出来的specMode和specSize也都是0, specMode对应MeasureSpec.UNSPECIFIED, 返回结果size, 也就是0.但是因为很多控件都会重写onMeasure方法, 所以这里返回的结果并不一定都是0.下面就说一下结论:
根据View的LayoutParams来分:
- match_parent
直接放弃,无法measure出具体的宽/高。原因很简单,根据view的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量处view的大小。 - wrap_content
1 | int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST); |
注意到(1<<30)-1,我们知道MeasureSpec的前2位为mode,后面30位为size,所以说我们使用最大size值去匹配该最大化模式,让view自己去计算需要的大小。
- 具体的数值(dp/px)
这种模式下,只需要使用具体数值去measure即可,比如宽/高都是100px:
1 | int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); |
Layout流程
测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。
其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。
1 | public void layout(int l, int t, int r, int b) { |
确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。
1 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。
如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。
由此便完成了一层层的的布局工作。
View的布局流程:
Draw流程
View的绘制过程遵循如下几步:
绘制背景 background.draw(canvas)
绘制自己(onDraw)
绘制Children(dispatchDraw)
绘制装饰(onDrawScrollBars)
从源码中可以清楚地看出绘制的顺序。
1 | public void draw(Canvas canvas) { |
无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
值得一提的是, 重写ViewGroup的onDraw()方法, ViewGroup默认是不进行绘制的, 如果需要通过onDraw来绘制内容, 需要调用setWillNotDraw(false)
这句代码来显式地关闭WILL_NOT_DRAW这个标记位
View绘制流程:
总结
从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。
onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。
onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法