转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/16330267html
在上一篇文章中,我带着你们一块儿剖析了一下LayoutInflater的工做原理,能够算是对View进行深刻了解的第一步吧。那么本篇文章中,咱们将继续对View进行深刻探究,看一看它的绘制流程究竟是什么样的。若是你尚未看过个人上一篇文章,能够先去阅读 Android LayoutInflater原理分析,带你一步步深刻了解View(一) 。java
相信每一个Android程序员都知道,咱们天天的开发工做当中都在不停地跟View打交道,Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View的,如TextView、Button、ImageView、ListView等。这些控件虽然是Android系统自己就提供好的,咱们只须要拿过来使用就能够了,但你知道它们是怎样被绘制到屏幕上的吗?多知道一些老是没有坏处的,那么咱们赶快进入到本篇文章的正题内容吧。android
要知道,任何一个视图都不可能凭空忽然出如今屏幕上,它们都是要通过很是科学的绘制流程后才能显示出来的。每个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),下面咱们逐个对这三个阶段展开进行探讨。程序员
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于肯定视图的宽度和高度的规格和大小。canvas
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,以下所示:微信
1. EXACTLY框架
表示父视图但愿子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员固然也能够按照本身的意愿设置成任意的大小。ide
2. AT_MOST函数
表示子视图最多只能是specSize中指定的大小,开发人员应该尽量小得去设置这个视图,而且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员固然也能够按照本身的意愿设置成任意的大小。布局
3. UNSPECIFIED
表示开发人员能够将视图按照本身的意愿设置成任意的大小,没有任何限制。这种状况比较少见,不太会用到。
那么你可能会有疑问了,widthMeasureSpec和heightMeasureSpec这两个值又是从哪里获得的呢?一般状况下,这两个值都是由父视图通过计算后传递给子视图的,说明父视图会在必定程度上决定子视图的大小。可是最外层的根视图,它的widthMeasureSpec和heightMeasureSpec又是从哪里获得的呢?这就须要去分析ViewRoot中的源码了,观察performTraversals()方法能够发现以下代码:
能够看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在建立ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。而后看下getRootMeasureSpec()方法中的代码,以下所示:
能够看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。而且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图老是会充满全屏的。
介绍了这么多MeasureSpec相关的内容,接下来咱们看下View的measure()方法里面的代码吧,以下所示:
注意观察,measure()这个方法是final的,所以咱们没法在子类中去重写这个方法,说明Android是不容许咱们改变View的measure框架的。而后在第9行调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小,以下所示:
这里传入的measureSpec是一直从measure()方法中传递过来的。而后调用MeasureSpec.getMode()方法能够解析出specMode,调用MeasureSpec.getSize()方法能够解析出specSize。接下来进行判断,若是specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。以后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
固然,一个界面的展现可能会涉及到不少次的measure,由于一个布局中通常都会包含多个子视图,每一个视图都须要经历一次measure过程。ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小,以下所示:
这里首先会去遍历当前布局下的全部子视图,而后逐个调用measureChild()方法来测量相应子视图的大小,以下所示:
能够看到,在第4行和第6行分别调用了getChildMeasureSpec()方法来去计算子视图的MeasureSpec,计算的依据就是布局文件中定义的MATCH_PARENT、WRAP_CONTENT等值,这个方法的内部细节就再也不贴出。而后在第8行调用子视图的measure()方法,并把计算出的MeasureSpec传递进去,以后的流程就和前面所介绍的同样了。
固然,onMeasure()方法是能够重写的,也就是说,若是你不想使用系统默认的测量方式,能够按照本身的意愿进行定制,好比:
这样的话就把View默认的测量流程覆盖掉了,无论在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
须要注意的是,在setMeasuredDimension()方法调用以后,咱们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此以前调用这两个方法获得的值都会是0。
因而可知,视图大小的控制是由父视图、布局文件、以及视图自己共同完成的,父视图会提供给子视图参考的大小,而开发人员能够在XML文件中指定视图的大小,而后视图自己会对最终的大小进行拍板。
到此为止,咱们就把视图绘制流程的第一阶段分析完了。
measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的同样,这个方法是用于给视图进行布局的,也就是肯定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程,以下所示:
layout()方法接收四个参数,分别表明着左、上、右、下的坐标,固然这个坐标是相对于当前视图的父视图而言的。能够看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。那么咱们来看下layout()方法中的代码是什么样的吧,以下所示:
在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以肯定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会在第11行调用onLayout()方法,正如onMeasure()方法中的默认行为同样,也许你已经火烧眉毛地想知道onLayout()方法中的默认行为是什么样的了。进入onLayout()方法,咦?怎么这是个空方法,一行代码都没有?!
没错,View中的onLayout()方法就是一个空方法,由于onLayout()过程是为了肯定视图在布局中所在的位置,而这个操做应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,咱们来看下ViewGroup中的onLayout()方法是怎么写的吧,代码以下:
能够看到,ViewGroup中的onLayout()方法居然是一个抽象方法,这就意味着全部ViewGroup的子类都必须重写这个方法。没错,像LinearLayout、RelativeLayout等布局,都是重写了这个方法,而后在内部按照各自的规则对子视图进行布局的。因为LinearLayout和RelativeLayout的布局规则都比较复杂,就不单独拿出来进行分析了,这里咱们尝试自定义一个布局,借此来更深入地理解onLayout()的过程。
自定义的这个布局目标很简单,只要可以包含一个子视图,而且让子视图正常显示出来就能够了。那么就给这个布局起名叫作SimpleLayout吧,代码以下所示:
代码很是的简单,咱们来看下具体的逻辑吧。你已经知道,onMeasure()方法会在onLayout()方法以前调用,所以这里在onMeasure()方法中判断SimpleLayout中是否有包含一个子视图,若是有的话就调用measureChild()方法来测量出子视图的大小。
接着在onLayout()方法中一样判断SimpleLayout是否有包含一个子视图,而后调用这个子视图的layout()方法来肯定它在SimpleLayout布局中的位置,这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别表明着子视图在SimpleLayout中左上右下四个点的坐标。其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法获得的值就是在onMeasure()方法中测量出的宽和高。
这样就已经把SimpleLayout这个布局定义好了,下面就是在XML文件中使用它了,以下所示:
能够看到,咱们可以像使用普通的布局文件同样使用SimpleLayout,只是注意它只能包含一个子视图,多余的子视图会被舍弃掉。这里SimpleLayout中包含了一个ImageView,而且ImageView的宽高都是wrap_content。如今运行一下程序,结果以下图所示:
OK!ImageView成功已经显示出来了,而且显示的位置也正是咱们所指望的。若是你想改变ImageView显示的位置,只须要改变childView.layout()方法的四个参数就好了。
在onLayout()过程结束后,咱们就能够调用getWidth()方法和getHeight()方法来获取视图的宽高了。说到这里,我相信不少朋友长久以来都会有一个疑问,getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之因此会相同基本都是由于布局设计者的编码习惯很是好,实际上它们之间的差异仍是挺大的。
首先getMeasureWidth()方法在measure()过程结束后就能够获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是经过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是经过视图右边的坐标减去左边的坐标计算出来的。
观察SimpleLayout中onLayout()方法的代码,这里给子视图的layout()方法传入的四个参数分别是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),所以getWidth()方法获得的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,因此此时getWidth()方法和getMeasuredWidth() 获得的值就是相同的,但若是你将onLayout()方法中的代码进行以下修改:
这样getWidth()方法获得的值就是200 - 0 = 200,不会再和getMeasuredWidth()的值相同了。固然这种作法充分不尊重measure()过程计算出的结果,一般状况下是不推荐这么写的。getHeight()与getMeasureHeight()方法之间的关系同上,就再也不重复分析了。
到此为止,咱们把视图绘制流程的第二阶段也分析完了。
measure和layout的过程都结束后,接下来就进入到draw的过程了。一样,根据名字你就可以判断出,在这里才真正地开始对视图进行绘制。ViewRoot中的代码会继续执行并建立出一个Canvas对象,而后调用View的draw()方法来执行具体的绘制工做。draw()方法内部的绘制过程总共能够分为六步,其中第二步和第五步在通常状况下不多用到,所以这里咱们只分析简化后的绘制过程。代码以下所示:
能够看到,第一步是从第9行代码开始的,这一步的做用是对视图的背景进行绘制。这里会先获得一个mBGDrawable对象,而后根据layout过程肯定的视图位置来设置背景的绘制区域,以后再调用Drawable的draw()方法来完成背景的绘制工做。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中经过android:background属性设置的图片或颜色。固然你也能够在代码中经过setBackgroundColor()、setBackgroundResource()等方法进行赋值。
接下来的第三步是在第34行执行的,这一步的做用是对视图的内容进行绘制。能够看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也能够理解,由于每一个视图的内容部分确定都是各不相同的,这部分的功能交给子类来去实现也是理所固然的。
第三步完成以后紧接着会执行第四步,这一步的做用是对当前视图的全部子视图进行绘制。但若是当前的视图没有子视图,那么也就不须要进行绘制了。所以你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。
以上都执行完后就会进入到第六步,也是最后一步,这一步的做用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不必定是ListView或者ScrollView,为何要绘制滚动条呢?其实无论是Button也好,TextView也好,任何一个视图都是有滚动条的,只是通常状况下咱们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就再也不贴出来了,由于咱们的重点是第三步过程。
经过以上流程分析,相信你们已经知道,View是不会帮咱们绘制内容部分的,所以须要每一个视图根据想要展现的内容来自行绘制。若是你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,而且在里面执行了至关很多的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会做为参数传入到onDraw()方法中,供给每一个视图使用。Canvas这个类的用法很是丰富,基本能够把它当成一块画布,在上面绘制任意的东西,那么咱们就来尝试一下吧。
这里简单起见,我只是建立一个很是简单的视图,而且用Canvas随便绘制了一点东西,代码以下所示:
能够看到,咱们建立了一个自定义的MyView继承自View,并在MyView的构造函数中建立了一个Paint对象。Paint就像是一个画笔同样,配合着Canvas就能够进行绘制了。这里咱们的绘制逻辑比较简单,在onDraw()方法中先是把画笔设置成黄色,而后调用Canvas的drawRect()方法绘制一个矩形。而后在把画笔设置成蓝色,并调整了一下文字的大小,而后调用drawText()方法绘制了一段文字。
就这么简单,一个自定义的视图就已经写好了,如今能够在XML中加入这个视图,以下所示:
将MyView的宽度设置成200dp,高度设置成100dp,而后运行一下程序,结果以下图所示:
图中显示的内容也正是MyView这个视图的内容部分了。因为咱们没给MyView设置背景,所以这里看不出来View自动绘制的背景效果。
固然了Canvas的用法还有不少不少,这里我不可能把Canvas的全部用法都列举出来,剩下的就要靠你们自行去研究和学习了。
到此为止,咱们把视图绘制流程的第三阶段也分析完了。整个视图的绘制过程就所有结束了,你如今是否是对View的理解更加深入了呢?感兴趣的朋友能够继续阅读 Android视图状态及重绘流程分析,带你一步步深刻了解View(三) 。
第一时间得到博客更新提醒,以及更多技术信息分享,欢迎关注个人微信公众号,扫一扫下方二维码或搜索微信号guolin_blog,便可关注。