9

Android开发之自定义控件(一)---onMeasure详解

 3 years ago
source link: https://blog.csdn.net/dmk877/article/details/49558367
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

           话说一个有十年的编程经验的老汉,决定改行书法,在一个热火炎炎的中午,老汉拿着毛笔,在一张白纸上写了个“Hello World!”,从此开启了他的书法旅程。那么问题来了请问自定义一个控件需要怎样的流程?我们经常说自定义控件,那么究竟怎样去自定义一个控件?可能大家都听过自定义控件是android开发人员的一个槛,其实对于这个我们个人而言是赞同的,因为如果你掌握了自定义控件那么你对android的了解肯定更深了一个档次,为什么这样说呢?学过自定义控件你自然会知道。自定义控件相对来说还是比较复杂的,可能在阅读第一遍你理解的不是特别好,但是不要灰心你就会发现很清晰,我相信认真读完此博客,你肯定会有收获。如有谬误欢迎批评指针,如有疑问欢迎留言,谢谢

Android开发之自定义控件(二)---onLayout详解

通过本篇博客你将学到以下知识点:

①自定义控件onMeasure的过程

②彻底理解MeasureSpec

③了解View的绘制流程

④对测量过程中需要的谷歌工程师给我们准备好的其它的一些方法的源码深入理解。

        为了响应文章的开头,我们从一个“Hello World!”的小例子说起,这个例子我们自定义一个View让它显示“Hello World!”非常简单,代码如下

它的布局文件如下

运行结果如下

Center

         这样一个大大的"Hello World!"呈现在我们面前,可能有的人会问到底怎样去自定义一个控件呢?别急我们慢慢的,一点一点的去学习,首先你可以想象一下,假如我要求你去画一个空心的圆,你会怎么做,首先你要拿张白纸,然后你会问我圆的半径多大?圆的位置在哪?圆的线条颜色是什么?圆的线条粗细是多少?等我把这些问题都告诉你之后,你就会明白要求,并按照这个要求去画一个圆。我们自定义控件呢,也是这样需要下面三个步骤:

①重写onMeasure(获得半径的大小)

②重写onLayout(获得圆的位置)

③重写onDraw(用实例化的画笔包括:颜色,粗细等去绘画)

待这三个方法都重写完后我们的自定义控件就完成了,为了讲的能够详细我们这一篇专门来讲解onMeasure以及和其相关的方法,首先我们需要明白的是Android给我提供了可以操纵控件测量的方法是onMeasure()方法,在上面的自定义控件中我们采用了其默认的实现

看到这,可能大部分人都要问,这里的widthMeasureSpec和heightMeasureSpec是从何处来?要到哪里去?其实这两个参数是由View的父容器传递过来的测量要求,在上述自定义控件中也就是我们的LinearLayout,为什么这么说?这么说是有依据的我们都知道在Activity中可以调用其setContentView方法为界面填充一个布局

在setContentView方法中做了哪些事情呢?我们看看他的源码

我们看到它调用了getWindow方法,没什么可说的,跟着步骤去看getWindow方法的源码

这里返回一个Window实例,其本质是继承Window的PhoneWindow,所以在Acitivity中的setContentView中getWindow.setContentView()getWindow.setContentView()其实就是PhoneWindow.setContentView()我们来Look Look它的代码

该方法首先会判断是否是第一次调用setContentView方法,如果是第一次调用则调用installDecor()方法,否则将mContentParent中的所有View移除掉

然后调用LayoutInflater将我们的布局文件加载进来并添加到mContentParent视图中。跟上节奏我们来看看installDecor()方法的源码

可以发现在这个方法中首先会去判断mDecor是否为空如果为空会调用generateDecor方法,它干了什么呢?

可以看到它返回了一个DecorView,DecorView类是FrameLayout的子类,是一个内部类存在于PhoneWindow类中,这里我们知道它是FrameLayout的子类就ok了。

在installDecor方法中判断了mDecor是否为空后,接着会在该方法中判断mContentParent是否为空,如果为空就会调用generateLayout方法,我们来看看它做了什么。。。

根据窗口的风格修饰类型为该窗口选择不同的窗口布局文件(根视图),这些窗口修饰布局文件指定一个用来存放Activity自定义布局文件的ViewGroup视图,一般为FrameLayout 其id 为: android:id="@android:id/content",并将其赋给mContentParent,到这里mContentParent和mDecor均已生成,而我们xml布局文件中的布局则会被添加至mContentParent。接着对上面的过程做一个简单的总结如下图

Center

我们用张图来说明下层次结构

Center

注:此图引自http://blog.csdn.net/qinjuning/article/details/7226787这位大神的博客。

所以说实际上我们在写xml布局文件的时候我们的根布局并不是我们能在xml文件中能看到的最上面的那个,而是FrameLayout,我们再用谷歌给我提供的hierarchyviewer这个工具来看看我们最开始的那个小例子的布局情况,看完你就明白了

Center

看到了吧,在LinearLayout的上面是FrameLayout。到这可能有的人会说你这是写的啥?跟自定义控件一点关系都没有,其实只有了解了上面过程我们才能更好的去理解自定义控件
到这里我们回到最初我们提出的问题widthMeasureSpec和heightMeasureSpec是从哪来?我们在上面提到是从其父View传递过来的,那么它的父View的这两个参数又是从哪来,这样一步一步我们就需要知道View绘制的时候是从儿开始的,其实担任此重任的是ViewRootImpl,绘制开始是从ViewRootImpl中的performTraversals()这个方法开始的,我们来看看源码,可能有的人会说又看源码,只有看源码才能学的更透彻,这里我们只看主要的代码,理解其流程即可,其实performTraversals()方法的代码很多,我们省略后如下

我们清楚的看到在此调用了getRootMeasureSpec方法后会得到childWidthMeasureSpec和childHeightMeasureSpec,得到的这个数据作为参数传给host(这里的host是View)measure方法。在调用getRootMeasureSpec时需要两个参数desiredWindowWidth ,lp.width和desiredWindowHeight  , lp.height这里我们看到desiredWindowWidth 和desiredWindowHeight就是我们窗口的大小而lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。参数搞明白后我们来看看getRootMeasureSpec的源码,看看它都是干了个啥。

上面三种情况的英文注释很简单自己翻译下即可理解。总之这个方法执行后不管是哪一种情况我们的根视图都是全屏的。在上面中大家看到MeasureSpec这个类有点陌生,MeasureSpec这个类的设计很精妙,对于学习自定义View也非常重要,理解它对于学习自定义控件非常有用接下来我们就花点篇幅来详细的讲解一下这个类,measurespec封装了父类传递给子类的测量要求,每个measurespec代表宽度或者高度的要求以及大小,也就是说一个measurespec包含size和mode。它有三种mode(模式)
 ①UNSPECIFIED:父View没有对子View施加任何约束。它可以是任何它想要的大小。 
 ②EXACTLY:父View已经确定了子View的确切尺寸。子View将被限制在给定的界限内,而忽略其本身的大小。 

 ③AT_MOST:子View的大小不能超过指定的大小

它有三个主要的方法:

getMode(imeasureSpec)它的作用就是根据规格提取出mode,这里的mode是上面的三种模式之一

getSize(int measureSpec)它的作用就是根据规格提取出size,这里的size就是我们所说的大小

makeMeasureSpec(int size, int mode)根据size和mode,创建一个测量要求。

说了这些可能大家仍然是一头雾水接下来我们看看它的源码,MeasureSpec是View的内部类,它的源码如下

MeasureSpec这个类的设计是非常巧妙的,用int类型占有32位,它将其高2位作为mode,后30为作为size这样用32位就解决了size和mode的问题

看完的它的源码大家可能似懂非懂,那么我们就举个例子画个图,让你彻底理解它的设计思想。

假如现在我们的mode是EXACTLY,而size=101(5)那么size+mode的值为:

Center

