前言
这几日又苦心学习了下View的工作原理,我分为两类,一是View的工作流程,二是自定义View。至于事件分发,事件冲突这些知识,已经有了日期规划,须在下周进行详细学习。
一. View的工作流程入口以及思想综述
有时,View的工作流程也被称为View的绘制流程,但我个人不太喜欢这种叫法,因为绘制在View中是有专门的方法,也即draw方法,所以容易引起歧义,故后面统一称为View的工作流程,工作流程包括绘制流程。
1. ViewRoot
ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带。DecorView,之前在学习事件分发的入口时也学到过,它是顶级VIew,是一个FrameLayout,内部包含一个Vertical的LinearLayout,这个LinearLayout包含上下两部分,上面是标题栏,下面是内容栏,内容栏是我们平时添加布局的地方。由于DecorView是顶级View,因此View层的事件都需要先经过DecorView,才能传递给我们的View,这也不难解释为什么之前学习事件分发的时候,也有DecorView的相关知识。
2. 总体的绘制流程
这里DecorView其实不重要,重要的是ViewRoot。因为View的三大工作流程均是通过ViewRoot来完成的。具体来说,就是,View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout和draw三个过程才能最终将一个View绘制出来(从这个角度来说,把View的工作流程也称为绘制流程,也变得合情合理了,因为绘制出来才是最终的结果)。measure是用来测量view的宽和高,layout用来确定View在父容器中的放置位置,draw用来将View绘制在屏幕上。关于performTraversals的工作流程,可以参考下图
其中,performMeasure,performLayout和performDraw方法分别完成顶级View也就是DecorView的三大绘制流程,然后分别调用onMeasure、onLayout、onDraw来完成子View的三大绘制流程。三个方法都是递归调用的,会遍历整棵View树。
二. MeasureSpec的思想以及具体使用规则
MeasureSpec决定了子View的大小。具体是这样决定的:View的LayoutParams和父容器的SpecMode,来决定View的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高。记住,首先是得到MeasureSpec,然后再根据它去得到最终的测量宽高。
1. 总体介绍下MeasureSpec
MeasureSpec是一个32位的int值,高2位表示specMode,低30位表示specSize。为何将specMode和specMode打包成一个int值呢?还是为了避免过多的对象内存分配。
2. 关于SpecMode
SpecMode有三类,具体为
- EXACTLY
分两种情况,
假如父容器指定了EXACTLY,此时如果子View的LayoutParams为match_parent或者是确切的值,比如30dp,那么子View的SpecMode就是EXACTLY。
假如父容器指定了AT_MOST,此时如果子View的LayoutParams为确切的值,比如30dp,那么子View的SpecMode就是EXACTLY。多出AT_MOST的部分不显示就好了。 - AT_MOST
分两种情况,
假如父容器的SpecMode是AT_MOST,那么假如子View的LayoutParams是match_parent或者wrap_content,那么子VIew的SpecMode也是AT_MOST。
假如父容器的SpecMode是EXACTLY,那么假如子View的LayoutParams是wrap_content,那么子View的SpecMode也是AT_MOST。 - UNSPECIFIED
这个主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。
从上面的分析也可得出以下结论:
- 子View的MeasureSpec由父View的MeasureSpec和子View的LayoutParams共同决定
- 一般来说,关于SpecMode,我们只需要关注AT_MOST和EXACTLY两种类型
- 父View的SpecMode,和子View的LayoutParams,决定子View的MeasureSpec的所有转换规则,如下图所示
[图片]
三. View的工作流程
View的工作流程主要是指measure、layout、draw三大流程,有时也被称为View的绘制流程。这三步走完之后,View会被最终绘制到屏幕上。
1. measure过程
View的measure只需要完成自己的测量过程,而如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个流程。下面分情况介绍。
1.1 View的measure流程
View的measure流程是由measure方法完成,measure方法是一个final方法,内部调用了onMeasure方法,onMeasure方法不是final类型的,所以子类可以重写。View的onMeasure方法通过setMeasuredDimension,来设置View的宽高的测量值,setMeasuredDimension方法,需要传入参数来得到View的宽高的测量值,具体就是getDefaultSIze方法,内部实现是返回SpecSize也即MeasureSpec中的size。也就是说View的宽高,是由MeasureSpec的specSize来决定的,这也印证了前文所说的观点。但这样也带来了一个问题,即
默认情况下,在布局中使用wrap_content就相当于使用match_parent
为什么呢?原因是,参考前文MeasureSpec的计算规则,当子View的LayoutParams为wrap_content的时候,不管父容器是EXACTLY还是AT_MOST,最终计算出的子View的SpecMode都是AT_MOST。而此AT_MOST,SpecSize是parentSize,也就是父容器留给子View的大小,这就和子View为match_parent所计算出的SpecSize结果是一样的。解决这个问题的方法,一般是当子View的LayoutParams为wrap_content,且子View的SpecMode为AT_MOST的时候,为子View的大小设置一个默认值,在setMeasuredDimension方法中传入,就可以使得wrap_content发挥出和match_parent不一样的效果了。
1.2 ViewGroup的measure流程
ViewGroup除了需要完成自己的measure过程之外,还需要完成所有子View的measure方法,方式是递归遍历。
ViewGroup还是一个抽象类,没有重写View的onMeasure方法,但提供了一个measureChildren方法。measureChildren的思想是这样的:首先得到子View的LayoutParams,然后再通过getChildMeasureSpec来得到子View的MeasureSpec,接着将MeasureSpec直接传递给子View的measure方法,完成子View的测量。
因为不同的ViewGroup有不同的布局特性,导致它们的测量细节各不相同,因此ViewGroup没有像View一样对onMeasure方法做统一的实现,而是不同的ViewGroup实现类自己实现属于自己特点的onMeasure方法。
1.3 对measure过程做个总结
measure过程是三大流程中最复杂的一个,不仅涉及递归遍历,还需要区分单个View和ViewGroup的不同情况。measure完成以后,通过getMeasuredWidth或者getMeasuredHeight就可以正确的获取View的测量宽高,记住,这里是测量宽高,并非最终宽高。因为在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情况下,在onMeasure方法中拿到的测量宽高很有可能是不准确的。因此,一般可以在onLayout方法中去获取View的测量宽高。
1.4 题外话,如何在ui控件比如Activity的生命周期方法里面,得到某个View的最终宽高?
总结:在onCreate、onStart和onResume中均无法正确得到某个View的宽高信息
原因:因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume的时候,某个View已经测量完成了,如果没有测量完成,那么获得的宽高值就为0。这也是异步的特点。
那么具体如何实现此目的呢?有如下4个方法
- Activity/View的onWindowFocusChanged方法
这个方法的含义是,View已经初始化完毕了,宽高已经准备好了。当hasFocus为true的时候,就可以调用view的getMeasuredWidth方法来得到宽,高也是同理。
onWindowFocusChanged在Activity的窗口得到焦点和失去焦点的时候均会被调用,即会被调用多次 - view的post(runnable)方法
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。 - ViewTreeObserver
当View树的状态发生改变或者View树的内部View可见性发生改变的时候,OnGlobalLayoutListener接口的的onGlobalLayout方法会被回调,因此在这里可以获取View的宽高。
因为View树会发生多次改变,因此onGlobalLayout方法会被调用多次。 - view的measure(int widthMeasureSpec,int heightMeasureSpec)方法
这种方式的核心思想是手动对View调用measure方法,来得到View的宽高,这种方式比较复杂,平时用的也比较少。只需大概了解下即可。
当View的LayoutParams为match_parent的时候,无法使用此方法。因为不知道parentSize大小
当View的LayoutParams为wrap_content或为具体值的时候,首先调用MeasureSpec的makeMeasureSpec方法得到MeasureSpec,然后将此MeasureSpec传入measure方法,即可得到宽高。
2. layout过程
layout就是ViewGroup用来确定子元素的位置的,layout方法确定View本身的位置,而layout调用的onLayout方法则会确定所有子View的位置。
它会首先通过setFrame方法来确定自身四个顶点的位置,四个顶点位置确定了,那么此View在父容器中的位置也就确定了,然后调用onLayout方法,用于确定子View的布局。和onMeasure一样,onLayout也和具体的布局有关,因此ViewGroup也没有实现onLayout,当然View虽然实现了onMeasure方法,但是也没有实现onLayout方法。
关于测量宽高和最终宽高
在View的默认实现中,View的测量宽高和最终宽高是相等的,只是测量宽高形成于View的measure过程,最终宽高形成于View的layout过程,即赋值时机不同,但都是从同一个变量取得值。在日常开发中,可以认为测量宽高等于最终宽高,但有特殊情况。
- 假如在layout方法中,是这样调用的
layout(l,t,r + 100,b + 100)
那么在任何情况下,View的最终宽高总是会比测量宽高大100px,虽然这样会导致View显示不正常,且没有实际意义。 - 还有,View可能需要多次measure才能确定自己的测量宽高,那么在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致。
因此,最终宽高,还是建议在layout方法执行完之后,调用getWidth或getHeight来得到。
3. draw过程
draw遵循如下4步
- 绘制背景: background.draw(canvas)
- 绘制自身: onDraw
- 绘制children: dispatchDraw
- 绘制装饰: onDrawScrollBars
View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子View的draw方法。
关于draw的一个小方法:setWillNotDraw(boolean willNotDraw)
如果一个View不需要绘制任何内容,那么可以通过此方法,设置willNotDraw这个标记为为true,系统会进行相应的优化。默认情况下是false,即不进行优化。
所以,当我们的自定义控件继承ViewGroup,并且本身不具备绘制功能的时候,就可以开启这个标记位,从而便于系统进行后续的优化。
四. 自定义View
自定义View即不满足系统原生控件的功能,继承View去自定义控件的样式功能等。
1. 自定义View的种类
总共可分为四种
- 继承VIew的(需要重写onDraw方法,同时建议重写onMeasure,来处理wrap_content)
- 继承ViewGroup的(最复杂,需要重写onMeasure、onLayout方法)
- 继承特定系统View的,比如TextView(不需要重写onMeasure方法,但一般需要重写onDraw方法)
- 继承特定系统ViewGroup的(比如继承LinearLayout方法,不需要强制重写onMeasure和onLayout)
2. 自定义View的注意事项
- 记得在onMeasure方法中处理wrap_content,否则wrap_content就无法达到预期效果
- 记得支持padding和margin
直接继承VIew的控件中,记得在draw方法中处理padding,不然padding是没有效果的。
直接继承ViewGroup的控件中,需要在onMeasure和onLayout中考虑padding和子元素margin的影响。 - View中如果有线程或者动画,可以在View的onDetachedFromWindow方法中停止,以避免内存泄漏
当包含此View的Activity退出或者当前View被remove的时候,此View的onDetachedFromWindow方法会被调用。与此方法对应的是onAttachedToWindow
3. 实践
关于这里,之前我写过一篇直接继承ViewGroup来实现流式布局的文章,可以参考这篇文章