Android自定义View学习记录

前言:这是一个关于Android自定义View的学习记录系列,系列中出现的知识点均出自《Android艺术开发探索》,部分摘录自热心网友文章

本文结构:

Android自定义View

1. View的基础知识

1.1 View的概念

什么是View,在Android中不管是Button还是TextView,又或者是LinearLayout,它们的共同基类都是View,所以View是界面层的控件的一种抽象,它代表了一种控件。除了View,还有ViewGroup,ViewGroup是控件组,即一组控件(或者说是一组View),它的父类是View,综上,View可以解释为单个控件,也可以解释为由多个控件组件的一组控件。

View示例

1.2 View的位置参数

View的位置主要由它的四个属性决定:top、left、right、bottom。如下图坐标系所示:

View的坐标轴

View的坐标值都是相对于父容器而言的。

这四个属性值的获取方式:

1
2
3
4
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();

而从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,默认值为0。

2.自定义View的原因和方式

2.1 为什么要自定义View

通过自定义View,我们可以实现各种五花八门的效果

2.2 自定义View的方式

  1. 把系统内置的控件组合起来成一个新的控件
  2. 继承系统现有的控件,加入新的功能
  3. 自己绘制控件,继承系统View

3. View的工作流程

View的工作流程主要是指measure、layout和draw这三个流程,即测量、布局和绘制。其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。

View的绘制流程是从ViewRoot的performTraversals,它经过measure、layout和draw三个过程才能最终将一个view绘制出来,针对performTraversals的大致流程,如下图所示:

performTraversals的工作流程

performTraversals会依次调用performMeasure、performLayout和performDraw这三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中performMeasure会调用measure方法(measure方法是一个final类型的方法),在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个view树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的。

3.1 measure过程

在上面的分析中我们知道在View的measure方法中会去调用View的onMeasure,因此只需要看onMeasure的实现即可,View的onMeasure方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置控件的宽高,记住这里默认是px,记得要分辨率转换实现适配,这里不做说明
setMeasuredDimension(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
}

private int getSize(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode){
case MeasureSpec.EXACTLY:
//父控件告诉我们子控件了一个确定的大小,你就按这个大小来布局。
// 比如我们指定了确定的dp值和macth_parent的情况。
result = 200;
break;
case MeasureSpec.AT_MOST:
//当前控件不能超过一个固定的最大值,一般是wrap_content的情况。
result = Math.min(100, specSize);
break;
case MeasureSpec.UNSPECIFIED:
//当前控件没有限制,要多大就有多大,这种情况很少出现
result = 400;
break;
}
return result;
}

onMeasure方法里有两个重要的参数, widthMeasureSpec和heightMeasureSpec,它们包含了两个信息:mode和size;

mode(测量模式)代表了我们当前控件的父控件告诉我们控件,你应该按怎样的方式来布局。mode的分类如下表格所示

模式 二进制数值 描述
UNSPECIFIED 00 父容器不对View有任何限制,要多大给多大
EXACTLY 01 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应LayoutParams的match_parent和具体的数值这两种模式
AT_MOST 10 父容器指定了一个可用大小SpecSize,View的大小不能大于这个值,它对应LayoutParams的wrap_content

注意:
如果对View的宽高进行修改了,不要调用 super.onMeasure( widthMeasureSpec, heightMeasureSpec); 要调用 setMeasuredDimension( widthsize, heightsize); 这个函数,否则会报异常。

onSizeChanged(确定View的大小):这个函数在view第一次被指定了大小值或者view的大小发生改变时会被调用。所以一般用来计算一些位置和与view的size有关的值。

3.2 layout过程

layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦被确定,那么View在父容器中的位置也就被确定了;接着会调用onLayout方法,onLayout的具体实现同样和具体的布局有关。

3.3 draw过程

draw过程比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循:

  1. 绘制背景background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

4. 自定义View的官方套路

步骤 关键字 作用
1 构造函数 初始化和定义自定义属性
2 onMeasure 确定view的测量宽/高
3 onSizeChanged 确定view的大小
4 onLayout 确定view在父容器中的布局
5 onDraw 实际绘制内容
6 提供接口 控制View或监听View某些状态

参考资料:

《Android艺术开发探索》

http://www.gcssloop.com/customview/CustomViewProcess

https://juejin.im/entry/579865765bbb500063fd66f4