9dcfa72bdf30e8ab7ef71deb0c4ef888
安卓自定义View进阶-画笔基础(Paint)

在Android自定义View系列文章中,前面的部分有详细的讲解画布(Canvas)的功能和用法,但是和画布(Canvas)共同出现的画笔(Paint)却没有详细的讲解,本文带大家较为详细的了解一下画笔的相关内容。

Paint 在英文中作为名词时主要含义是涂料,油漆,颜料的意思,作为动词则具有绘画、粉刷的意思,不过在程序相关的中文博客里面,Paint 通常被解释为画笔,本文也将采用这种翻译,因此本文里面提到的画笔如没有特殊表明就指代 Paint。

0.引子

通过本系列前面的文章知道,View 上的内容是通过 Canvas 绘制出来的,但 Canvas 中的大多数绘制方法都是需要 Paint 作为参数的,例如 canvas.drawCircle(100, 100, 50, paint) 最后就需要传递一个 Paint。这是为什么呢?

因为画布本身只是呈现的一个载体,真正绘制出来的效果却要取决于画笔,就像同样白纸,要绘制一幅山水图,用毛笔画和用铅笔画的效果肯定是完全不同的,决定不同显示效果的并不是画布(Canvas), 而是画笔(Paint)。

同样,在程序设计中也采用的类似的设计思想,画布的 draw 方法只是规定了所需要绘制的是什么东西,但具体绘制出什么效果则通过画笔来控制。
例如: canvas.drawCircle(100, 100, 50, paint),这个方法说明了要在坐标 (100, 100) 的位置绘制一个半径为 50 的圆,但是这个圆具体要绘制成什么样子却没有明确的表明,圆的颜色,圆环还是圆饼等都没有明确的指示,而这些内容正存在于画笔之中。

1. 内容概览

既然是介绍画笔,自然要先总览一下它都有哪些功能,下面简要的列出一些本文中会涉及到的内容。

1.1 内部类

类型 简介
enum Paint.Cap
Cap指定了描边线和路径(Path)的开始和结束显示效果。
enum Paint.Join
Join指定线条和曲线段在描边路径上连接的处理。
enum Paint.Style
Style指定绘制的图元是否被填充,描边或两者均有(以相同的颜色)。

1.2 常量

类型 简介
int ANTI_ALIAS_FLAG
开启抗锯齿功能的标记。
int DITHER_FLAG
在绘制时启用抖动的标志。
int FILTER_BITMAP_FLAG
绘制标志,在缩放的位图上启用双线性采样。

1.3 构造方法

构造方法 摘要
Paint() 使用默认设置创建一个新画笔。
Paint(int flags) 创建一个新画笔并提供一些特殊设置(通过 flags 参数)。
Paint(Paint paint) 创建一个新画笔,并使用指定画笔参数初始化。

1.4 公开方法

画笔有 100 个左右的公开方法,限于篇幅,在本文中只会列举一部分方法,其余的内容,则放置于后续文章中再详细介绍。

返回值 简介
int getFlags()
获取画笔相关的一些设置(标志)。
int getFlags()
获取画笔相关的一些设置(标志)。
void setFlags(int flags)
设置画笔的标志位。
void set(Paint src)
复制 src 的画笔设置。
void reset()
将画笔恢复为默认设置。
int getAlpha()
只返回颜色的alpha值。
void setAlpha(int a)
设置透明度。
int getColor()
返回画笔的颜色。
void setColor(int color)
设置颜色。
void setARGB(int a, int r, int g, int b)
设置带透明通道的颜色。
float getStrokeWidth()
返回描边的宽度。
void setStrokeWidth(float width)
设置线条宽度。
Paint.Style getStyle()
返回paint的样式,用于控制如何解释几何元素(除了drawBitmap,它总是假定为FILL_STYLE)。
void setStyle(Paint.Style style)
设置画笔绘制模式(填充,描边,或两者均有)。
Paint.Cap getStrokeCap()
返回paint的Cap,控制如何处理描边线和路径的开始和结束。
void setStrokeCap(Paint.Cap cap)
设置线帽。
Paint.Join getStrokeJoin()
返回画笔的笔触连接类型。
void setStrokeJoin(Paint.Join join)
设置连接方式。
float getStrokeMiter()
返回画笔的笔触斜接值。用于在连接角度锐利时控制斜接连接的行为。
void setStrokeMiter(float miter)
设置画笔的笔触斜接值。用于在连接角度锐利时控制斜接连接的行为。
PathEffect getPathEffect()
获取画笔的 patheffect 对象。
PathEffect setPathEffect(PathEffect effect)
设置 Path 效果。
boolean getFillPath(Path src, Path dst)
将任何/所有效果(patheffect,stroking)应用于src,并将结果返回到dst。
结果是使用此画笔绘制绘制 src 将与使用默认画笔绘制绘制 dst 相同(至少从几何角度来说是这样的)。

2. 画笔介绍

由于画笔需要控制的内容也相当的多,因此它内部包含了相当多的属性变量,配置起来也相当繁杂,不过比较好的是,画笔会提供一套默认设置来供我们使用。例如,创建一个新画笔,这个新画笔已经默认设置了绘制颜色为黑色,绘制模式为填充。

2.1 画笔基本设置

要使用画笔就要会创建画笔,创建一个画笔是非常简单的,在之前的文章中也有过简单的介绍。它有三种创建方法,如下:

// 1.创建一个默认画笔,使用默认的配置
Paint()
// 2.创建一个新画笔,并通过 flags 参数进行配置。
Paint(int flags)
// 3.创建一个新画笔,并复制参数中画笔的设置。
Paint(Paint paint)

第1种方式创建默认画笔的方式相信大家都会。

第2种方式如果设置 flags 为 0 创建出来和默认画笔也是相同的,至于 flags 参数可用设置哪些内容,可以参考最上面的常量表格,里面的参数都是可以设置的,如果需要设置多个参数,参数之间用 | 进行连接即可。如下:

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 

第3种方式是根据已有的画笔复制一个画笔,就是将已有画笔的所有属性都复制到新画笔种,也比较容易理解:

Paint paintCopy = new Paint(paint);

复制后的画笔是一个全新的画笔,对复制后的画笔进行任何修改调整都不会影响到被复制的画笔。

你可以观察下面的测试代码来了解以上的3种创建方式。

Paint paint1 = new Paint();
Log.i(TAG, "paint1 isAntiAlias = " + paint1.isAntiAlias());
Log.i(TAG, "paint1 isDither = " + paint1.isDither());

Paint paint2 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
Log.i(TAG, "paint2 isAntiAlias = " + paint2.isAntiAlias());
Log.i(TAG, "paint2 isDither = " + paint2.isDither());

Paint paint3 = new Paint(paint2);
paint3.setAntiAlias(false);
Log.i(TAG, "paint3 isAntiAlias = " + paint3.isAntiAlias());
Log.i(TAG, "paint3 isDither = " + paint3.isDither());

输出结果:

paint1 isAntiAlias = false
paint1 isDither = false

paint2 isAntiAlias = true
paint2 isDither = true

paint3isAntiAlias = false
paint3 isDither = true

画笔在创建之后依旧可以调整,上面的第2种和第3种创建方式,进行的参数设置,在画笔创建完成后依旧可以进行。通过如下的方法:

返回值 简介
int getFlags()
获取画笔相关的一些设置(标志)。
void setFlags(int flags)
设置画笔的标志位。
void set(Paint src)
复制 src 的画笔设置。
void reset()
将画笔恢复为默认设置。

不过并不建议使用 setFlags 方法,这是因为 setFlags 方法会覆盖之前设置的内容,例如:

Paint paint = new Paint();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setFlags(Paint.DITHER_FLAG);
Log.i(TAG, "paint isAntiAlias = " + paint.isAntiAlias());
Log.i(TAG, "paint isDither = " + paint.isDither());

输出结果:

paint isAntiAlias = false
paint isDither = true

从结果可以看出,只有最后一次设置的内容有效,之前设置的所有内容都会被覆盖掉。因此不推荐使用。

如果了解 Google 工程师比较喜欢的编码规范就可以知道原因是什么,最终的flags是由多个flag用"或(|)"连接起来的,也就是一个变量,如果直接使用 set 方法,自然是会覆盖掉之前设置的内容的。如果想要调整 flag 个人建议还是使用 paint 提供的一些封装方法,如:setDither(true),而不要自己手动去直接操作 flag。

如果有人对 flags 的存储方式感兴趣可以看看这个例子,假如:0x0001 表示类型A, 0x0010 表示类型B,0x0100 表示类型C,0x1000 表示类型D,那么当类型ABD同时存在,但C不存在时时只用存储 0x1011 即可,相比于使用4个 boolean 值来说,这种方案可以显著的节省内存空间的占用,并且用户设置起来也比较方便,可以使用或"\|"同时设置多个类型。当然弊端也是有的,那就是单独更改其中一个参数时时稍微麻烦一点,需要进行一些位运算。

使用 set(Paint src) 可以复制一个画笔,但需要注意的是,如果调用了这个方法,当前画笔的所有设置都会被覆盖掉,而替换为所选画笔的设置。

如果想将画笔重置为初始状态,那就调用 reset() 方法,该方法会让画笔的所有设置都还原为初始状态,调用该方法后得到的画笔和刚创建时的状态是一样的。

2.2 画笔颜色

这个是最常用的方法,它相关的方法如下;

返回值 简介
int getAlpha()
只返回颜色的alpha值。
void setAlpha(int a)
设置透明度。
int getColor()
返回画笔的颜色。
void setColor(int color)
设置颜色。
void setARGB(int a, int r, int g, int b)
设置带透明通道的颜色。

Android 中有 1 个透明通道(Alpha)和 3 个色彩通道(RGB),其中 Alpha 通道可以单独设置。通过 getAlpha()setAlpha(int a) 方法可以单独调整透明通道,其中 setAlpha(int a) 中参数的取值范围是 0 - 255,即对应 16 进制中的 0x00 - 0xFF。

// 下面两种设置方式是等价的,一种是 10 进制,一种是 16 进制
paint1.setAlpha(204);
paint2.setAlpha(0xCC);

同理,setARGB(int a, int r, int g, int b) 的 4 个参数的取值范围也是 0 - 255,对应 0x00 - 0xFF,下面的设置同样是等价的。

paint1.setARGB(204, 255, 255, 0);
paint2.setARGB(0xCC, 0xFF, 0xFF, 0x00);

当然,这样设置起来比较麻烦,我们最常用的还是直接使用 setColor(int color) 方法,它接受一个 int 类型的参数来表示颜色,我们既可以使用系统内置的一些标准颜色,也可以使用自定义的一些颜色,如下:

paint.setColor(Color.GREEN);
paint.setColor(0xFFE2A588);

注意:

在使用 setColor 方法时,所设置的颜色必须是 ARGB 同时存在的,通常每个通道用两位16进制数值表示,如 0xFFE2A588。总共 8 位,其中 FF 表示 Alpha 通道。

如果不设置 Alpha 通道,则默认Alpha通道为 0,即完全透明,如:0xE2A588,总共 6 位,没有 Alpha 通道,如果这样设置,则什么颜色也绘制不出来。

同样需要注意的是,setColor 不能直接引用资源,不能这样使用:paint.setColor(R.color.colorPrimary); 如果你这样使用了,编译器会报错的。如果想要使用预定义的颜色资源,可以像下面这样调用:

int color = context.getResources().getColor(R.color.colorPrimary);
paint.setColor(color);

2.4 画笔宽度

画笔宽度,就是画笔的粗细,它通过下面的方式设置。

// 将画笔设置为描边
paint.setStyle(Paint.Style.STROKE);
// 设置线条宽度
paint.setStrokeWidth(120);

注意: 这条线的宽度是同时向两边进行扩展的,例如绘制一个圆时,将其宽度设置为 120 则会向外扩展 60 ,向内缩进 60,如下图所示。

因此如果绘制的内容比较靠近视图边缘,使用了比较粗的描边的情况下,一定要注意和边缘保持一定距离(边距>StrokeWidth/2) 以保证内容不会被剪裁掉。

如下面这种情况,直接绘制一个矩形,如果不考虑画笔宽度,则绘制的内容就可能不正常。

在一个 1000x1000 大小的画布上绘制与个大小为 500x500 ,宽度为 100 的矩形。

灰色部分为画布大小。
红色为分割线,将画笔分为均等的四份。
蓝色为矩形。

paint.setStrokeWidth(100);
paint.setColor(0xFF7FC2D8);
Rect rect = new Rect(0, 0, 500, 500);
canvas.drawRect(rect, paint);

如果考虑到画笔宽度,需要绘制一个大小刚好填充满左上角区域的矩形,那么实际绘制的矩形就要小一些,(如果只是绘制一个矩形的话,可以将矩形向内缩小画笔宽度的一半) 这样绘制出来就是符合预期的。

paint.setStrokeWidth(100);
paint.setColor(0xFF7FC2D8);
Rect rect = new Rect(0, 0, 500, 500);
rect.inset(50, 50);     // 注意这里,向内缩小半个宽度
canvas.drawRect(rect, paint);

这里只是用矩形作为例子说明,事实上,绘制任何图形,只要有描边的,就要考虑描边宽度占用的空间,需要适当的缩小图形,以保证其可以完整的显示出来。

注意:在实际的自定义 View 中也不要忽略 padding 占用的空间哦。

hairline mode (发际线模式):

在设置画笔宽度的的方法有如下注释:

