文章中出现的源码均基于9.0
1. 知识储备
1.1 ViewGroup.LayoutParams
作用: 指定View的高度(height)和宽度(width)等布局参数,用来子视图向父视图传达自己意愿的一个东西
具体使用:
参数 | 说明 |
---|---|
fill_parent | 强制性使子视图的大小扩展到父视图大小相等(不含padding),现在几乎不怎么使用 |
match_parent | 与fill_parent相同,用于Android2.3以后的版本 |
wrap_content | 自适应大小,强制性使视图扩展以便显示出全部内容(含padding) |
具体值 | 直接设置具体的数值,单位一般为dp |
例子:xml中使用
1 | android:layout_width="wrap_content" //自适应大小 |
子类:
- ViewGroup.MarginLayoutParams:只能设置childView的margin属性信息
- LinearLayout.LayoutParams:由于继承了ViewGroup.MarginLayoutParams,所以可以支持动态设置高度、宽度、以及margin属性,另外它还有自己特有属性:gravity和weight属性。
- RelativeLayout.Params:
- ……
1.2 MeasureSpec
1.定义
字面翻译过来,就是测量规格类,可以理解为测量View大小的依据。MeasureSpec在很大程度上决定了一个View的大小。
2.组成
MeasureSpec:测量规格,代表的是一个32位的int值。MeasureSpec=SpecMode+SpecSize,通过使用二进制,可以将测量模式与大小打包成一个int值,并提供了打包和解包的方法:
- makeMeasureSpec(int mode,int size):根据测量模式和测量大小打包成一个MeasureSpec
- getMode(int measureSpec):根据MeasureSpec来得到SpecMode
- getSize(int measureSpec):根据MeasureSpec来得到SpecSize
SpecMode:测量模式,占MeasureSpec的高2位。测量模式的类型有3种:
- UNSPECIFIED:父容器不对View有任何限制,一般用于系统内部,表示一种测量的状态
- EXACTLY:父视图为子视图指定一个确切的尺寸,子视图大小必须在该指定尺寸内。对应了LayoutParams中的match_parent和具体数值这两种模式
- AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。它对应了LayoutParams中的wrap_content
SpecSize:测量大小,占measureSpec的低30位
3.MeasureSpec值的计算
对于DecorVeiw和普通View来说,MeasureSpec的计算稍微有点区别。
- Decor View:MeasureSpec由窗口尺寸和自身的LayoutParams来共同确定
- 普通View:子View的LayoutParams和父容器的MeasureSpec共同确定
然后我们从源码来验证普通View的MeasureSpec的计算,先来看看View中getChildMeasureSpec的源码:
1 | /** |
上述的源码其实并不难理解,从源码中我们也可以看出子元素的MeasureSpec是由父容器的MeasureSpec和子元素的LayoutParams共同确定的。而具体的创建规则如下(根据源码整理得出):
前提 | 子View的MeasureMode | 子View的MeasureSize |
---|---|---|
子View采用具体数值 | EXACTLY | 自身设置的具体数值 |
子View采用match_parent时 | 父容器的测量模式 | 父容器剩余空间 |
子View采用wrap_content时 | AT_MOST | 父容器剩余空间 |
2.View的工作流程
View的工作流程主要是指下面三大流程:
- measure:测量,确定View的测量宽/高
- layout:布局,确定View的最终宽/高和四个顶点的位置
- draw:绘制,将View绘制到屏幕上
那么三大流程的入口在哪呢?
答:View的绘制是从ViewRootImpl类的performTraversals方法开始的,通过阅读performTraversals的源码我们可以知道,在里面会依次调用performMeasure,performLayout,performDraw来分别完成顶级View的measure,layou和draw这三大流程。然后在performMeasure,performLayout,performDraw中又会各自调用measure,layout,draw方法。performTraversals的大致流程如下:
从流程图我们可以看出,三大流程的入口分别是measure(),layout(),draw()方法,那么接下来让我们一步一步的从源码来分析每个流程。
2.1 measure过程
measure过程根据View的类型分为两种情况:
- 单一View:通过measure过程就完成了其测量过程
- ViewGroup:除了完成自己的测量过程外,还会遍历调用子元素的measure方法,各个子元素再递归执行该流程。
由于两种情况都涉及到了子元素的measure方法,所以我们首先来分析单一View的measure过程。
1. 单一View的measure过程
单一View的measure过程从measure方法开始,来看看View的measure方法:
源码:View#measure()
1 | /** |
从上面源码我们可以发现measure方法是一个final类型的方法,这就意味着子类不能重写此方法,另外在这个方法中我们还能发现会调用onMeasure方法,因此来看看onMeasure方法的实现:
1 | /** |
这个方法很简单,setMeasuredDimension方法会设置View的测量值,重点看getDefaultSize这个方法:
1 | /** |
可以发现,上面这个方法的逻辑也很简单,首先根据当前View的宽/高规格来得到测量模式和测量大小,然后根据测量模式来计算View的宽/高。正常情况下,我们只需要关注AT_MOST或EXACTLY两种情况就行,在代码中可以看到,这两个情况下View的测量大小都是measureSpec中的SpecSize。所以最终View的测量宽/高也会由SpecSize决定,这个时候我们回顾下上面提到的子View的MeasureSpec计算,当View布局中使用的是wrap_content,那么specMode就会是AT_MOST模式,在这种模式下的View的specSize是parentSize,也就是View的大小是父容器当前剩余的大小,意思就是在这个情况下当View设置为wrap_content相当于使用match_parent。what?这时候你肯定一脸懵逼,那这个wrap_content有什么用?
其实解决方法也是很简单的。在继承View的自定义控件时,重写onMeasure方法并在wrap_content情况下设置默认的内部宽/高即可,对于非wrap_content情况,则按照系统测量值即可。这时候你可能会有疑问:我们平时使用控件的时候,怎么可以设置wrap_content来达到自适应的要求呢?这是因为我们平时使用的控件,如TextView等,从它们的源码可以发现都是针对wrap_content情形,在它们的onMeasure方法做了特殊处理。
从我们也可以得出结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于match_parent。
至于在View中的onMeasure方法中的getSuggestedMinimumWidth和getSuggestedMinimumHeight方法我们就不深究其实现了,其实这两个方法都是在测量模式下为UNSPECIFIED的默认大小,一般用于系统内部的测量过程。至此,单一VIew的measure过程已经完成,其流程图如下:
2. ViewGroup的measure过程
对于ViewGroup来说,measure过程分为两步走:一是遍历测量所有子View的尺寸,而是合并所有子VIew的尺寸,最终得到ViewGroup的测量值。其最开始也是从measure方法开始的:
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { |
细心的你肯定会发现这方法怎么似曾相识。没错,这与上面单一View measure过程中讲的measure()方法是一致,就是ViewGroup父类View中的measure方法。阅读源码时你会发现在ViewGroup根本找不到这个方法,其实找不到就对了,这是因为在上面我们提到过View中的measure是final方法,所以不能重写该方法。但是ViewGroup的measure过程的入口也是从这个方法开始的。
在measure中我们看到了熟悉的onMeasure方法,想必你又会开始在ViewGroup中寻找onMeasure方法,那么很遗憾的告诉你,在ViewGroup中也没有onMeasure()方法。what? 这是因为不同的ViewGroup子类(LinearLayout、RelativeLayout 、自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View
的测量方法各有不同,所以在ViewGroup中并没有像单一View中measure过程那样对onMeasure()做统一的实现。这也是单一View的measure过程与ViewGroup的measure过程最大的不同。
所以在自定义ViewGroup中,关键性的步骤就是:需要根据需求来重写onMeasure()从而实现子View的测量逻辑,而一般重写onMeasure分为三部曲,如下所示:
1 |
|
从上面的代码中可以发现,第一步和第三步都是直接调用系统方法,只有第二步需要根据具体需求来合并所有子View的尺寸大小。
现在让我们来看看第一步方法具体是实现的:
1 | /** |
从上面代码可以在measureChildren中,ViewGroup在measure时,会对每一个子View进行measure,然后调用measureChild这个方法,这个方法也很容易理解,首先取出子View的LayoutParams,然后根据子View的LayoutParams和父容器的measureSpec通过getChildMeasureSpec计算出子View的measureSpec,至于getChildMeasureSpec的工作流程我们在上面对View的MeasureSpec的计算已经进行了详细分析,这里就不在进行分析。当计算出子View的measureSpec后就传递给View的measure方法来进行测量,测量过程就是上面分析的单一View的measure过程。遍历并测量所有子View后第一步就完成了。
然后进行第二步操作:合并所有子View的尺寸大小,最终得到ViewGroup的测量值,这里的源码分析需要根据具体的ViewGroup子类才能进行分析(下文会分析)。当得到ViewGroup的测量值后就可以调用setMeasuredDimension设置测量后ViewGroup宽/高值。至此三部曲结束,ViewGroup的measure过程也完成了,其流程图如下:
measure完成后,通过getMeasureWidth或getMeasureHeiht方法可以获取到View的测量宽/高。但是在某些情况下需要多次measure才能确定最终测量宽高,此时measure过程得到的宽高是不准确的,所以更好的做法是:在Layout方法中获取最终的宽高
2.2 layout过程
layout过程跟measure过程类型,也可以根据View的类型分为两种情况:
- 单一View:仅计算本身View的位置
- ViewGroup: 除了计算自身View的位置外,还需要确定子View在父容器的位置
同样的我们从单一View的layout过程开始分析:
1. 单一View的layout过程
单一View的layout过程是从layout方法开始的,我们来看看View中的layout方法:
源码:View#layout
1 | //设置View本身四个顶点的位置 |
从上面我们可以发现在layout中通过setOpticalFrame和setFrame两个方法来确定View的位置。而从源码中我们可以知道setOpticalFrame内部其实是调用了setFrame方法。
1 | //最终确定View本身的位置 |
在setFrame方法中会对mLeft,mTop,mRight,mBottom进行初始化,然后确定View的位置。让我们回顾下单一View的layout任务,仅计算本身View的位置,这样岂不是任务已经完成了?
没错,既然计算出本身View的位置,按理说单一View就需要再干其它事情了,但是从源码中你会发现在layout方法中,当计算出View的位置后,还是会继续执行onlayout方法。既然已经任务完成,那就什么也不干!没错View就是这么做的,在源码中可以发现onlayout方法其实就是一个空实现而已。如下所示:
1 | //由于在layout中已经对自身View进行了位置计算,所以该方法为空实现 |
到此,单一View的layout过程已经分析完了,其流程如下所示:
2. ViewGroup的layout过程
ViewGroup的layout过程从其任务来讲可以分成两部分:
- 计算自身ViewGroup的位置
- 遍历子View,确定自身子View在ViewGroup的位置
所以我们一步一步来分析,首先是计算自身ViewGroup的位置。从上面单一View的layout过程,我们应该可想而知,计算自身ViewGroup的位置,也应该是从layout方法开始的:
源码:ViewGroup#layout
1 |
|
从上面可以发现,ViewGroup的layout是final方法,所以表示子类不能重写该方法,然后里面会调用super.layout,即调用父类View的layout方法。
源码:View#layout
1 | //设置View本身四个顶点的位置 |
在上面我们已经分析过了View的layout方法了,这里就不再展开继续讲了。当通过setFrame方法确定ViewGroup自身的位置时,ViewGroup的layout过程已经完成第一部分的任务了。于是开始第二部分遍历子View,确定自身子View在ViewGroup的位置的任务。这个任务通过谁来完成呢?
聪明的你一定想到了!没错就是onLayout方法,在单一View的过程中在计算出自身的位置后任务就完成了,所以在View中onLayout方法为空实现,而在ViewGroup中由于还需要计算出子View在自身的位置,所以并不是空实现,如下:
1 | // 对于ViewGroup的layout过程:由于确定位置与具体布局有关,所以onLayout在ViewGroup为抽象方法 |
这时候估计你又有疑问了,what?实现在哪?其实这和ViewGroup的onMeasure方法类型,onLayout确定子View的位置与具体布局有关,所以在ViewGroup中onLayout方法只是一个抽象方法。
既然是个抽象方法,那就意味着在自定义ViewGroup时必须要重写onLayout方法,而重写的任务总的来说可以分为三部分:
- 遍历子View
- 计算当前子View的四个位置值
- 确定子View在父容器的位置
示例代码如下:
1 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
而child.layout其实就是调用View的layout方法 ,就是单个View的layout过程。单有示例代码估计还很能使你信服,就让我们来瞧瞧LinearLayout是如何来重写onLayout方法的。
1 |
|
用过LinearLayout自然知道我们是可以设置布局是水平还是垂直,所以在onLayout方法对这两个布局做了分别处理,在这里我们选择layoutVertical来追踪其实现。
源码:LinearLayout#layoutVertical&setChildFrame
1 | void layoutVertical(int left, int top, int right, int bottom) { |
而setChildFrame其实就是调用了子View的layout,然后在子view的layout中会通过setFrame去设置子元素四个顶点的位置,这样一来子元素在父元素的位置就确定了。其实现总体上来说是跟上面讲到的任务三步骤是一致的。这么一来ViewGroup的layout过程大功告成,其流程如下所示:
2.3 draw过程
前面我们分析了View三大流程的measure,,layout过程,最后当然要显示出来,故draw过程作用就在于此,将View绘制到屏幕上来,与前面两个过程一致,draw过程也分为两大类型:
- 单一View:仅绘制View本身
- ViewGroup:除了绘制自身View外,还需绘制所有子View
两大类型的View绘制过程都可以归纳为如下几步:
- 绘制背景
- 绘制自己
- 绘制子View
- 绘制装饰
然后我们还是根据老规则,从单一View的draw过程开始分析。
1. 单一View的draw过程
单一View的draw过程也是从draw方法入口进入的,如下:
源码:ViewGroup#draw
1 | public void draw(Canvas canvas) { |
上面贴出了单一View的draw()方法中的关键性代码,也可以证明前面提到的draw步骤。然后我们继续分析draw方法中的4个步骤调用的方法:
1 | /** |
结合注释和代码应该不难理解上面4个方法,不过需注意的两点:一是onDraw方法,由于每个View的内容各不相同,所以该方法为空实现,当自定义View时,必须重写而且一般来说只需要重写这个方法就行了。二是dispatchDraw方法,我们知道这个方法是用来绘制子View的,但是单一View是没有子View的,所以该方法自然而然为空实现。至此,单一View的draw过程就分析完毕了,其流程如下:
2. ViewGroup的draw过程
ViewGroup的draw过程也是从draw开始的,当时ViewGroup中并没有重写父类View的draw()方法,因此ViewGroup的draw过程的入口是View的draw()方法,源码与上面子View的draw过程贴出来的draw方法是一样的,所以这里就不再贴出来了,然后ViewGroup都没有重写drawBackground,onDraw,onDrawForeground方法,所以这些方法都是与单一View的draw过程是一致的,这里不再过多描述。
ViewGroup和View的draw过程最大的不同就是绘制子View的dispatchDraw方法,让我们来瞧瞧ViewGroup中是如何来绘制子View的:
源码:ViewGroup#dispatchDraw
1 | //遍历并绘制子View,一般不需要重写该方法 |
dispatchDraw方法代码很多,在这里只贴出关键代码,可以看到绘制子view与measure,layout过程相似,首先遍历子View,然后进行子视图的绘制。而drawChild就是简单调用子View的draw方法。然后就是单一View的draw过程。
1 | //绘制子View |
至此ViewGroup的draw过程分析完毕。其流程如下:
到这里我们就把View的三大流程:measure,layout,draw都分析完啦!最后贴上总的工作流程图: