实际开发过程中会遇到很多自定义的view 或者viewgroup ,要怎么去实现view的自定义,我们需要处理哪些事情,要了解这些 我必须知道 view的工作原理,view是怎么绘制的
首先onMeasure
1.现看下这个方法onMeasure(int widthMeasureSpec, int heightMeasureSpec) ,这个方法是用来测量控件大小的,也是可以用来控制控件大小 。有两个参数widthMeasureSpec,heightMeasureSpec。这两个参数表示宽度和高度的测量规格,在源码里面可以看到在view实现的是
接着看这个方法 getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
可以很清楚的看到 通过一个widthMeasureSpec 参数可以获取两个值 一个是specSize 一个是specMode
从这里可以看出来widthMeasureSpec是一个组合参数, MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,是为了减少对象的分配开支。MeasureSpec 类似于下图,只不过这边用的是十进制的数,而MeasureSpec 是二进制存储的。
现在我们来看下MeasureSpec的模式 ,一共有三种模式
UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大
EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
AT_MOST:子容器可以是声明大小内的任意大小
这个是官方的文档说明,看完也不是很清楚 ,先看看这两个参数是从哪里来的,我们都知道view是不会独立存在的 ,先简单了解下android 视图架构
所以onMeasure(int widthMeasureSpec, int heightMeasureSpec) 这个方法的参数肯定是父类里传过来的 ,那么这个参数的值是有什么决定的呢,看上图最终肯定是DecorView 处理的 ,涉及DecorView 就要看下ViewRoot的源码了(这个不做详细解释 ,DecorView 和 ViewRoot关系不一般),我截了别人帖子里的图大概说明下
链接: 传送门
现在我们来看下widthMeasureSpec 是怎么赋值的 看图
这里是初始化MeasureSpec的地方 在看怎么赋值的
看这这个方法 就知道了 我们在写布局文件的是 view的 layout_width ,layout_height 的属性对应的MeasureSpec的模式了 ,系统就给了两种 ,还有一个是给开发者自己用的
现在总结下:
1、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是确切,子View的大小又MATCH_PARENT(充满整个父View),那么子View的大小肯定是确切的,而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY
2、如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根据自己的content 来决定的,但是子View的毕竟是子View,大小不能超过父View的大小,但是子View的是WRAP_CONTENT,我们还不知道具体子View的大小是多少,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 调用的时候才去真正测量子View 自己content的大小(比如TextView wrap_content 的时候你要测量TextView content 的大小,也就是字符占用的大小,这个测量就是在child.measure(childWidthMeasureSpec, childHeightMeasureSpec)的时候,才能测出字符的大小,MeasureSpec 的意思就是假设你字符100px,但是MeasureSpec 要求最大的只能50px,这时候就要截掉了)。通过上述描述,子View MeasureSpec mode的应该是AT_MOST,而size 暂定父View的 size,表示的意思就是子View的大小没有不确切的值,子View的大小最大为父View的大小,不能超过父View的大小(这就是AT_MOST 的意思),然后这个MeasureSpec 做为子View measure方法 的参数,做为子View的大小的约束或者说是要求,有了这个MeasureSpec子View再实现自己的测量。
3、如果如果子View 的layout_xxxx是确定的值(200dp),那么就更简单了,不管你父View的mode和size是什么,我都写死了就是200dp,那么控件最后展示就是就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是这么大,所以这种情况MeasureSpec 的mode = EXACTLY 大小size=你在layout_xxxx 填的那个值。
4、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不确定(只知道最大只能多大),子View的大小MATCH_PARENT(充满整个父View),那么子View你即使充满父容器,你的大小也是不确定的,父View自己都确定不了自己的大小,你MATCH_PARENT你的大小肯定也不能确定的,所以子View的mode=AT_MOST,size=父View的size,也就是你在布局虽然写的是MATCH_PARENT,但是由于你的父容器自己的大小不确定,导致子View的大小也不确定,只知道最大就是父View的大小。
5、如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不确定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content没算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暂定父View的 size。
(以上总结,不是我总结的,传送门)
layout方法
performTraversals 方法执行完mView.measure 计算出mMeasuredXXX后就开始执行layout 函数来确定View具体放在哪个位置,我们计算出来的View目前只知道view矩阵的大小,具体这个矩阵放在哪里,这就是layout 的工作了。layout的主要作用 :根据子视图的大小以及布局参数将View树放到合适的位置上。
既然是通过mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 那我们来看下layout 函数做了什么,mView肯定是个ViewGroup,不会是View,我们直接看下ViewGroup 的layout函数
1、setFrame(l, t, r, b) 可以理解为给mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的矩形区域就是该View显示的位置,这里的具体位置都是相对与父视图的位置。
2、回调onLayout,对于View来说,onLayout只是一个空实现,一般情况下我们也不需要重载该函数,:
对于ViewGroup 来说,唯一的差别就是ViewGroup中多了关键字abstract的修饰,要求其子类必须重载onLayout函数。
我们在重载onLayout函数的时候要求对ViewGroup 里面的子view 进行布局代码随意
代码很简单 就是根据自己的需求进行布局就好了
MeasuredWidth和MeasuredHeight这两个参数为layout过程提供了一个很重要的依据(如果不知道View的大小,你怎么固定四个点的位置呢),但是这两个参数也不是必须的,layout过程中的4个参数l, t, r, b完全可以由我们任意指定,而View的最终的布局位置和大小(mRight - mLeft=实际宽或者mBottom-mTop=实际高)完全由这4个参数决定,measure过程得到的mMeasuredWidth和mMeasuredHeight提供了视图大小测量的值,但我们完全可以不使用这两个值,所以measure过程并不是必须的。如果我们不使用这两个值,那么getMeasuredWidth() 和getWidth() 就很有可能不是同一个值,它们的计算是不一样的
onDraw()
performTraversals 方法的下一步就是mView.draw(canvas); 因为View的draw 方法一般不去重写,官网文档也建议不要去重写draw 方法,所以下一步执行就是View.java的draw 方法,这里不在贴源码了 说下大概的过程:
1.背景绘制
2.skip
3.对View的内容进行绘制
onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。
4 .对当前View的所有子View进行绘制
就是遍历子View然后drawChild(),drawChild()方法实际调用的是子View.draw()方法,ViewGroup类已经为我们实现绘制子View的默认过程,这个实现基本能满足大部分需求,所以ViewGroup类的子类(LinearLayout,FrameLayout)也基本没有去重写dispatchDraw方法,我们在实现自定义控件,除非比较特别,不然一般也不需要去重写它, drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制了
5.skip
6. 对View的滚动条进行绘制
字面意思
这里面少一个ViewGroup onMeasure 的分析
简单说下,ViewGroup遍历自己的子View,只要不是GONE的都会参与测量,基本思想就是父View把自己的MeasureSpec 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。