Set the width for stroking.
Pass 0 to stroke in hairline mode.
Hairlines always draws a single pixel independent of the canva's matrix.

在画笔宽度为 0 的情况下,使用 drawLine 或者使用描边模式(STROKE)也可以绘制出内容。只是绘制出的内容始终是 1 像素,不受画布缩放的影响。该模式被称为hairline mode (发际线模式)

如果你设置了画笔宽度为 1 像素,那么如果画布放大到 2 倍,1 像素会变成 2 像素。但如果是 0 像素,那么不论画布如何缩放,绘制出来的宽度依旧为 1 像素。

// 缩放 5 倍
canvas.scale(5, 5, 500, 500);

// 0 像素 (Hairline Mode)
paint.setStrokeWidth(0);
paint.setColor(0xFF7FC2D8);
canvas.drawCircle(500, 455, 40, paint);

// 1 像素
paint.setStrokeWidth(1);
paint.setColor(0xFF7FC2D8);
canvas.drawCircle(500, 545, 40, paint);

可以看到,在放大 5 倍的情况下,1 像素已经变成了 5 像素,但 hairline mode 绘制出来依旧是 1 像素。

2.3 画笔模式

这里的画笔模式(Paint.Style)就是指绘制一个图形时,是绘制边缘轮廓,绘制内容区域还是两者都绘制,它有三种模式。

Style 简介
Paint.Style.FILL 填充内容,也是画笔的默认模式。
Paint.Style.STROKE 描边,只绘制图形轮廓。
Paint.Style.FILL_AND_STROKE 描边+填充,同时绘制轮廓和填充内容。
//填充
mPaint.setStyle(Paint.Style.FILL);
// 描边
mPaint.setStyle(Paint.Style.STROKE);
// 描边+填充
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

示例程序:

用一个简单的例子说明一下不同模式的区别。

// 画笔初始设置
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(50);
paint.setColor(0xFF7FC2D8);

// 填充,默认
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(500, 200, 100, paint);

// 描边
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(500, 500, 100, paint);

// 描边 + 填充
paint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(500, 800, 100, paint);

2.5 画笔线帽

画笔线帽(Paint.Cap)用于指定线段开始和结束时的效果。

// 它通过下面方式设置
paint.setStrokeCap(Paint.Cap.ROUND);

Android 中有三种线帽可供选择。

Cap 简介
Paint.Cap.BUTT 无线帽,也是默认类型。
Paint.Cap.SQUARE 以线条宽度为大小,在开头和结尾分别添加半个正方形。
Paint.Cap.ROUND 以线条宽度为直径,在开头和结尾分别添加一个半圆。

我们用以下代码来测试线帽。

// 画笔初始设置
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
paint.setStrokeWidth(80);
float pointX = 200;
float lineStartX = 320;
float lineStopX = 800;
float y;

// 默认
y = 200;
canvas.drawPoint(pointX, y, paint);
canvas.drawLine(lineStartX, y, lineStopX, y, paint);

// 无线帽(BUTT)
y = 400;
paint.setStrokeCap(Paint.Cap.BUTT);
canvas.drawPoint(pointX, y, paint);
canvas.drawLine(lineStartX, y, lineStopX, y, paint);

// 方形线帽(SQUARE)
y = 600;
paint.setStrokeCap(Paint.Cap.SQUARE);
canvas.drawPoint(pointX, y, paint);
canvas.drawLine(lineStartX, y, lineStopX, y, paint);

// 圆形线帽(ROUND)
y = 800;
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(pointX, y, paint);
canvas.drawLine(lineStartX, y, lineStopX, y, paint);

注意:

  1. 画笔默认是无线帽的,即 BUTT。
  2. Cap 也会影响到点的绘制,在 Round 的状态下绘制的点是圆的。
  3. 在绘制线条时,线帽时在线段外的,如上图红色部分所显示的内容就是线帽。
  4. 上图中红色的线帽是用特殊方式展示出来的,直接绘制的情况下,线帽颜色和线段颜色相同。

2.6 线段连接方式(拐角类型)

画笔的连接方式(Paint.Join)是指两条连接起来的线段拐角显示方式。

// 通过下面方式设置连接类型
paint.setStrokeJoin(Paint.Join.ROUND);

它同样有三种样式:

Cap 简介
Paint.Join.MITER 尖角 (默认模式)
Paint.Join.BEVEL 平角
Paint.Join.ROUND 圆角

通过效果图可以看出几种不同模式的补偿规则。

2.7 斜接模式长度限制

Android 中线段连接方式默认是 MITER,即在拐角处延长外边缘,直到相交位置。

top Created with Sketch.