自定义View基础

966 阅读6分钟

掌握一门技术当然需要知道它的作用,不然学习起来就没有了动力。今天的主题是自定义View,既然谷歌已经提供了那么多得控件,那么为什么还要去自定义View呢?我总结一下主要有以下三大点:

1.有些UI效果是官方控件没有提供的

这种情况很普遍,比如下拉刷新吧,官方只是提供了一个默认的转圈圈,奇丑无比,当然入不了设计师的眼了,设计师将设计书一改,自定义View的需求就来了。虽然有很多的第三方大牛已经做好了各种各样的效果,但是我们也要了解其中的实现方式,万一效果有差池才能修改。

2.为了提升页面性能

多层的View嵌套是很影响渲染性能的,因为会增加测量的次数

3.出于代码规范性的考虑

将一些常用可复用的效果抽离成自定义View,方便进行复用和统一维护


自定义View的核心方法

自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw();View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。其中自定义View:只需要重写onMeasure()和onDraw(),自定义ViewGroup则只需要重写onMeasure()和onLayout()

  • 测量:onMeasure()决定View的大小;

  • 布局:onLayout()决定View在ViewGroup中的位置;

  • 绘制:onDraw()决定绘制这个View。


AttributeSet与自定义属性

系统自带的View可以在xml中配置属性,对于写的好的自定义View同样可以在xml中配置属性,为了使自定义的View的属性可以在xml中配置,需要以下4个步骤:

1.通过为自定义View添加属性

2.在xml中为相应的属性声明属性值

3.在运行时(一般为构造函数)获取属性值

4.将获取到的属性值应用到View


View视图结构

PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayoutViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等


对于多View的视图,结构是树形结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下图:


注意:无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。为什么不从子View开始计算呢,因为子View个数太多,处理的顺序不好弄,而且子View若为wrap_content或者match_parent的情况下,就会出现不知道onMeasure时该View应该显示多大了。


Android坐标系

Android的坐标系定义为:

  • 屏幕的左上角为坐标原点

  • 向右为x轴增大方向

  • 向下为y轴增大方向



getY代表触摸点到控件上边缘的距离。getRawY上图有点问题,将就着看看,getRawY应该是从X轴到触摸点的距离,也就是getRawY那条白线还要再往上面延伸一些。


View树的绘制流程

View树的绘制流程是谁负责的?

view树的绘制流程是通过ViewRootImpl去负责绘制的,它的主要作用是View树的管理者,负责将DecorView和PhoneWindow“组合”起来,而View树的根节点严格意义上来说只有DecorView;每个DecorView都有一个ViewRoot与之关联,这种关联关系是由WindowManager去进行管理的;


从上图可以看到,从添加View以后就一直在ViewRootImpl中做处理,最终是执行到了performTraversals方法,这个方式非常核心,它负责调用onMeasure、onLayout、onDraw这几个核心方法,是一条主线。


那么Android系统为什么要有onMeasure呢?

因为Android支持非精确的大小,具体包括warp_content和match_parent,所以在显示的时候并不能只能确定出自己的实际大小,而是需要根据父控件给出的限制以及自身的属性设置来决定,所以我们需要复写onMeasure来设定自己想要的大小策略。

说到父控件给出的限制,这里想要重点讲一下,其实就是MeasureSpec,在复写onMeasure的时候系统给我们回调的参数。其中包括widthMeasureSpec和heightMeasureSpec,也就是父控件分别给的宽高限制,限制包括父容器给当前控件的测量模式,以及父容器具体的大小(当然只有确定了的时候才能给)


在父容器给的MeasureSpec中,总共有32位,最高的2位存储着父容器的测量模式,其余的低30位存储着父容器具体的大小,子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里


在子View复写的onMeasure中,需要根据父容器给的MeasureSpec以及该子View使用时设置的宽高属性来得出该View的最终宽高。这里最好能覆盖自定能设置的所有的情况,假设只能设置固定宽高,就不能算是一个合格的自定义View了

系统得出该View的具体策略如下图所示,横向代表父容器给予的测量模式,纵向代表字View自身设置的宽高属性,其中Unspecified代表不知道自己应该有多大,也就是说父容器也是设置的wrap_content的情况,这种情况优先子View确定宽高,如果子View是精确值,则直接确定为具体宽高,否则也不确定



上面说的可能有些抽象,接下来就以自定义ViewGroup为例,说明一下测量过程。在自定义ViewGroup时,具体流程如下:

1.测量ViewGroup自身的尺寸

需要调用super.onMeasure来测量自身的尺寸,如果不调用就需要调用setMeasuredDimension。这里其实不调用也没关系,因为一般都会自定义宽度,后面会被自己定义的给覆盖

2.为每个子View计算测量的限制信息

获取到第一步测量好的信息,包括测量模式、测量出来的数据

3.将上一步获得到的限制信息传递给该ViewGroup的每一个子View,好让子View能做自己的onMeasure测量


4.获取子View测量完成后的尺寸


5.计算ViewGroup自身的尺寸

由于当前ViewGroup的尺寸和子View有关,所以必须等子View的尺寸获取到以后再决定自身的尺寸,这里使用switch是考虑到当前ViewGroup的xml中定义的widh和height有三种情况

6.保存ViewGroup自身的尺寸作为本次onMeasure结果,本次测量结束

最后的画龙点睛之笔,将计算好的当前ViewGroup宽高告知系统




onLayout方法

onMeasure方法确定了当前ViewGroup的大小后,onlayout方法就简单了,按照自己的想法调用每个子View的layout方法决定好子View的位置即可,注意位置是相对当前控件的相对位置