Android自定义View全解

目录

image-20190212134418595

1. 自定义View基础

1.1 分类

自定义View的实现方式有以下几种

类型定义
自定义组合控件多个控件组合成为一个新的控件,方便多处复用
继承系统View控件继承自TextView等系统控件,在系统控件的基础功能上进行扩展
继承View不复用系统控件逻辑,继承View进行功能定义
继承系统ViewGroup继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展
继承ViewViewGroup不复用系统控件逻辑,继承ViewGroup进行功能定义

1.2 View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

1.3 坐标系

在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:

image-20190212134501403

Android坐标系.png

除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。

image-20190212134531559

视图坐标系.png

View获取自身高度

由上图可算出View的高度:

View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度和高度,其内部方法和上文所示是相同的,我们可以直接调用来获取View得宽高。

View自身的坐标

通过如下方法可以获取View到其父控件的距离。

1.4 构造函数

无论是我们继承系统View还是直接继承View,都需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。如我们新建TestView

1.5 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。 Android自定义属性可分为以下几步:

  1. 自定义一个View
  2. 编写values/attrs.xml,在其中编写styleable和item等标签元素
  3. 在布局文件中View使用自定义的属性(注意namespace)
  4. 在View的构造方法中通过TypedArray获取

实例说明

属性值的类型format

(1). reference:参考某一资源ID

(2). color:颜色值

(3). boolean:布尔值

(4). dimension:尺寸值

(5). float:浮点值

(6). integer:整型值

(7). string:字符串

(8). fraction:百分数

(9). enum:枚举值

注意:枚举类型的属性在使用的过程中只能同时使用其中一个,不能 android:orientation = “horizontal|vertical"

(10). flag:位或运算

注意:位运算类型的属性在使用的过程中可以使用多个值

(11). 混合类型:属性定义时可以指定多种类型值

2. View绘制流程

这一章节偏向于解释View绘制的源码实现,可以更好地帮助我们掌握整个绘制过程。

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

2.1 Measure()

MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec = mode + size

MeasureSpec当中一共存在三种modeUNSPECIFIEDEXACTLYAT_MOST

对于View来说,MeasureSpec的mode和Size有如下意义

模式意义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大一般系统内部使用

使用方式

在View当中,MeasureSpace的测量代码如下:

这里需要注意,这段代码只是在为子View设置MeasureSpec参数而不是实际的设置子View的大小。子View的最终大小需要在View中具体设置。

从源码可以看出来,子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的。

父View mode子View
UNSPECIFIED父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0
EXACTLY父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围
AT_MOST父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围

在测量子View大小时:

父View mode子View
UNSPECIFIED父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0
EXACTLY父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围
AT_MOST父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围

onMeasure()

整个测量过程的入口位于Viewmeasure方法当中,该方法做了一些参数的初始化之后调用了onMeasure方法,这里我们主要分析onMeasure

onMeasure方法的源码如下:

很简单这里只有一行代码,涉及到了三个方法我们挨个分析。


ViewGroup的测量过程与View有一点点区别,其本身是继承自View,它没有对Viewmeasure方法以及onMeasure方法进行重写。

为什么没有重写onMeasure呢?ViewGroup除了要测量自身宽高外还需要测量各个子View的大小,而不同的布局测量方式也都不同(可参考LinearLayout以及FrameLayout),所以没有办法统一设置。因此它提供了测量子View的方法measureChildren()以及measureChild()帮助我们对子View进行测量。

measureChildren()以及measureChild()的源码这里不再分析,大致流程就是遍历所有的子View,然后调用Viewmeasure()方法,让子View测量自身大小。具体测量流程上面也以及介绍过了


measure过程会因为布局的不同或者需求的不同而呈现不同的形式,使用时还是要根据业务场景来具体分析,如果想再深入研究可以看一下LinearLayoutonMeasure方法。

2.2 Layout()

要计算位置首先要对Android坐标系有所了解,前面的内容我们也有介绍过。

layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。

layout()方法是整个Layout()流程的入口,看一下这部分源码

从源码我们知道,在layout()方法中已经通过setOpticalFrame(l, t, r, b)setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。

有兴趣的可以看一下LinearLayoutonLayout源码,可以帮助加深理解。

2.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在Viewdraw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

  1. 如果需要,绘制背景。
  2. 有过有必要,保存当前canvas。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,如滚动条等等。

通过各个步骤的源码再做分析:

3. 自定义组合控件

自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的HeaderView以及dailog等,我们都可以把他们组合成一个新的控件。

我们通过一个自定义HeaderView实例来了解自定义组合控件的用法。

1. 编写布局文件

布局很简单,中间是title的文字,左边是返回按钮,右边是一个添加按钮。

2. 实现构造方法

3. 初始化UI

4. 提供对外的方法

可以根据业务需求对外暴露一些方法。

5. 在布局当中引用该控件

到这里基本的功能已经有了。除了这些基础功能外,我们还可以做一些功能扩展,比如可以在布局时设置我的View显示的元素,因为可能有些需求并不需要右边的按钮。这时候就需要用到自定义属性来解决了。

前面已经简单介绍过自定义属性的相关知识,我们之间看代码

1.首先在values目录下创建attrs.xml

内容如下:

这里我们定义了三个属性,文字内容、颜色以及要显示的元素。

2.在java代码中进行设置

3.在布局文件中进行设置

OK,到这里整个View基本定义完成。整个YFHeaderView的代码如下

4. 继承系统控件

继承系统的控件可以分为继承View子类(如TextVIew等)和继承ViewGroup子类(如LinearLayout等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承自View的实现方式。

业务需求:为文字设置背景,并在布局中间添加一条横线。

因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的onMeaseuronLayout流程,所以我们只需要重写onDraw方法 。实现非常简单,话不多说,直接上代码。

对于View的绘制还需要对Paint()canvas以及Path的使用有所了解,不清楚的可以稍微了解一下。

这里的实现比较简单,因为具体实现会与业务环境密切相关,这里只是做一个参考。

5. 直接继承View

直接继承View会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写onDraw外还需要对onMeasure方法进行重写。

我们用自定义View来绘制一个正方形。

之前我们讲到过View的measure过程,再看一下源码对这一步的处理

在View的源码当中并没有对AT_MOSTEXACTLY两个模式做出区分,也就是说View在wrap_contentmatch_parent两个模式下是完全相同的,都会是match_parent,显然这与我们平时用的View不同,所以我们要重写onMeasure方法。

整个自定义View的代码如下:

整个过程大致如下,直接继承View时需要有几点注意:

1、在onDraw当中对padding属性进行处理。 2、在onMeasure过程中对wrap_content属性进行处理。 3、至少要有一个构造方法。

6. 继承ViewGroup

自定义ViewGroup的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子View的测量参数负责。

需求实例

实现一个类似于Viewpager的可左右滑动的布局。

代码比较多,我们结合注释分析。

到这里我们的View布局就已经基本结束了。但是要实现Viewpager的效果,还需要添加对事件的处理。事件的处理流程之前我们有分析过,在制作自定义View的时候也是会经常用到的,不了解的可以参考之前的文章Android Touch事件分发超详细解析

这部分代码比较多,为了方便阅读,在代码当中进行了注释。 之后就是在XML代码当中引入自定义View

好了,可以运行看一下效果了。

总结

本篇文章对常用的自定义View的方式进行了总结,并简单分析了View的绘制流程。对各种实现方式写了简单的实现。

原文