这时候通过size+mode构造除了MeasureSpec对象及测量要求,当需要获得Mode的时候只需要用measureSpec与MODE_TASK相与即可如下图

Center

我们看到得到的值就是上面的mode,而如果想获得size的话只需要只需要measureSpec与~MODE_TASK相与即可如下图

Center

我们看到得到值就是上面的size。关于这个设计思想大家好好的,慢慢的体会下。

好了到这里我们应该对MeasureSpec有了一定的理解。这时返回去看看我们的getRootMeasureSpec方法,你是不是能看懂了?看懂后回到performTraversals方法,通过getRootMeasureSpec方法得到childWidthMeasureSpec和childHeightMeasureSpec后,我们看到在performTraversals方法中会调用host.measure(childWidthMeasureSpec,childHeightMeasureSpec),这样childWidthMeasureSpec和childHeightMeasureSpec这两个测量要求就一步一步的传下去并由当前View与其父容器共同决定其测量大小,在这里View与ViewGroup中的递归调用过程中有几个重要的方法,而对于View是measure方法,接着我们看看host.measure也就是View的measure方法的源码吧

看到了吧,在measure方法中调用了onMeasure方法,你是不是应该笑30分钟?终于见到我们的onMeasure方法了,这里的onMeasure就是我们重写的onMeasure,它接收两个参数widthMeasureSpec和heightMeasureSpec这两个参数由父View构建,表示父View对子View的测量要求。它有它的默认实现,即重写后我们什么都不做直接调用super.onMeasure方法它的默认实现如下

在onMeasure方法中直接调用setMeasuredDimension方法,在这里它会调用getSuggestedMinimumWidth方法得到的数据传递给getDefaultSize方法,首先来看看getSuggestedMinimunWidth,getDefaultSize以及setMeasuredDimension这三个方法的源码吧

这只是一个自定义View的默认实现,如果想按照我们的要求来进行绘制的话,重写onMeasure需要添加我们自己的逻辑去实现,最终在onMeasure方法中会调用setMeasureDimenSion决定我们的View的大小,这也是我们重写onMeasure方法的最终目的。

上面这些是对于一个View的测量,android中在进行测量时有两种情况,一种是一个View如Button,ImaeView这中,不能包含子View的对于这种测量一下就ok了,另外一种就是ViewGroup像LinearLayout,FrameLayout这种可以包含子View的,对于这种我们就需要循环遍历每一个子View并为其设置大小,在自定义的ViewGroup中重写onMeasure如下的伪代码

其实ViewGroup已经为我们提供了测量子View的方法,主要有measureChildren,measureChild和getMeasureSpec,下面我们来分别看看这三个方法都是干了个啥?

measureChildren方法的源码如下

可以看到在measureChildren方法中会遍历所有的View然后对每一个View(不包括gone的View)调用measureChild方法,顺其自然我们来看看measureChild方法的源码

在measureChild方法中通过getChildMeasureSpec得到最终的测量要求,并将这个测量要求传递给childView的measure方法,就会按照View的那一套逻辑运行。在这里看到调用了getChildMeasureSpec方法我们来看看这个方法的源码

我们经常说View的大小是由父View以及当前View共同决定的,这一点从上面这个方法也可以看出。但是这只是一个期望的大小,其大小的最终决定权由setMeasureDimenSion方法决定。

所以最终View的大小将受以下几个方面的影响(以下三点摘自:http://blog.csdn.net/qinjuning/article/details/8074262此博客,这是一个大神。。)

 1、父View的MeasureSpec属性;

 2、子View的LayoutParams属性;

 3、setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。

关于View的测量过程就介绍完了,可能你一遍没有读懂,只要你认真的去看我相信你一定会有收获,如果你一遍就读懂了,千万别告诉我,我会伤心的,哈哈,因为我花了一周的时间才对onMeasure有了点理解。

如果你觉得本篇博客对你有帮助就留言顶一个呗。

转载注明出处:http://blog.csdn.net/dmk877/article/details/49558367

如有谬误欢迎批评指正,如有疑问欢迎留言。我将在第一时间改正或回答。。。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK