阅读 4841

探索 Android 自定义控件:基础图形

首图.png

前言

1. 文章内容

这篇文章分为下面 5 个部分。

  1. 绘图基础

    这一节会介绍 Android 中的画笔 Paint 和画布 Canvas 的用法。

  2. 路径绘制

    这一节会介绍 Android 中路径 Path 的用法,包括直线、弧线和雷达图等图形的绘制方法。

  3. 文字绘制

    这一节会介绍 Android 中绘制文字的方法,包括粗体、斜体、加载字体等。

  4. 区域操作

    这一节会介绍 Android 中区域 Region 的用法,包括区域裁剪、区域合并等操作。

  5. 画布进阶

    这一节会对画布的用法进行更详细的介绍,包括保存、指向恢复和圆形头像。

2. 注意事项

  • 对象创建

    下面的示例代码会在 onDraw() 方法中创建 Paint、Path、Rect 和 Region等对象,这在实际开发中是禁止的。

    因为当 View 需要重绘时会调用 onDraw() 函数,每一次调用 onDraw() 函数都会重新创建这些对象。

    这样会引起频繁的 GC,严重时会导致 App 卡顿。

  • 图片清晰度

    点击文章中的图片可以查看原图。

  • 示例代码

    文中的示例代码源码在文章最下方可以找到。

1. 绘图基础

本节包含内容如下。

  • 画笔用法
  • 画布用法
  • 颜色构造

1.1 画笔用法

我们这一节来看一下画笔的用法。

本节包含内容如下。

  • 设置画笔颜色
  • 设置画笔样式
  • 设置画笔宽度
  • 设置画笔锯齿

1.1.1 设置画笔颜色

画笔颜色.png

Paint 的 setColor(int color) 方法可用于设置画笔颜色,下面是 color 参数的取值。

一种颜色是由红绿蓝三色合成的,所以 color 只能取 8 位 0xAARRGGBB 样式颜色值。

  • 透明度

    A 表示透明度 Alpha,取值范围是 0~255,值越小,图像越透明

  • 红色

    R 表示红色 Red,取值范围是 0~255,取值越小红色越少

  • 绿色

    G 表示绿色 Green,取值范围是 0~255,取值越小绿色越少

  • 蓝色

    B 表示蓝色 Blue,取值范围是 0~255,取值越小蓝色越少

除了手动组合颜色,系统还提供了一个用于解析颜色的类 Color,关于 Color 在后面会有更详细的介绍。

1.1.2 设置画笔样式

画笔样式.png

Paint 的 setStyle(Style style) 方法可用于设置画笔样式,下面是 style 参数的取值。

  • 描边

    • STROKE
  • 填充

    • FILL

    默认样式。

  • 描边且填充

    • FILL_AND_STROKE

    描边与填充叠加在一起显示的效果,也就是这个值比填充多了一个描边的宽度。

1.1.3 设置画笔宽度

Paint 的 setStrokeWidth(width) 方法用于设置描边宽度,单位是 px。

  • 注意事项

    当画笔的 Style 是 STROKE 或 FILL_AND_STROKE 时画笔宽度才有意义。

1.1.4 设置画笔锯齿

画笔 Paint 绘制图形时默认不是抗锯齿的,也就是边边会有锯齿。

Paint 提供了 setAntiAlias() 方法,这个方法可以开启画笔的抗锯齿功能。

下面是两个放大后的圆,右边的圆用的是抗锯齿的画笔绘制的。

抗锯齿.png

1.2 画布用法

上一节演示画笔的同时也演示了画布绘制圆的方法,这一节我们来看一下画布的其他方法。

本节内容如下。

  • 设置背景
  • 绘制直线
  • 绘制点
  • 矩形结构
  • 绘制矩形
  • 圆角矩形

1.2.1 设置背景

Canvas 提供了三个设置背景的方法。

  • drawColor(int color)
  • drawARGB(int a, int r, int g, int b)
  • drawRGB(int r, int g, int b)

需要注意的是,设置画布背景要在其他图形绘制前设置,否则设置好的背景色会覆盖原有的图形。

画布背景.png

1.2.2 绘制直线

绘制直线.png

Canvas 的 drawLine() 方法可以绘制直线,直线的粗细取决于 Paint 的 setStrokeWith(width) 中传入的宽度。

绘制直线需要注意的是,只有当 Style 是 STROKE、FILL_AND_STROKE 时绘制才有效。

  • drawLine(float startX, float starY, float stopX, float stopY, Paint paint)

1.2.3 绘制点

绘制点.png

Canvas 的 drawPoint() 方法可以用于绘制点,点的大小取决于 setStrokeWith(width) 中传入的宽度。

  • drawPoint(float x, float y, Paint paint)

1.2.4 矩形结构

矩形结构在绘制矩形区域的时候需要用到。

Android 提供了 Rect 和 RectF 类用于存储矩形数据结构,下面是 Rect 和 RectF 的构造函数。

Rect 构造函数.png

Rect 和 RectF 在于存储的数据类型不同。

  • Rect

    用于保存 int 类型数值的矩形结构

  • RectF

    用于保存 float 类型数值的矩形结构

1.2.5 绘制矩形

绘制矩形.png

Canvas 的 drawPoint() 和 drawRect() 方法都可用于绘制矩形。

下面是这两个方法的区别。

  • 形状

    • drawPoint()

      只能指定矩形中心的坐标,只能画出正方形。

    • drawRect()

      需要指定矩形左上和右下两个点的位置,可以是长方形。

  • 样式

    • drawPoint()

      只能是填充样式。

    • drawRect()

      可以自己选择样式,可以是描边也可以是填充。

下面是 Canvas 中提供用于绘制矩形的三个方法。

  • drawRect(float left, float top, float right, float bottom, Paint paint)
  • drawRect(RectF rect, Paint paint)
  • drawRect(Rect r, Paint paint)

1.2.6 圆角矩形

Canvas 中提供了一个 drawRoundRect 方法用于绘制圆角矩形,圆角矩形的四个角是椭圆的一角,下面是它的基本用法。

圆角矩形.png

1.3 颜色构造

Color 类是 Android 中与颜色有关的类。

本节内容如下。

  • 颜色常量
  • 构造颜色

1.3.1 颜色常量

Color 中定义了下面的颜色常量值。

Color 常量.png

1.3.2 构造颜色

除了颜色常量外,Color 还提供了一些构造颜色的方法。

构造颜色

alpha << 就是进 24 位,当我们调用 Color.argb(255, 0, 0,0 0) 时,转换后的 16 进制颜色值就是 0xFF000000。

进位.png

2. 路径绘制

路径 Path 指的是画笔画出来的一条不间断的曲线,本节会讲解直线和弧线路径,除了这两种方式外还有很多方法可以实现很多效果。

drawPath 是 Canvas 绘制路径的方法。

  • drawPath(Path path, Paint paint)

本节内容如下。

  1. 直线路径
  2. 弧线路径
  3. 添加路径
  4. 填充路径
  5. 重置路径
  6. 雷达路径

2.1 直线路径

绘制直线路径.png

上面是一段绘制直线路径的代码,这里需要注意的是,上面代码中的画笔样式设为描边,默认是填充的。

也就是在默认情况下,多条线相连形成闭环后,中间的区域会被填充。

绘制直线路径涉及:起点、终点和闭环。

  • 起点

    Path 的 moveTo() 用于指定直线路径的起点,参数 x1 和 y1 是起点坐标值。

    • moveTo(float x1, float y1)
  • 终点

    Path 的 lineTo() 用于指定直线路径的起点,参数 x2 和 y2 是终点坐标值,也是下一次绘制直线的起点。

    • lineTo(float x2, float y2)
  • 闭环

    Path 的 close() 方法用于形成闭环。

    • close()

    如果连续画了几条直线,但没有形成闭环,调用 close() 函数会把路径首尾连接起来形成闭环,相当于是帮我们画多一条直线。

    如果只画了一条直线,那 close() 方法是不会起作用的。

2.2 弧线路径

Path 的 arcTo() 方法可用于绘制弧线,弧线在这里指的是椭圆上截取的一部分。

  • arcTo(RectF oval, float startAngle, sweepAngle)

本节内容如下。

  • 弧线样式
  • 弧线起点
  • 弧线角度
  • 弧线函数

2.2.1 弧线样式

这里的椭圆底色默认是不存在的,在这里画出来主要是为了突出弧线。

在这里我们要注意的是,弧线默认是填充的,更准确的来说 drawArc() 方法是切出椭圆中的一块。

如果我们只想要一条线的话,就要自己设置描边样式和描边宽度。

弧线样式.png

2.2.2 弧线起点

除了样式以外,绘制弧线要注意的另外一点就是起点。

  • 改变起点

    如果我们调用了 moveTo() 改变了路径的起点,那弧线就会从 moveTo() 接收到的坐标开始绘制。

  • 重置起点

    如果我们想重置起点到弧线正常该开始的位置,我们可以把 forceMoveTo 设为 true。

弧线起点.png

2.2.3 弧线角度

arcTo 中有两个跟角度有关的参数 startAngle 和 sweepAngle,这两个参数分别代表起始角和扫描角。

  • 起始角

    起始角指定弧线从哪里开始画起。

  • 扫描角

    扫描角可以看成是弧线的长度。

弧线角度.png

2.2.4 弧线函数

arcTo.png

Path 中定义了的三个 arcTo() 函数,下面是 arcTo() 函数中主要四个参数的含义。

  • oval

    生成椭圆的矩形

  • startAngle

    弧开始的角度,以 X 轴正方向为 0°

  • sweepAngle

    弧持续的角度

  • forceMoveTo

    重置起点,把绘制弧线的起点从 moveTo 的坐标重置到 startAngle 的位置。

2.3 添加路径

本节内容如下。

  • 路径添加方法
  • 路径绘制方向

2.3.1 路径添加方法

Path 提供了 addXXX 函数用于添加路径,添加的路径可以是不连续的,还可以是曲线。

下面是 Path 中提供的一些添加路径的方法。

添加路径函数.png

下面是添加路径的示例。

添加路径.png

2.3.2 路径绘制方向

在添加路径的函数中有一些函数有 Direction 参数,这个 Direction 就是绘制的方向。

方向分为顺时针逆时针 CCW(Counter-Clockwise)。

Direction.png

下面是不同方向的绘制示例,这个示例中用到了 drawTextOnPath 方法,这个方法在讲绘制文字的时候会有更详细的介绍,现在我们先看绘制方向对绘制效果的影响。

绘制方向.png

2.4 填充路径

路径 Path 的填充模式与画笔 Paint 的填充模式不同,Path 的填充模式是指填充 Path 的哪部分。

在 Path 中有一个 FillType 枚举类,其中定义了 4 种填充类型。

FillType.png

下面是这四种填充类型的示例。

路径填充.png

2.5 重置路径

Android 提供了两个重置路径的方法,让我们可以重复使用 Path 对象。

路径 Path 一旦被重置,其中保存的所有路径都会被清空,这样就不需要重新创建一个 Path 对象了。

  • reset()

    reset() 函数类似于新建一个路径对象,除了填充类型 FillType 以外,Path 的所有数据都会被回收并重新分配。

  • rewind()

    rewind() 函数会清除 FillType 和所有的路径,保留内部数据结构,以便更快地重用。

    比如我们需要重复绘制一类线段,它们的点和数量相等,使用 rewind() 函数可以保留装载点数据的数据结构,效率更高。

    要注意的是,只有重复绘制相同路径时,这些数据结构才是可复用的。

2.6 雷达路径

本节包含如下内容。

  1. 创建画笔
  2. 计算圆心
  3. 画多边形
  4. 画网格线
  5. 画数据图

2.6.1 创建画笔

雷达初始化.png

2.6.2 计算圆心

在控件大小发生变化时,onSizeChanged() 会被回调并得到最新的控件大小,所以我们需要重写这个方法。

网状路径的大小占当前控件的 90%,所以半径为:宽高最小值 ÷ 2 × 0.9。

然后根据中心点,分别绘制雷达网格、网格中线、数据图。

雷达onDraw.png

2.6.3 画多边形

drawPolygon 涉及到三角函数,下面是公式计算示意图。

公式示意图.png

下面是公式的具体实现。

画多边形.png

2.6.4 画网格线

画网格线.png

2.6.5 画数据图

画数据.png

3. 文字绘制

本文包含如下内容。

  • 画笔文字参数
  • 画布绘制文字
  • 设置字体样式
  • 获取文字宽高

3.1 画笔文字参数

本节包含内容如下。

  • 文字参数
  • 文字样式
  • 文字对齐
  • 文字变形
  • 其他设置

3.1.1 文字参数

下面是几个 Paint 中设置文字绘制效果的方法。

文字画笔函数.png

3.1.2 文字样式

文字样式.png

3.1.2 文字对齐

我们可以通过 Paint 的 setTextAlign(align) 方法来设置绘制文字时的对齐方向。

Paint 中有一个枚举类 Align,在 Align 中定义了三种对齐模式:LEFT、CENTER、RIGHT,这三种模式分别代表左对齐,居中对齐和右对齐。

文字对齐.png

3.1.4 文字变形

文本变形.png

3.1.6 其他设置

其他效果.png

3.2 画布绘制文字

上一节我们看到了Canvas 的 drawText() 方法,下面我们来看一下 Canvas 提供的其他绘制文本的方法。

本节包含内容如下。

  • 逐字绘制文字
  • 路径绘制文字

3.2.1 逐字绘制文字

逐字绘制.png

3.2.2 路径绘制文字

在讲解路径绘制方向时我们就已经用到过 Canvas 提供了 drawTextOnPath() 方法,在这里我们看一下这个方法中的偏移参数。

路劲文字.png

3.3 设置字体样式

Paint 中提供了 setTypeFace(typeFace) 方法同于设置字体样式。

TypeFace 是专门用于设置字体样式的,我们可以指定系统提供的字体也可以指定自定义字体。

当创建 TypeFace 类时,可以指定是正常字体、斜体或者粗体,当指定样式中没有相关文字的样式时,就会用系统默认的样式显示,一般默认是宋体。

本节包含如下内容。

  • 系统字体
  • 字体样式
  • 加载字体

3.3.1 系统字体

TypeFace 中提供了下面三种字体,这三种字体对中文的支持不是很好,当遇到不支持的文字时,会用系统默认字体来写。

TypeFace.png

下面是这三种字体的显示效果。

系统字体.png

3.3.2 字体样式

除了可以选择特定字体以外,我们还可以通过 defaultFromStyle() 方法选择特定具体的某种样式。

TypeFace 中还定义了下面四种字体样式。

TypeFaceStyle.png

下面是这四种样式的显示效果。

字体样式.png

3.3.3 加载字体

Android 提供了下面三种加载自定义字体的方式。

  • 根据字体名加载
  • 根据资源名加载
  • 根据文件名加载

1. 根据字体名加载

TypeFace 中提供了一个用于根据字体名加载字体的 create() 方法,比如下面这行代码这样。

由于模拟器上没有别的字体,在这里就不演示了,大家有兴趣的可以自己尝试下。

createFace.png

2. 根据资源名加载

假如我们从网上下了个 ttf 字体文件,想放在包中直接使用,我们可以在 app/src/main 目录下建一个 assets 目录,再建一个 fonts 目录,然后把 ttf 文件放到里面。

而根据资源名加载字体的方式就是 TypeFace 的 createFromAsset() 方法。

加载字体资源.png

3. 根据文件名加载字体

从资源中加载字体的弊端就是会让 APK 包变大,如果我们把字体下载到本地的话就可以避免这个问题。

加载字体文件.png

3.4 获取文字宽高

Paint 提供了三种获取文字宽高的方法,下面我们来看一下这几个方法的用法。

本节内容如下。

  • 获取字体宽度
  • 获取字体宽高

3.4.1 获取字体宽度

文字宽度.png

3.4.2 获取字体宽高

文字宽高.png

4. 区域操作

区域指的是 Region 类,Region 是一块任意形状的封闭图形,这一节我们来看一下 Region 提供的一些操作区域的方法。

本节包含如下内容。

  1. 创建区域
  2. 合并区域
  3. 裁剪区域
  4. 其他操作

4.1 创建区域

直接构造区域指的是用 Region 的构造函数创建区域,下面是 Region 的四个构造函数。

Region 构造函数.png

下面是一个绘制 Region 的简单示例,由于 Canvas 中没有绘制 Region 的方法,所以我们在这里自己定义一个 drawRegion() 方法来绘制 Region。

Region 可用于绘制各种各样的形状,我们先看一下怎么绘制一个简单的正方形 Region。

直接构造区域.png

4.2 合并区域

Region 提供了一个 union(rect) 函数,传入目标矩阵到 union 函数中就能实现合并区域。

合并区域.png

4.3 裁剪区域

Region 中声明了下面的 5 个设置区域的方法。

设置区域方法.png

裁剪区域需要先创建一个空 Region,然后调用 setPath 方法,下面是裁剪区域的操作,椭圆路径和裁剪区域相交的区域就是裁剪结果。

裁剪区域.png

4.4 其他操作

本节如下内容。

  • 集合运算方法
  • 集合运算类型
  • 集合运算示例

1. 集合运算方法

除了 union 以外,Region 还提供了下面几个区域操作方法,而且 union 本身也是调用了 op 方法。

op.png

2. 集合运算类型

在上面这些方法中比较重要的是 Op 参数,Op 是 Region 中定义的枚举类,是集合运算操作。

在这里,集合中的元素就是 Region 矩阵范围中的子矩阵。

opEnum.png

**3. 集合运算示例 **

下面是这些集合运算的示例。

集合运算.png

5. 画布

上一节我们讲了怎么用画布 Canvas 绘制各种图形,画布除了能用来绘制各种图形以外,我们还能对画笔进行变换和裁剪。

本节内容如下。

  1. 画布平移
  2. 画布合成
  3. 画布裁剪
  4. 画布保存

4.1 画布平移

画布 Canvas 提供了一个可用于平移的方法 translate(),画布的原始状态是以左上角为圆点,向右是 X 轴正方向,向下是 Y 轴正方向。

由于画布的左上角是坐标轴的原点(0, 0),所以平移画布后,坐标系也会被平移。

平移后的画笔的左上角是新的坐标原点。

画布平移.png

4.2 画布合成

合成画布与屏幕的操作是由系统进行的,这一节我们来看一下这个操作的简单介绍。

本节内容如下。

  • 合成
  • 平移
  • 小结

4.2.1 合成

我们每次在画布上画图时,系统会先产生一个透明图层,在这个图层上画图,花完后再覆盖在屏幕上。

合成

4.2.2 平移

当我们绘制红色矩形时,会产生另一个新的 Canvas 透明图层,此时画布坐标改变了,所以绘图方式如下图所示。

由于 Canvas 已经平移了 350 像素,所以画图时是以新原点来产生视图的,然后再合成到屏幕上。

当屏幕移动后,有一部分超出屏幕的范围,超出范围的图像是不显示的。

再绘制.png

4.2.3 小结

使用 Canvas 的绘制方法有下面三个需要注意的点。

  1. 生成新图层

    每次调用绘制方法 drawXXX 时,都会产生一个新的 Canvas 透明图层。

  2. 操作不可逆

    调用了绘制方法前,平移和旋转等函数对 Canvas 进行了操作,那么这个操作是不可逆的,每次产生的画布的最新位置都是这些操作后的位置。

  3. 超出不显示

    在 Canvas 图层与屏幕合成时,超出屏幕范围的图像是不会显示出来的

4.3 画布裁剪

画布裁剪是指用 Canvas 的 clipXXX 函数与矩形、路径和区域取交、并、差等集合运算来获得最新的画布形状。

除非使用了保存和恢复函数,否则裁剪操作是不可逆的,也就是一旦裁剪就无法恢复。

在 Canvas 中定义了下面几个裁剪函数。

裁剪函数.png

下面是一个裁剪示例。

画布裁剪.png

4.4 画布保存

在前面我们说到了画布的操作是不可逆的,这会造成很多麻烦。

比如为了实现一些效果需要对画布进行操作,但画布状态改变后又影响了后面的绘制效果。

因为这个原因,画布提供了保存和恢复功能,这两个功能对应的方法是 save() 和 restore(),每调用一次 save() 就会把当前画布状态保存到栈中。

本节内容如下。

  • 多次保存
  • 指向恢复
  • 圆形头像

4.4.1 多次保存

画布单次保存恢复的操作比较简单,我们来看一下画布多次保存和多次恢复的效果。

画布多次保存.png

4.4.2 指向恢复

指向恢复指的是恢复到特定的状态,比如我们上面这个例子,假如我们要恢复到蓝色400,那我们要调用三次 resoter()。

为了解决这个问题,Canvas 提供了另一个恢复画布状态的函数 restoreToCount(count),下面我们来看一下这个函数的用法。

指向恢复.png

4.4.3 圆形头像

下面我们来看一下怎么用画布的保存和恢复功能实现绘制圆形头像后,再把画布恢复回来。

本节包含如下内容。

  • 初始化
  • 绘制头像
  • 去除锯齿

1. 初始化

这里之所以要禁用硬件加速,是因为硬件加速是使用的 OpenGL 函数完成实际绘制,而 OpenGL 并不能完全支持原始绘制函数,比如 clipPath() 在开启硬件加速的情况下只有在 API 18 以上的系统才会生效。

圆形头像初始化.png

2. 绘制头像

圆形头像.png

3. 去除锯齿

clipPath 实现的圆形图像是有锯齿的,我们可以用 PorterDuffXfermode 来实现无锯齿的圆形图片。

混合模式.png

参考资料

1. 书籍

  1. 《Android 自定义控件开发与实战》
  2. 《Android 自定义控件开发与实战》—下载资源

2. 文章

  1. Android Path 最佳实践之绘制雷达图
  2. Android自定义View(八) -- 硬件加速
  3. Android Bitmap 常见的几个操作:缩放,裁剪,旋转,偏移
  4. Android中测量文字的宽度和高度
  5. 使用PorterDuff解决clipPath无法抗锯齿问题
  6. Android Bitmap 图片裁剪成圆形Raw]

其他

  1. 示例代码源码