android自定义View学习2-绘制(Canvas+paint)

参考

hencoder(极力推荐,讲得很细致,本文只是记录我学到的东西,不全,建议看这篇文章)
文档Canvas
文档paint

自定义绘制

  • 自定义绘制的方式是重写绘制方法,其中最常用的是 onDraw()
  • 绘制的关键是 Canvas 的使用
    • Canvas 的绘制类方法: drawXXX() (关键参数:Paint)
    • Canvas 的辅助类方法:范围裁切和几何变换
  • 可以使用不同的绘制方法来控制遮盖关系
  • Canvas确定画什么内容,paint决定怎么画

Canvas

Canvas从名字来看是画布,在安卓中他是一个绘制工具,就是用来绘制

常用的几个绘制方法:

方法 作用
drawColor 颜色填充
drawCircle 画圆
drawRect 画矩形
drawPoint 画点
drawPoints 批量画点
drawOval 画椭圆
drawLine 画线
drawLines 批量画线
drawRoundRect 画圆角矩形
drawArc 画弧形或者扇形
drawBitmap() 画一个Bitmap
drawText() 画文字
drawPath 画自定义图形

Canvas.drawColor(@ColorInt int color) 颜色填充

这是最基本的 drawXXX() 方法:在整个绘制区域统一涂上指定的颜色。

  • 例如 drawColor(Color.BLACK) 会把整个区域染成纯黑色,覆盖掉原有内容; drawColor(Color.parse(“#88880000”) 会在原有的绘制效果上加一层半透明的红色遮罩。

    1
    2
    drawColor(Color.BLACK);  // 纯黑
    drawColor(Color.parse("#88880000")); // 半透明红色
  • 类似的方法还有 drawRGB(int r, int g, int b) 和 drawARGB(int a, int r, int g, int b) ,它们和 drawColor(color) 只是使用方式不同,作用都是一样的。

    1
    drawColor(Color.parse("#88880000")); // 半透明红色

drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆

前两个参数 centerX centerY 是圆心的坐标,第三个参数 radius 是圆的半径,单位都是像素,它们共同构成了这个圆的基本信息(即用这几个信息可以构建出一个确定的圆);第四个参数 paint 它提供基本信息之外的所有风格信息,例如颜色、线条粗细、阴影等。

1
canvas.drawCircle(300, 300, 200, paint);

drawRect(float left, float top, float right, float bottom, Paint paint) 画矩形

left, top, right, bottom 是矩形四条边的坐标。

1
2
3
4
5
paint.setStyle(Style.FILL);
canvas.drawRect(100, 100, 500, 500, paint);//画一个实心矩形

paint.setStyle(Style.STROKE);
canvas.drawRect(700, 100, 1100, 500, paint);//画一个空心矩形

  • 另外,它还有两个重载方法 drawRect(RectF rect, Paint paint) 和 drawRect(Rect rect, Paint paint) ,让你可以直接填写 RectF 或 Rect 对象来绘制矩形。

drawPoint(float x, float y, Paint paint) 画点

x 和 y 是点的坐标。点的大小可以通过 paint.setStrokeWidth(width) 来设置;点的形状可以通过 paint.setStrokeCap(cap) 来设置:ROUND 画出来是圆形的点,SQUARE 或 BUTT 画出来是方形的点。

注:Paint.setStrokeCap(cap) 可以设置点的形状,但这个方法并不是专门用来设置点的形状的,而是一个设置线条端点形状的方法。端点有圆头 (ROUND)、平头 (BUTT) 和方头 (SQUARE) 三种

1
2
3
4
5
6
7
8
paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(50, 50, paint);//画一个半径是20的圆点


paint.setStrokeWidth(20);
paint.setStrokeCap(Paint.Cap.SQUARE);
canvas.drawPoint(50, 50, paint);//画一个半径是20的方点

好像有点像 FILL 模式下的 drawCircle() 和 drawRect() ?事实上确实是这样的,它们和 drawPoint() 的绘制效果没有区别。各位在使用的时候按个人习惯和实际场景来吧,哪个方便和顺手用哪个。

drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 画点(批量)

同样是画点,它和 drawPoint() 的区别是可以画多个点。pts 这个数组是点的坐标,每两个成一对;offset 表示跳过数组的前几个数再开始记坐标;count 表示一共要绘制几个点。说这么多你可能越读越晕,你还是自己试试吧,这是个看着复杂用着简单的方法。

1
2
3
float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};
// 绘制四个点:(50, 50) (50, 100) (100, 50) (100, 100)
canvas.drawPoints(points, 2 /* 跳过两个数,即前两个 0 */,8 /* 一共绘制 8 个数(4 个点)*/, paint);

drawOval(float left, float top, float right, float bottom, Paint paint) 画椭圆

只能绘制横着的或者竖着的椭圆,不能绘制斜的(斜的倒是也可以,但不是直接使用 drawOval(),而是配合几何变换,后面会讲到)。left, top, right, bottom 是这个椭圆的左、上、右、下四个边界点的坐标。

1
2
3
4
5
paint.setStyle(Style.FILL);
canvas.drawOval(50, 50, 350, 200, paint);//画一个实心椭圆

paint.setStyle(Style.STROKE);
canvas.drawOval(400, 50, 700, 200, paint);//画一个空心椭圆

另外,它还有一个重载方法 drawOval(RectF rect, Paint paint),让你可以直接填写 RectF 来绘制椭圆。

drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画线

startX, startY, stopX, stopY 分别是线的起点和终点坐标。

1
canvas.drawLine(200, 200, 800, 500, paint);

  • 由于直线不是封闭图形,所以 setStyle(style) 对直线没有影响。

drawLines(float[] pts, int offset, int count, Paint paint) / drawLines(float[] pts, Paint paint) 画线(批量)

drawLines() 是 drawLine() 的复数版。

1
2
float[] points = {20, 20, 120, 20, 70, 20, 70, 120, 20, 120, 120, 120, 150, 20, 250, 20, 150, 20, 150, 120, 250, 20, 250, 120, 150, 120, 250, 120};
canvas.drawLines(points, paint);

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 画圆角矩形

left, top, right, bottom 是四条边的坐标,rx 和 ry 是圆角的横向半径和纵向半径。

1
canvas.drawRoundRect(100, 100, 500, 300, 50, 50, paint);

  • 另外,它还有一个重载方法 drawRoundRect(RectF rect, float rx, float ry, Paint paint),让你可以直接填写 RectF 来绘制圆角矩形。

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形

drawArc() 是使用一个椭圆来描述弧形的。left, top, right, bottom 描述的是这个弧形所在的椭圆;startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。

1
2
3
4
5
paint.setStyle(Paint.Style.FILL); // 填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint); // 绘制扇形
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint); // 绘制弧形
paint.setStyle(Paint.Style.STROKE); // 画线模式
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint); // 绘制不封口的弧形

组合

到此为止,以上就是 Canvas 所有的简单图形的绘制。除了简单图形的绘制, Canvas 还可以使用 drawPath(Path path) 来绘制自定义图形。

drawPath(Path path, Paint paint) 画自定义图形

这个方法是先用path指定一个要画的路径,然后画出来
比如画一个心

1
2
3
4
5
// 使用 path 对图形进行描述(这段描述代码不必看懂)
path.addArc(200, 200, 400, 400, -225, 225);
path.arcTo(400, 200, 600, 400, -180, 225, false);
path.lineTo(400, 542);
canvas.drawPath(path,paint);

自定义心形
先用path绘制出新型的路径,然后画出来
path具体使用看本章以下内容

drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画 Bitmap

绘制 Bitmap 对象,也就是把这个 Bitmap 中的像素内容贴过来。其中 left 和 top 是要把 bitmap 绘制到的位置坐标。它的使用非常简单。

drawBitmap(bitmap, 200, 100, paint);

1
2
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(),R.mipmap.ic_launcher);
canvas.drawBitmap(bitmap,0,0,paint);

画图片
它的重载方法:

  • drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
  • drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
  • drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)

drawBitmap 还有一个兄弟方法 drawBitmapMesh(),可以绘制具有网格拉伸效果的 Bitmap

drawText(String text, float x, float y, Paint paint) 绘制文字

界面里所有的显示内容,都是绘制出来的,包括文字。 drawText() 这个方法就是用来绘制文字的。参数 text 是用来绘制的字符串,x 和 y 是绘制的起点坐标。

canvas.drawText(text, 200, 100, paint);

1
2
String text = "你好,hello,拜拜,bye";
canvas.drawText(text,100,100,paint);

还能设置多种字体,具体请看paint的相关用法

绘制范围的裁切

绘制内容的几何变换

  • 就是绘制四个顶点的变换

Paint

Paint从名字看是颜料,在安卓中加强版颜料,比如颜色,是否空心,风格,阴影等

Paint.setColor(int color)/setARGB(int a, int r, int g, int b)设置颜色

1
2
paint.setColor(Color.RED); // 设置为红色
canvas.drawCircle(300, 300, 200, paint); //画一个坐标300*300半径200的红色圆

setShader(Shader shader) 设置 Shader(着色器)

LinearGradient 线性渐变

1
2
3
Shader shader = new LinearGradient(300, 100, 300, 500, Color.RED,Color.GREEN, Shader.TileMode.CLAMP);
paint.setShader(shader);
canvas.drawCircle(300, 300, 200, paint);

线性渐变图片丢失

  • 前四个参数表示两个点的x和y坐标,第五六个参数表示前面两个点的颜色,最后一个参数表示端点范围之外的着色规则
  • 端点范围之外的着色规则一共有 3 个值可选: CLAMP, MIRROR 和 REPEAT。CLAMP会在端点之外延续端点处的颜色;MIRROR 是镜像模式;REPEAT 是重复模式。
    CLAMP:
    CLAMP图片丢失
    MIRROR:
    MIRROR图片丢失
    REPEAT:
    REPEAT图片丢失

RadialGradient 辐射渐变

辐射渐变很好理解,就是从中心向周围辐射状的渐变

1
2
3
Shader shader = new RadialGradient(300, 300, 200,Color.RED,Color.GREEN, Shader.TileMode.CLAMP);
paint.setShader(shader);
canvas.drawCircle(300, 300, 200, paint);

辐射渐变图片丢失

  • centerX centerY:辐射中心的坐标
  • radius:辐射半径
  • centerColor:辐射中心的颜色
  • edgeColor:辐射边缘的颜色
  • tileMode:辐射范围之外的着色模式。
    CLAMP:
    CLAMP图片丢失
    MIRROR:
    MIRROR图片丢失
    REPEAT:
    REPEAT图片丢失

SweepGradient 扫描渐变

1
Shader shader = new SweepGradient(300, 300, Color.parseColor("#E91E63"),Color.parseColor("#2196F3"));

扫描渐变图片丢失

  • cx cy :扫描的中心
  • color0:扫描的起始颜色
  • color1:扫描的终止颜色

BitmapShader(图着色)

用 Bitmap 来着色:

1
2
3
4
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.batman);
Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(shader);
canvas.drawCircle(300, 300, 200, paint)

BitmapShader图片丢失

  • bitmap:用来做模板的 Bitmap 对象
  • tileX:横向的 TileMode
  • tileY:纵向的 TileMode。
    CLAMP:
    CLAMP图片丢失
    MIRROR:
    MIRROR图片丢失
    REPEAT:
    REPEAT图片丢失

ComposeShader 混合着色器

所谓混合,就是把两个 Shader 一起使用。

1
2
3
4
5
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
Shader shader1 = new SweepGradient(200, 200, Color.parseColor("#E91E63"), Color.parseColor("#2196F3"));
Shader shader2 = new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
Shader shader = new ComposeShader(shader1,shader2,PorterDuff.Mode.SRC_OVER);
paint.setShader(shader);

ComposeShader图片丢失

  • 上面这段代码我是用了一个渐变着色器和一个图片着色器混合而成的效果
  • 注意,如果是两个相同类型的shader在开启硬件加速的情况下是异常的
  • shaderA, shaderB:两个相继使用的 Shader
  • mode: 两个 Shader 的叠加模式,即 shaderA 和 shaderB 应该怎样共同绘制。它的类型是 PorterDuff.Mode

PorterDuff.Mode
PorterDuff.Mode 是用来指定两个图像共同绘制时的颜色策略的。它是一个 enum,不同的 Mode 可以指定不同的策略。「颜色策略」的意思,就是说把源图像绘制到目标图像处时应该怎样确定二者结合后的颜色,而对于 ComposeShader(shaderA, shaderB, mode) 这个具体的方法,就是指应该怎样把 shaderB 绘制在 shaderA 上来得到一个结合后的 Shader。
最符合直觉的结合策略,就是我在上面这个例子中使用的 Mode: SRC_OVER。它的算法非常直观:就像上面图中的那样,把源图像直接铺在目标图像上。不过,除了这种,其实还有一些其他的结合方式。具体来说, PorterDuff.Mode 一共有 17 个,可以分为两类:

  • Alpha 合成 (Alpha Compositing)
  • 混合 (Blending)
    第一类,Alpha 合成,其实就是 「PorterDuff」 这个词所指代的算法。 「PorterDuff」 并不是一个具有实际意义的词组,而是两个人的名字(准确讲是姓)。这两个人当年共同发表了一篇论文,描述了 12 种将两个图像共同绘制的操作(即算法)。而这篇论文所论述的操作,都是关于 Alpha 通道(也就是我们通俗理解的「透明度」)的计算的,后来人们就把这类计算称为Alpha 合成 ( Alpha Compositing ) 。
    源图像和目标图像:
    原图片和目标图片丢失
    Alpha 合成:
    Alpha合成图片丢失
    第二类,混合,也就是 Photoshop 等制图软件里都有的那些混合模式(multiply darken lighten 之类的)。这一类操作的是颜色本身而不是 Alpha 通道,并不属于 Alpha 合成,所以和 Porter 与 Duff 这两个人也没什么关系,不过为了使用的方便,它们同样也被 Google 加进了 PorterDuff.Mode 里。
    混合图片丢失
结论

从效果图可以看出,Alpha 合成类的效果都比较直观,基本上可以使用简单的口头表达来描述它们的算法(起码对于不透明的源图像和目标图像来说是可以的),例如 SRC_OVER 表示「二者都绘制,但要源图像放在目标图像的上面」,DST_IN 表示「只绘制目标图像,并且只绘制它和源图像重合的区域」。

setColorFilter(ColorFilter colorFilter)颜色过滤

  • 颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX() 方法会对每个像素都进行过滤后再绘制出来。
  • 颜色效果就像透过一个有色玻璃或者有色光照射

LightingColorFilter简单的光照效果

  • LightingColorFilter 的构造方法是 LightingColorFilter(int mul, int add) ,参数里的 mul 和 add 都是和颜色值格式相同的 int 值,其中 mul 用来和目标像素相乘,add 用来和目标像素相加:

    1
    2
    3
    R' = R * mul.R / 0xff + add.R
    G' = G * mul.G / 0xff + add.G
    B' = B * mul.B / 0xff + add.B
  • 一个「保持原样」的「基本 LightingColorFilter 」,mul 为 0xffffff,add 为 0x000000(也就是0),那么对于一个像素,它的计算过程就是:

    1
    2
    3
    R' = R * 0xff / 0xff + 0x0 = R // R' = R
    G' = G * 0xff / 0xff + 0x0 = G // G' = G
    B' = B * 0xff / 0xff + 0x0 = B // B' = B
  • 基于这个「基本 LightingColorFilter 」,你就可以修改一下做出其他的 filter。比如,如果你想去掉原像素中的红色,可以把它的 mul 改为 0x00ffff (红色部分为 0 ) ,那么它的计算过程就是:

    1
    2
    3
    R' = R * 0x0 / 0xff + 0x0 = 0 // 红色被移除
    G' = G * 0xff / 0xff + 0x0 = G
    B' = B * 0xff / 0xff + 0x0 = B
1
2
paint.setColorFilter(new LightingColorFilter(0xff00ff,0x000000));//去除颜色中的绿色
canvas.drawCircle(200, 650, 200, paint);

LightingColorFilter图片丢失

  • 设置mul中绿色部分为0,可以去除图片中的绿色,也可以稍微大一些,减弱绿色效果
  • 也可以设置add的值用来加强绿色部分
    1
    2
    paint.setColorFilter(new LightingColorFilter(0xffffff,0x005000));
    canvas.drawCircle(200, 650, 200, paint);

绿色加强图片丢失

PorterDuffColorFilter

这个 PorterDuffColorFilter 的作用是使用一个指定的颜色和一种指定的 PorterDuff.Mode 来与绘制对象进行合成。它的构造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode) 其中的 color 参数是指定的颜色, mode 参数是指定的 Mode。同样也是 PorterDuff.Mode ,不过和 ComposeShader 不同的是,PorterDuffColorFilter 作为一个 ColorFilter,只能指定一种颜色作为源,而不是一个 Bitmap。

ColorMatrixColorFilter

这个就厉害了。ColorMatrixColorFilter 使用一个 ColorMatrix 来对颜色进行处理。 ColorMatrix 这个类,内部是一个 4x5 的矩阵:

[ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t ]
通过计算, ColorMatrix 可以把要绘制的像素进行转换。对于颜色 [R, G, B, A] ,转换算法是这样的:

R’ = aR + bG + cB + dA + e;
G’ = fR + gG + hB + iA + j;
B’ = kR + lG + mB + nA + o;
A’ = pR + qG + rB + sA + t;
ColorMatrix 有一些自带的方法可以做简单的转换,例如可以使用 setSaturation(float sat) 来设置饱和度;另外你也可以自己去设置它的每一个元素来对转换效果做精细调整。具体怎样设置会有怎样的效果,我就不讲了(其实是我也不太会)。如果你有需求,可以试一下程大治同学做的这个库:(StyleImageView)[https://github.com/chengdazhi/StyleImageView]

以上,就是 Paint 对颜色的第二层处理:通过 setColorFilter(colorFilter) 来加工颜色。

除了基本颜色的设置( setColor/ARGB(), setShader() )以及基于原始颜色的过滤( setColorFilter() )之外,Paint 最后一层处理颜色的方法是 setXfermode(Xfermode xfermode) ,它处理的是「当颜色遇上 View」的问题。

Paint.setXfermode(Xfermode xfermode)

“Xfermode” 其实就是 “Transfer mode”,用 “X” 来代替 “Trans” 是一些美国人喜欢用的简写方式。严谨地讲, Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色。但通俗地说,其实就是要你以绘制的内容作为源图像,以 View 中已有的内容作为目标图像,选取一个 PorterDuff.Mode 作为绘制内容的颜色处理方案。就像这样:

1
2
3
4
5
6
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
...
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方
paint.setXfermode(xfermode); // 设置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆
paint.setXfermode(null); // 用完及时清除 Xfermode

图片丢失
图片丢失

又是 PorterDuff.Mode 。 PorterDuff.Mode 在 Paint 一共有三处 API ,它们的工作原理都一样,只是用途不同:

图片丢失

另外,从上面的示例代码可以看出,创建 Xfermode 的时候其实是创建的它的子类 PorterDuffXfermode。而事实上,Xfermode 也只有这一个子类。所以在设置 Xfermode 的时候不用多想,直接用 PorterDuffXfermode 吧。

其实在更早的 Android 版本中,Xfermode 还有别的子类,但别的子类现在已经 deprecated 了,如今只剩下了 PorterDuffXfermode。所以目前它的使用看起来好像有点啰嗦,但其实是由于历史遗留问题。

Xfermode 注意事项

Xfermode 使用很简单,不过有两点需要注意:

  1. 使用离屏缓冲(Off-screen Buffer)
    实质上,上面这段例子代码,如果直接执行的话是不会绘制出图中效果的,程序的绘制也不会像上面的动画那样执行,而是会像这样:

图片丢失

为什么会这样?
按照逻辑我们会认为,在第二步画圆的时候,跟它共同计算的是第一步绘制的方形。但实际上,却是整个 View 的显示区域都在画圆的时候参与计算,并且 View 自身的底色并不是默认的透明色,而且是遵循一种迷之逻辑,导致不仅绘制的是整个圆的范围,而且在范围之外都变成了黑色。就像这样:

图片丢失

这……那可如何是好?

要想使用 setXfermode() 正常绘制,必须使用离屏缓存 (Off-screen Buffer) 把内容绘制在额外的层上,再把绘制好的内容贴回 View 中。也就是这样:
图片丢失

通过使用离屏缓冲,把要绘制的内容单独绘制在缓冲层, Xfermode 的使用就不会出现奇怪的结果了。使用离屏缓冲有两种方式:

  • Canvas.saveLayer()

saveLayer() 可以做短时的离屏缓冲。使用方法很简单,在绘制代码的前后各加一行代码,在绘制之前保存,绘制之后恢复:

1
2
3
4
5
6
7
8
9
10
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);


canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方
paint.setXfermode(xfermode); // 设置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆
paint.setXfermode(null); // 用完及时清除 Xfermode


canvas.restoreToCount(saved);

  • View.setLayerType()

View.setLayerType() 是直接把整个 View 都绘制在离屏缓冲中。 setLayerType(LAYER_TYPE_HARDWARE) 是使用 GPU 来缓冲, setLayerType(LAYER_TYPE_SOFTWARE) 是直接直接用一个 Bitmap 来缓冲。

如果没有特殊需求,可以选用第一种方法 Canvas.saveLayer() 来设置离屏缓冲,以此来获得更高的性能。更多关于离屏缓冲的信息,可以看官方文档中对于硬件加速的介绍。

  1. 控制好透明区域
    使用 Xfermode 来绘制的内容,除了注意使用离屏缓冲,还应该注意控制它的透明区域不要太小,要让它足够覆盖到要和它结合绘制的内容,否则得到的结果很可能不是你想要的。我用图片来具体说明一下:

图片丢失

如图所示,由于透明区域过小而覆盖不到的地方,将不会受到 Xfermode 的影响。

好,到此为止,前面讲的就是 Paint 的第一类 API——关于颜色的三层设置:直接设置颜色的 API 用来给图形和文字设置颜色; setColorFilter() 用来基于颜色进行过滤处理; setXfermode() 用来处理源图像和 View 已有内容的关系。

再贴一次本章开始处的图作为回顾:

图片丢失

Paint.setStyle(Paint.Style style)设置绘制风格

而如果你想画的不是实心圆,而是空心圆(或者叫环形),也可以使用 paint.setStyle(Paint.Style.STROKE) 来把绘制模式改为画线模式。

1
2
paint.setStyle(Paint.Style.STROKE); // Style 修改为画线模式
canvas.drawCircle(300, 300, 200, paint); //只会画出这个圆的边线

  • setStyle(Style style) 这个方法设置的是绘制的 Style 。Style 具体来说有三种: FILL, STROKE 和 FILL_AND_STROKE 。FILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。

Paint.setStrokeWidth(float width)填充线条的宽度

  • 在style为FILL_AND_STROKE或者STROKE模式下可以用这个设置填充线条的宽度
    1
    2
    3
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(20); // 线条宽度为 20 像素
    canvas.drawCircle(300, 300, 200, paint); //画一个线宽度为20的圆环

线条宽度 0 和 1 的区别
默认情况下,线条宽度为 0,但你会发现,这个时候它依然能够画出线,线条的宽度为 1 像素。那么它和线条宽度为 1 有什么区别呢?
其实这个和后面要讲的一个「几何变换」有关:你可以为 Canvas 设置 Matrix 来实现几何变换(如放大、缩小、平移、旋转),在几何变换之后 Canvas 绘制的内容就会发生相应变化,包括线条也会加粗,例如 2 像素宽度的线条在 Canvas 放大 2 倍后会被以 4 像素宽度来绘制。而当线条宽度被设置为 0 时,它的宽度就被固定为 1 像素,就算 Canvas 通过几何变换被放大,它也依然会被以 1 像素宽度来绘制。Google 在文档中把线条宽度为 0 时称作「hairline mode(发际线模式)」。

抗锯齿

在绘制的时候,往往需要开启抗锯齿来让图形和文字的边缘更加平滑。开启抗锯齿很简单,只要在 new Paint() 的时候加上一个 ANTI_ALIAS_FLAG 参数就行:

1
2
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//或者调用setAntiAlias(true)

  • 抗锯齿的原理也并不是选择了更精细的算法来算出了更平滑的图形边缘。 实质上,锯齿现象的发生,只是由于图形分辨率过低,导致人眼察觉出了画面中的像素颗粒而已。换句话说,就算不开启抗锯齿,图形的边缘也已经是最完美的了,而并不是一个粗略计算的粗糙版本。 抗锯齿的原理是:修改图形边缘处的像素颜色,从而让图形在肉眼看来具有更加平滑的感觉
  • 简单点说抗锯齿会失真

setStrokeCap(Paint.Cap cap)

设置线头的形状。线头形状有三种:BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT。

放出「平头」「圆头」「方头」这种翻译我始终有点纠结:既觉得自己翻译得简洁清晰尽显机智,同时又担心用词会不会有点太过通俗,让人觉得我不够高贵冷艳?

当线条的宽度是 1 像素时,这三种线头的表现是完全一致的,全是 1 个像素的点;而当线条变粗的时候,它们就会表现出不同的样子:
图片丢失
虚线是额外加的,虚线左边是线的实际长度,虚线右边是线头。有了虚线作为辅助,可以清楚地看出 BUTT 和 SQUARE 的区别。

setStrokeJoin(Paint.Join join)

设置拐角的形状。有三个值可以选择:MITER 尖角、 BEVEL 平角和 ROUND 圆角。默认为 MITER。

图片丢失

setStrokeMiter(float miter)

这个方法是对于 setStrokeJoin() 的一个补充,它用于设置 MITER 型拐角的延长线的最大值。所谓「延长线的最大值」,是这么一回事:

当线条拐角为 MITER 时,拐角处的外缘需要使用延长线来补偿:
图片丢失
而这种补偿方案会有一个问题:如果拐角的角度太小,就有可能由于出现连接点过长的情况。比如这样:
图片丢失
所以为了避免意料之外的过长的尖角出现, MITER 型连接点有一个额外的规则:当尖角过长时,自动改用 BEVEL 的方式来渲染连接点。例如上图的这个尖角,在默认情况下是不会出现的,而是会由于延长线过长而被转为 BEVEL 型连接点:
图片丢失
至于多尖的角属于过于尖,尖到需要转为使用 BEVEL 来绘制,则是由一个属性控制的,而这个属性就是 setStrokeMiter(miter) 方法中的 miter 参数。miter 参数是对于转角长度的限制,具体来讲,是指尖角的外缘端点和内部拐角的距离与线条宽度的比。也就是下面这两个长度的比:
图片丢失
用几何知识很容易得出这个比值的计算公式:如果拐角的大小为 θ ,那么这个比值就等于 1 / sin ( θ / 2 ) 。

这个 miter limit 的默认值是 4,对应的是一个大约 29° 的锐角:
图片丢失

默认情况下,大于这个角的尖角会被保留,而小于这个夹角的就会被「削成平头」

setDither(boolean dither)

设置图像的抖动。

setFilterBitmap(boolean filter)

设置是否使用双线性过滤来绘制 Bitmap 。

图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。效果依然盗维基百科的图:
图片丢失

1
paint.setFilterBitmap(true);

加上这一行,在放大绘制 Bitmap 的时候就会使用双线性过滤了。

setPathEffect(PathEffect effect)

使用 PathEffect 来给图形的轮廓设置效果。对 Canvas 所有的图形绘制有效,也就是 drawLine() drawCircle() drawPath() 这些方法。大概像这样:

1
2
3
4
5
6
PathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 10);
paint.setPathEffect(pathEffect);

...

canvas.drawCircle(300, 300, 200, paint);

图片丢失

下面就具体说一下 Android 中的 6 种 PathEffect。PathEffect 分为两类,单一效果的 CornerPathEffect DiscretePathEffect DashPathEffect PathDashPathEffect ,和组合效果的 SumPathEffect ComposePathEffect。

CornerPathEffect

把所有拐角变成圆角。

1
2
3
4
PathEffect pathEffect = new CornerPathEffect(20);
paint.setPathEffect(pathEffect);
...
canvas.drawPath(path, paint);

图片丢失
它的构造方法 CornerPathEffect(float radius) 的参数 radius 是圆角的半径。

DiscretePathEffect

把线条进行随机的偏离,让轮廓变得乱七八糟。乱七八糟的方式和程度由参数决定。

1
2
3
4
PathEffect pathEffect = new DiscretePathEffect(20, 5);
paint.setPathEffect(pathEffect);
...
canvas.drawPath(path, paint);

图片丢失

DiscretePathEffect 具体的做法是,把绘制改为使用定长的线段来拼接,并且在拼接的时候对路径进行随机偏离。它的构造方法 DiscretePathEffect(float segmentLength, float deviation) 的两个参数中, segmentLength 是用来拼接的每个线段的长度, deviation 是偏离量。这两个值设置得不一样,显示效果也会不一样,具体的你自己多试几次就明白了

DashPathEffect

使用虚线来绘制线条。

1
2
3
4
PathEffect pathEffect = new DiscretePathEffect(20, 5);
paint.setPathEffect(pathEffect);
...
canvas.drawPath(path, paint);

图片丢失

它的构造方法 DashPathEffect(float[] intervals, float phase) 中, 第一个参数 intervals 是一个数组,它指定了虚线的格式:数组中元素必须为偶数(最少是 2 个),按照「画线长度、空白长度、画线长度、空白长度」……的顺序排列,例如上面代码中的 20, 5, 10, 5 就表示虚线是按照「画 20 像素、空 5 像素、画 10 像素、空 5 像素」的模式来绘制;第二个参数 phase 是虚线的偏移量。

PathDashPathEffect

这个方法比 DashPathEffect 多一个前缀 Path ,所以顾名思义,它是使用一个 Path 来绘制「虚线」。具体看图吧:

1
2
3
4
5
Path dashPath = ...; // 使用一个三角形来做 dash
PathEffect pathEffect = new PathDashPathEffect(dashPath, 40, 0, PathDashPathEffectStyle.TRANSLATE);
paint.setPathEffect(pathEffect);
...
canvas.drawPath(path, paint);

图片丢失

它的构造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中, shape 参数是用来绘制的 Path ; advance 是两个相邻的 shape 段之间的间隔,不过注意,这个间隔是两个 shape 段的起点的间隔,而不是前一个的终点和后一个的起点的距离; phase 和 DashPathEffect 中一样,是虚线的偏移;最后一个参数 style,是用来指定拐弯改变的时候 shape 的转换方式。style 的类型为 PathDashPathEffect.Style ,是一个 enum ,具体有三个值:

TRANSLATE:位移
ROTATE:旋转
MORPH:变体
图片丢失

SumPathEffect

这是一个组合效果类的 PathEffect 。它的行为特别简单,就是分别按照两种 PathEffect 分别对目标进行绘制。

1
2
3
4
5
PathEffect dashEffect = new DashPathEffect(new float[]{20, 10}, 0);
PathEffect discreteEffect = new DiscretePathEffect(20, 5);
pathEffect = new SumPathEffect(dashEffect, discreteEffect);
...
canvas.drawPath(path, paint);

图片丢失

ComposePathEffect

这也是一个组合效果类的 PathEffect 。不过它是先对目标 Path 使用一个 PathEffect,然后再对这个改变后的 Path 使用另一个 PathEffect。

1
2
3
4
5
PathEffect dashEffect = new DashPathEffect(new float[]{20, 10}, 0);
PathEffect discreteEffect = new DiscretePathEffect(20, 5);
pathEffect = new ComposePathEffect(dashEffect, discreteEffect);
...
canvas.drawPath(path, paint);

图片丢失

它的构造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 中的两个 PathEffect 参数, innerpe 是先应用的, outerpe 是后应用的。所以上面的代码就是「先偏离,再变虚线」。而如果把两个参数调换,就成了「先变虚线,再偏离」。至于具体的视觉效果……我就不贴图了,你自己试试看吧!

上面这些就是 Paint 中的 6 种 PathEffect。它们有的是有独立效果的,有的是用来组合不同的 PathEffect 的,功能各不一样。

注意: PathEffect 在有些情况下不支持硬件加速,需要关闭硬件加速才能正常使用:
Canvas.drawLine() 和 Canvas.drawLines() 方法画直线时,setPathEffect() 是不支持硬件加速的;
PathDashPathEffect 对硬件加速的支持也有问题,所以当使用 PathDashPathEffect 的时候,最好也把硬件加速关了。

剩下的两个效果类方法:setShadowLayer() 和 setMaskFilter() ,它们和前面的效果类方法有点不一样:它们设置的是「附加效果」,也就是基于在绘制内容的额外效果。

setShadowLayer(float radius, float dx, float dy, int shadowColor)

在之后的绘制内容下面加一层阴影。

1
2
3
paint.setShadowLayer(10, 0, 0, Color.RED);
...
canvas.drawText(text, 80, 300, paint);

图片丢失

效果就是上面这样。方法的参数里, radius 是阴影的模糊范围; dx dy 是阴影的偏移量; shadowColor 是阴影的颜色。
如果要清除阴影层,使用 clearShadowLayer() 。
注意:

  • 在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。

  • 如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。

setMaskFilter(MaskFilter maskfilter)

为之后的绘制设置 MaskFilter。上一个方法 setShadowLayer() 是设置的在绘制层下方的附加效果;而这个 MaskFilter 和它相反,设置的是在绘制层上方的附加效果。

到现在已经有两个 setXxxFilter(filter) 了。前面有一个 setColorFilter(filter) ,是对每个像素的颜色进行过滤;而这里的 setMaskFilter(filter) 则是基于整个画面来进行过滤。

  • MaskFilter 有两种: BlurMaskFilter 和 EmbossMaskFilter。

BlurMaskFilter

  • 模糊效果的 MaskFilter。
    1
    2
    3
    4
    5
    PathEffect dashEffect = new DashPathEffect(new float[]{20, 10}, 0);
    PathEffect discreteEffect = new DiscretePathEffect(20, 5);
    pathEffect = new ComposePathEffect(dashEffect, discreteEffect);
    ...
    canvas.drawPath(path, paint);

图片丢失
它的构造方法 BlurMaskFilter(float radius, BlurMaskFilter.Blur style) 中, radius 参数是模糊的范围, style 是模糊的类型。一共有四种:

  • NORMAL: 内外都模糊绘制
  • SOLID: 内部正常绘制,外部模糊
  • INNER: 内部模糊,外部不绘制
  • OUTER: 内部不绘制,外部模糊(什么鬼?)
    图片丢失

EmbossMaskFilter

  • 浮雕效果的 MaskFilter。
    1
    2
    3
    paint.setMaskFilter(new EmbossMaskFilter(new float[]{0, 1, 1}, 0.2f, 8, 10));
    ...
    canvas.drawBitmap(bitmap, 100, 100, paint);

图片丢失
它的构造方法 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius) 的参数里, direction 是一个 3 个元素的数组,指定了光源的方向; ambient 是环境光的强度,数值范围是 0 到 1; specular 是炫光的系数; blurRadius 是应用光线的范围。

不过由于我没有在项目中使用过 EmbossMaskFilter,对它的每个参数具体调节方式并不熟,你有兴趣的话自己研究一下吧。

获取绘制的 Path

这是效果类的最后一组方法,也是效果类唯一的一组 get 方法。
这组方法做的事是,根据 paint 的设置,计算出绘制 Path 或文字时的实际 Path。

getFillPath(Path src, Path dst)

所谓实际 Path ,指的就是 drawPath() 的绘制内容的轮廓,要算上线条宽度和设置的 PathEffect。
默认情况下(线条宽度为 0、没有 PathEffect),原 Path 和实际 Path 是一样的;而在线条宽度不为 0 (并且模式为 STROKE 模式或 FLL_AND_STROKE ),或者设置了 PathEffect 的时候,实际 Path 就和原 Path 不一样了:
图片丢失

通过 getFillPath(src, dst) 方法就能获取这个实际 Path。方法的参数里,src 是原 Path ,而 dst 就是实际 Path 的保存位置。 getFillPath(src, dst) 会计算出实际 Path,然后把结果保存在 dst 里。

getTextPath(String text, int start, int end, float x, float y, Path path) / getTextPath(char[] text, int index, int count, float x, float y, Path path)

「文字的 Path」。文字的绘制,虽然是使用 Canvas.drawText() 方法,但其实在下层,文字信息全是被转化成图形,对图形进行绘制的。 getTextPath() 方法,获取的就是目标文字所对应的 Path 。这个就是所谓「文字的 Path」。
图片丢失
这两个方法, getFillPath() 和 getTextPath() ,就是获取绘制的 Path 的方法。之所以把它们归类到「效果」类方法,是因为它们主要是用于图形和文字的装饰效果的位置计算,比如自定义的下划线效果
图片丢失

初始化类

这一类方法很简单,它们是用来初始化 Paint 对象,或者是批量设置 Paint 的多个属性的方法。

reset()

重置 Paint 的所有属性为默认值。相当于重新 new 一个,不过性能当然高一些啦。

set(Paint src)

把 src 的所有属性全部复制过来。相当于调用 src 所有的 get 方法,然后调用这个 Paint 的对应的 set 方法来设置它们。

setFlags(int flags)

批量设置 flags。相当于依次调用它们的 set 方法。

1
paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

这行代码,和下面这两行是等价的:

1
2
paint.setAntiAlias(true);
paint.setDither(true);

setFlags(flags) 对应的 get 方法是 int getFlags()。

Paint.setTextSize(float textSize)

通过 Paint.setTextSize(textSize),可以设置文字的大小。

1
2
3
4
5
6
7
8
9
String text = "你好,hello,拜拜,bye";paint.setTextSize(18);
paint.setTextSize(18);
canvas.drawText(text, 100, 25, paint);
paint.setTextSize(36);
canvas.drawText(text, 100, 70, paint);
paint.setTextSize(60);
canvas.drawText(text, 100, 145, paint);
paint.setTextSize(84);
canvas.drawText(text, 100, 240, paint);

设置字体

path

  • 这一类方法还可以细分为两组:添加子图形(addxxx)和画线(xxxTo)(直线或曲线)

addXXX

addCircle(float x, float y, float radius, Direction dir) 添加圆

x, y, radius 这三个参数是圆的基本信息,最后一个参数 dir 是画圆的路径的方向。

  • 路径方向有两种:顺时针 (CW clockwise) 和逆时针 (CCW counter-clockwise) 。对于普通情况,这个参数填 CW 还是填 CCW 没有影响。它只是在需要填充图形 (Paint.Style 为 FILL 或 FILL_AND_STROKE) ,并且图形出现自相交时,用于判断填充范围的setFillType会详细讲解
    1
    2
    3
    4
    5
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.FILL); // 填充模式
    Path path = new Path();
    path.addCircle(500,500,400,Path.Direction.CCW);
    canvas.drawPath(path,paint);//画一个圆

addCircle

  • 可以看出,path.AddCircle(x, y, radius, dir) + canvas.drawPath(path, paint) 这种写法,和直接使用 canvas.drawCircle(x, y, radius, paint) 的效果是一样的,区别只是它的写法更复杂。所以如果只画一个圆,没必要用 Path,直接用 drawCircle() 就行了。drawPath() 一般是在绘制组合图形时才会用到的。

其他的 Path.add-() 方法和这类似,例如:

addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, Direction dir) 添加椭圆

addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir) 添加矩形

addRoundRect(RectF rect, float rx, float ry, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) / addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加圆角矩形

addPath(Path path) 添加另一个 Path

上面这几个方法和 addCircle() 的使用都差不多,不再做过多介绍。

XXXTo

  • 这一组和第一组 addXxx() 方法的区别在于,第一组是添加的完整封闭图形(除了 addPath() ),而这一组添加的只是一条线。

lineTo(float x, float y) / rLineTo(float x, float y) 画直线

从当前位置向目标位置画一条直线, x 和 y 是目标位置的坐标。这两个方法的区别是,lineTo(x, y) 的参数是绝对坐标,而 rLineTo(x, y) 的参数是相对当前位置的相对坐标 (前缀 r 指的就是 relatively 「相对地」)。

当前位置:所谓当前位置,即最后一次调用画 Path 的方法的终点位置。初始值为原点 (0, 0)。

1
2
3
4
5
6
Paint paint = new Paint();
Path path = new Path();
paint.setStyle(Paint.Style.STROKE);
path.lineTo(100, 100); // 由当前位置 (0, 0) 向 (100, 100) 画一条直线
path.rLineTo(100, 0); // 由当前位置 (100, 100) 向正右方 100 像素的位置画一条直线
canvas.drawPath(path,paint);

lineTo

quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 画二次贝塞尔曲线

这条二次贝塞尔曲线的起点就是当前位置,而参数中的 x1, y1 和 x2, y2 则分别是控制点和终点的坐标。和 rLineTo(x, y) 同理,rQuadTo(dx1, dy1, dx2, dy2) 的参数也是相对坐标

贝塞尔曲线:贝塞尔曲线是几何上的一种曲线。它通过起点、控制点和终点来描述一条曲线,主要用于计算机图形学。概念总是说着容易听着难,总之使用它可以绘制很多圆润又好看的图形

  • 参数两个坐标,他是能在当前坐标圆润的过度到第一个坐标,然后圆润的过度到第二个坐标
1
2
path.quadTo(100,100,200,0);//从0,0过度到100,100再过度到200,0
canvas.drawPath(path,paint);

quadTo

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) / rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 画三次贝塞尔曲线

和上面这个 quadTo() rQuadTo() 的二次贝塞尔曲线同理,cubicTo() 和 rCubicTo() 是三次贝塞尔曲线

moveTo(float x, float y) / rMoveTo(float x, float y) 移动到目标位置

不论是直线还是贝塞尔曲线,都是以当前位置作为起点,而不能指定起点。但你可以通过 moveTo(x, y) 或 rMoveTo() 来改变当前位置,从而间接地设置这些方法的起点。

1
2
3
4
path.lineTo(100, 100); // 画斜线
path.moveTo(200, 100); // 我移~~
path.lineTo(200, 0); // 画竖线
canvas.drawPath(path,paint);

moveTo

  • moveTo(x, y) 虽然不添加图形,但它会设置图形的起点,所以它是非常重要的一个辅助方法。

另外,第二组还有两个特殊的方法: arcTo() 和 addArc()。它们也是用来画线的,但并不使用当前位置作为弧线的起点。

arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle) 画弧形

这个方法和 Canvas.drawArc() 比起来,少了一个参数 useCenter,而多了一个参数 forceMoveTo 。

少了 useCenter ,是因为 arcTo() 只用来画弧形而不画扇形,所以不再需要 useCenter 参数;而多出来的这个 forceMoveTo 参数的意思是,绘制是要「抬一下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹。

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, true); // 强制移动到弧形起点(无痕迹)

arcTotrue

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.arcTo(100, 100, 300, 300, -90, 90, false); // 直接连线连到弧形起点(有痕迹)

false

  • 此时画笔开始是开始的角度,画笔最终落点是结束的角度

addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) / addArc(RectF oval, float startAngle, float sweepAngle)

又是一个弧形的方法。一个叫 arcTo ,一个叫 addArc(),都是弧形,区别在哪里?其实很简单: addArc() 只是一个直接使用了 forceMoveTo = true 的简化版 arcTo() 。

1
2
3
paint.setStyle(Style.STROKE);
path.lineTo(100, 100);
path.addArc(100, 100, 300, 300, -90, 90);

arcTotrue

close

作用是把当前的子图形封闭,即由当前位置向当前子图形的起点绘制一条直线。

1
2
3
4
paint.setStyle(Paint.Style.STROKE);
path.arcTo(100, 100, 300, 300, 0, 180, false); // 强制移动到弧形起点(无痕迹)
path.arcTo(400, 400, 600, 600, 0, 180, false); // 强制移动到弧形起点(无痕迹)
canvas.drawPath(path,paint);

未封闭

1
2
3
4
5
paint.setStyle(Paint.Style.STROKE);
path.arcTo(100, 100, 300, 300, 0, 180, false); // 强制移动到弧形起点(无痕迹)
path.arcTo(400, 400, 600, 600, 0, 180, false); // 强制移动到弧形起点(无痕迹)
path.close();
canvas.drawPath(path, paint);

封闭

  • close() 和 lineTo(起点坐标) 是完全等价的。

子图形:官方文档里叫做 contour 。但由于在这个场景下我找不到这个词合适的中文翻译(直译的话叫做「轮廓」),所以我换了个便于中国人理解的词:「子图形」。前面说到,第一组方法是「添加子图形」,所谓「子图形」,指的就是一次不间断的连线。一个 Path 可以包含多个子图形。当使用第一组方法,即 addCircle() addRect() 等方法的时候,每一次方法调用都是新增了一个独立的子图形;而如果使用第二组方法,即 lineTo() arcTo() 等方法的时候,则是每一次断线(即每一次「抬笔」),都标志着一个子图形的结束,以及一个新的子图形的开始。
另外,不是所有的子图形都需要使用 close() 来封闭。当需要填充图形时(即 Paint.Style 为 FILL 或 FILL_AND_STROKE),Path 会自动封闭子图形。

Path 方法第二类:辅助的设置或计算

Path.setFillType(Path.FillType ft) 设置填充方式

前面在说 dir 参数的时候提到, Path.setFillType(fillType) 是用来设置图形自相交时的填充算法的:
fileType取值

方法中填入不同的 FillType 值,就会有不同的填充效果。FillType 的取值有四个:

  • EVEN_ODD
  • WINDING (默认值)
  • INVERSE_EVEN_ODD
  • INVERSE_WINDING
    其中后面的两个带有 INVERSE_ 前缀的,只是前两个的反色版本,所以只要把前两个,即 EVEN_ODD 和 WINDING,搞明白就可以了。

EVEN_ODD 和 WINDING 的原理有点复杂,直接讲出来的话信息量太大,所以我先给一个简单粗暴版的总结,你感受一下: WINDING 是「全填充」,而 EVEN_ODD 是「交叉填充」:
交叉填充和全填充
之所以叫「简单粗暴版」,是因为这些只是通常情形下的效果;而如果要准确了解它们在所有情况下的效果,就得先知道它们的原理,即它们的具体算法。

EVEN_ODD 和 WINDING 的原理

EVEN_ODD

即 even-odd rule (奇偶原则):对于平面中的任意一点,向任意方向射出一条射线,这条射线和图形相交的次数(相交才算,相切不算哦)如果是奇数,则这个点被认为在图形内部,是要被涂色的区域;如果是偶数,则这个点被认为在图形外部,是不被涂色的区域。还以左右相交的双圆为例:
双圆

  • 射线的方向无所谓,同一个点射向任何方向的射线
  • 从上图可以看出,射线每穿过图形中的一条线,内外状态就发生一次切换,这就是为什么 EVEN_ODD 是一个「交叉填充」的模式。
WINDING

即 non-zero winding rule (非零环绕数原则):首先,它需要你图形中的所有线条都是有绘制方向的:
winding

然后,同样是从平面中的点向任意方向射出一条射线,但计算规则不一样:以 0 为初始值,对于射线和图形的所有交点,遇到每个顺时针的交点(图形从射线的左边向右穿过)把结果加 1,遇到每个逆时针的交点(图形从射线的右边向左穿过)把结果减 1,最终把所有的交点都算上,得到的结果如果不是 0,则认为这个点在图形内部,是要被涂色的区域;如果是 0,则认为这个点在图形外部,是不被涂色的区域。
winding1
和 EVEN_ODD 相同,射线的方向并不影响结果。

所以,我前面的那个「简单粗暴」的总结,对于 WINDING 来说并不完全正确:如果你所有的图形都用相同的方向来绘制,那么 WINDING 确实是一个「全填充」的规则;但如果使用不同的方向来绘制图形,结果就不一样了。

图形的方向:对于添加子图形类方法(如 Path.addCircle() Path.addRect())的方向,由方法的 dir 参数来控制,这个在前面已经讲过了;而对于画线类的方法(如 Path.lineTo() Path.arcTo())就更简单了,线的方向就是图形的方向。

所以,完整版的 EVEN_ODD 和 WINDING 的效果应该是这样的:
完整版
而 INVERSE_EVEN_ODD 和 INVERSE_WINDING ,只是把这两种效果进行反转而已,你懂了 EVEN_ODD 和 WINDING ,自然也就懂 INVERSE_EVEN_ODD 和 INVERSE_WINDING 了。

说在最后

安卓自定义内容太多了。。网上找了很多资源,就不学习了,等到需要的时候再去看好了