Paint
自定义 View: Paint
Paint 基础
Paint 即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色、样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,
大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。
Paint 的内部类
Paint.Cap Cap
指定了描边线和路径 (Path) 的开始和结束显示效果(线帽、笔触风格)
笔触风格 ,比如:ROUND,表示是圆角的笔触。那么什么叫笔触呢,其实很简单,就像我们现实世界中的笔,如果你用圆珠笔在纸上戳一点,那么这个点一定是个圆,即便很小,它代表了笔的笔触形状,如果我们把一支铅笔笔尖削成方形的,那么画出来的线条会是一条弯曲的 “ 矩形 “,这就是笔触的意思。
- Paint.Cap.BUTT 无线帽,也是默认类型。
- Paint.Cap.SQUARE 以线条宽度为大小,在开头和结尾分别添加半个正方形。
- Paint.Cap.ROUND 以线条宽度为直径,在开头和结尾分别添加一个半圆。
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
29
30
31
32
// 画笔初始设置
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);
- 画笔默认是无线帽的,即 BUTT。
- Cap 也会影响到点的绘制,在 Round 的状态下绘制的点是圆的。
- 在绘制线条时,线帽时在线段外的,如上图红色部分所显示的内容就是线帽。
- 上图中红色的线帽是用特殊方式展示出来的,直接绘制的情况下,线帽颜色和线段颜色相同。
Paint.Join Join
指定线条和曲线段在描边路径上连接的处理。 线段连接方式 (拐角类型)
画笔的连接方式 (Paint.Join) 是指两条连接起来的线段拐角显示方式。
1
2
// 通过下面方式设置连接类型
paint.setStrokeJoin(Paint.Join.ROUND);
- BEVEL 尖角 (默认模式)
- MITER 平角
- ROUND 圆角
斜接模式长度限制
Android 中线段连接方式默认是 MITER,即在拐角处延长外边缘,直到相交位置。
根据数学原理我们可知,如果夹角足够小,接近于零,那么交点位置就会在延长线上无限远的位置。 为了避免这种情况:如果连接模式为 MITER(尖角),当连接角度小于一定程度时会自动将连接模式转换为 BEVEL(平角)。
那么多大的角度算是比较小呢?根据资料显示,这个角度大约是 28.96°
,即 MITER(尖角) 模式下小于该角度的线段连接方式会自动转换为 BEVEL(平角) 模式。
我们可以通过下面的方法来更改默认限制:
1
2
// 设置 Miter Limit,参数并不是角度
paint.setStrokeMiter(10);
参数 miter 就是对长度的限制,它可以通过这个公式计算:miter = 1 / sin ( angle / 2 ) , angel 是两条线的形成的夹角。
其中 miter 的数值应该 >= 0,小于 0 的数值无效,其默认数值是 4,下表是 miter 和角度的一些对应关系。
关于这部分内容可以在 SkPaint_Reference
查看到。
Paint.Style 画笔模式
Style 指定绘制的图元是否被填充,描边或两者均有 (以相同的颜色)
这里的画笔模式 (Paint.Style) 就是指绘制一个图形时,是绘制边缘轮廓,绘制内容区域还是两者都绘制,它有三种模式。
- FILL 填充内容,也是画笔的默认模式。
- STROKE 描边,只绘制图形轮廓。
- 3.FILL_OR_STROKE 描边 + 填充,同时绘制轮廓和填充内容;和 FILL 的区别是,FILL_OR_STROKE 填充会大一点,多了 strokeWidth/2 的宽度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 画笔初始设置
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);
Paint 类的几个最常用的方法
画笔基本设置 flag
1. setAntiAlias
void setAntiAlias(boolean aa) 设置抗锯齿开关
也可以设置 FLAG ANTI_ALIAS_FLAG
开启抗锯齿功能的标记。
2. getFlags
int getFlags() 获取画笔相关的一些设置 (标志)
3. setFlags
void setFlags(int flags) 设置画笔的标志位。并不建议使用 setFlags 方法,这是因为 setFlags 方法会覆盖之前设置的内容;如果想要调整 flag 个人建议还是使用 paint 提供的一些封装方法,如:setDither(true),而不要自己手动去直接操作 flag
4. set(Paint src)
void set(Paint src) 复制 src 的画笔设置。使用 set(Paint src) 可以复制一个画笔,但需要注意的是,如果调用了这个方法,当前画笔的所有设置都会被覆盖掉,而替换为所选画笔的设置
5. reset()
void reset() 将画笔恢复为默认设置
画笔颜色 setColor/setShader
除了基本颜色的设置( setColor/ARGB(), setShader() )以及基于原始颜色的过滤( setColorFilter() )之外,Paint 最后一层处理颜色的方法是 setXfermode(Xfermode xfermode) ,它处理的是「当颜色遇上 View」的问题。
1. void setAlpha(int a) 设置透明度
2. int getAlpha() 只返回颜色的 alpha 值
3. int getColor() 返回画笔的颜色
4. void setColor(int color) 设置颜色
5. setARGB
void setARGB(int a, int r, int g, int b) 设置带透明通道的颜色
6. setColorFilter
void setColorFilter(ColorFilter colorfilter) 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
见 [[#ColorFilter]] 章节
7. setShader
void setShader(Shader shader) 设置图像效果,使用 Shader 可以绘制出各种渐变效果
Shader
这个英文单词很多人没有见过,它的中文叫做「着色器」,也是用于设置绘制颜色的。「着色器」不是 Android 独有的,它是图形领域里一个通用的概念,它和直接设置颜色的区别是,着色器设置的是一个颜色方案,或者说是一套着色规则。当设置了 Shader 之后,Paint 在绘制图形和文字时就不使用 setColor/ARGB() 设置的颜色了,而是使用 Shader 的方案中的颜色。
见 [[#7. setShader]] 章节
8. setXfermode
void setXfermode(Xfermode xfermode) 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制
作橡皮的擦除效果
见 [[PorterDuff]]
画笔宽度 StrokeWidth
画笔宽度,就是画笔的粗细
void setStyle(Style style) 设置绘制模式 Style
Paint.Style getStyle() 返回 paint 的样式,用于控制如何解释几何元素(除了 drawBitmap,它总是假定为 FILL_STYLE)
void setStrokeWidth(float width) 当画笔样式为 STROKE 或 FILL_OR_STROKE 时,设置笔刷的粗细度
float getStrokeWidth() 返回描边的宽度
1
2
3
4
// 将画笔设置为描边
paint.setStyle(Paint.Style.STROKE);
// 设置线条宽度
paint.setStrokeWidth(120);
注意: 这条线的宽度是同时向两边进行扩展的,例如绘制一个圆时,将其宽度设置为 120 则会向外扩展 60 ,向内缩进 60,如下图所示:
因此如果绘制的内容比较靠近视图边缘,使用了比较粗的描边的情况下,一定要注意和边缘保持一定距离 (边距>StrokeWidth/2) 以保证内容不会被剪裁掉。
如下面这种情况,直接绘制一个矩形,如果不考虑画笔宽度,则绘制的内容就可能不正常。
在一个 1000x1000 大小的画布上绘制与个大小为 500x500 ,宽度为 100 的矩形。灰色部分为画布大小。红色为分割线,将画笔分为均等的四份。蓝色为矩形。
1
2
3
4
paint.setStrokeWidth(100);
paint.setColor(0xFF7FC2D8);
Rect rect = new Rect(0, 0, 500, 500);
canvas.drawRect(rect, paint);
如果考虑到画笔宽度,需要绘制一个大小刚好填充满左上角区域的矩形,那么实际绘制的矩形就要小一些,(如果只是绘制一个矩形的话,可以将矩形向内缩小画笔宽度的一半) 这样绘制出来就是符合预期的。
1
2
3
4
5
paint.setStrokeWidth(100);
paint.setColor(0xFF7FC2D8);
Rect rect = new Rect(0, 0, 500, 500);
rect.inset(50, 50); // 注意这里,向内缩小半个宽度,或者rect减/加strokeWidth/2
canvas.drawRect(rect, paint)
这里只是用矩形作为例子说明,事实上,绘制任何图形,只要有描边的,就要考虑描边宽度占用的空间,需要适当的缩小图形,以保证其可以完整的显示出来。
注意:在实际的自定义 View 中也不要忽略 padding 占用的空间哦。
hairline mode (发际线模式)
在画笔宽度为 0 的情况下,使用 drawLine 或者使用描边模式 (STROKE) 也可以绘制出内容。只是绘制出的内容始终是 1 像素,不受画布缩放的影响。该模式被称为 hairline mode
(发际线模式)。
如果你设置了画笔宽度为 1 像素,那么如果画布放大到 2 倍,1 像素会变成 2 像素。但如果是 0 像素,那么不论画布如何缩放,绘制出来的宽度依旧为 1 像素。
绘制文本相关
setTextSize 文字大小
setTextSize(float textSize) 设置文字大小
setDither/setFilterBitmap 色彩优化
Paint 的色彩优化有两个方法: setDither(boolean dither)
和 setFilterBitmap(boolean filter)
。它们的作用都是让画面颜色变得更加「顺眼」,但原理和使用场景是不同的。
对应的 FLAG:
- DITHER_FLAG 在绘制时启用抖动的标志。
- FILTER_BITMAP_FLAG 绘制标志,在缩放的位图上启用双线性采样。
1. setDither
void setDither(boolean dither) 设置抖动来优化色彩深度降低时的绘制效果
抖动的原理和这个类似。所谓抖动(注意,它就叫抖动,不是防抖动,也不是去抖动,有些人在翻译的时候自作主张地加了一个「防」字或者「去」字,这是不对的),是指把图像从较高色彩深度(即可用的颜色数)向较低色彩深度的区域绘制时,在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法。
抖动可不只可以用在纯色的绘制。在实际的应用场景中,抖动更多的作用是在图像降低色彩深度绘制时,避免出现大片的色带与色块
setDither(dither) 已经没有当年那么实用了,因为现在的 Android 版本的绘制,默认的色彩深度已经是 32 位的 ARGB_8888 ,效果已经足够清晰了。只有当你向自建的 Bitmap 中绘制,并且选择 16 位色的 ARGB_4444 或者 RGB_565 的时候,开启它才会有比较明显的效果。
2. setFilterBitmap
setFilterBitmap(boolean filter) 设置是否使用双线性过滤来绘制 Bitmap (设置双线性过滤来优化 Bitmap 放大绘制的效果)
图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。
阴影 setShadowLayer
- 构造方法
setShadowLayer(float radius, float dx, float dy, int shadowColor) - 参数
radius 是阴影的模糊范围; dx dy 是阴影的 x 和 y 轴的偏移量; shadowColor 是阴影的颜色。
如果要清除阴影层,使用 clearShadowLayer()
。
1
2
3
paint.setShadowLayer(10, 0, 0, Color.RED);
// ...
canvas.drawText(text, 80, 300, paint);
- 注意:
- 在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。
- 如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。
MaskFilter
- void setMaskFilter(MaskFilter maskfilter) 设置 MaskFilter,可以用不同的 MaskFilter 实现滤镜的效果,如滤化,立体等
见 [[#Paint 之 MaskFilter]] 章节
PathEffect setPathEffect
- PathEffect getPathEffect() 获取画笔的 PathEffect 对象。
- void setPathEffect(PathEffect effect) 设置 Path 效果。
见 [[#Paint 之 PathEffect]] 章节
线条连接
Paint.Cap getStrokeCap()
返回 paint 的 Cap,控制如何处理描边线和路径的开始和结束。void setStrokeCap(Paint.Cap cap)
当画笔样式为 STROKE 或 FILL_OR_STROKE 时,设置笔刷的图形样式,如圆形样式 Cap.ROUND,或方形样式 Cap.SQUAREPaint.Join getStrokeJoin()
返回画笔的笔触连接类型。void setStrokeJoin(Paint.Join join)
设置绘制时各图形的结合方式,如平滑效果等
这个方法用于设置接合处的形态,就像你用代码画了一条线,但是这条线其实是由无数条小线拼接成的,拼接处的形状就由该方法指定。可选参数是:BEVEL,MITER,ROUND。float getStrokeMiter()
返回画笔的笔触斜接值。用于在连接角度锐利时控制斜接连接的行为。void setStrokeMiter(float miter)
这个方法是对于 setStrokeJoin() 的一个补充,它用于设置 MITER 型拐角的延长线的最大值。所谓「延长线的最大值」
获取绘制的 Path
这组方法做的事是,根据 paint 的设置,计算出绘制 Path 或文字时的实际 Path。
所谓实际 Path ,指的就是 drawPath() 的绘制内容的轮廓,要算上
线条宽度
和设置的PathEffect
。
1. getFillPath
boolean getFillPath(Path src, Path dst) 获取这个实际 Path。方法的参数里,src 是原 Path ,而 dst 就是实际 Path 的保存位置。 getFillPath(src, dst) 会计算出实际 Path,然后把结果保存在 dst 里
默认情况下(线条宽度为 0、没有 PathEffect),原 Path 和实际 Path 是一样的;而在线条宽度不为 0 (并且模式为 STROKE 模式或 FLL_AND_STROKE ),或者设置了 PathEffect 的时候,实际 Path 就和原 Path 不一样了:
2. getTextPath
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」。
它们主要是用于图形和文字的装饰效果的位置计算,比如自定义的下划线效果
初始化
1. reset()
重置 Paint 的所有属性为默认值。相当于重新 new 一个,不过性能当然高一些啦。
2. set(Paint src)
把 src 的所有属性全部复制过来。相当于调用 src 所有的 get 方法,然后调用这个 Paint 的对应的 set 方法来设置它们。
3.setFlags(int flags)
批量设置 flags。相当于依次调用它们的 set 方法
1
2
3
4
paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
// 等价
paint.setAntiAlias(true);
paint.setDither(true);
其他设置
- void setHinting(int mode) 设置画笔的隐藏模式。可以是
HINTING_OFF
orHINTING_ON
之一。 - void setSubpixelText (boolean subpixelText ) 设置自像素。如果该项为 true,将有助于文本在 LCD 屏幕上的显示效果。
Paint 之 MaskFilter
为之后的绘制设置 MaskFilter 。setShadowLayer() 是设置的在绘制层下方的附加效果;而这个 MaskFilter 和它相反,设置的是在绘制层上方的附加效果。
到现在已经有两个 setXxxFilter(filter) 了。setColorFilter(filter) 是对每个像素的颜色进行过滤;而这里的 setMaskFilter(filter) 则是基于整个画面来进行过滤。
MaskFilter 有两种: BlurMaskFilter
和 EmbossMaskFilter
。
BlurMashFilter
- 构造方法
1
BlurMaskFilter(float radius, Blur style)
- 参数
- radius 参数是模糊的范围
- style 是模糊的类型。一共有四种
BlurMaskFilter.Blur
- NORMAL: 内外都模糊绘制
- SOLID: 内部正常绘制,外部模糊
- INNER: 内部模糊,外部不绘制
- OUTER: 内部不绘制,外部模糊
EmbossMaskFilter
浮雕效果的 MaskFilter。
- 构造函数
1
EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)
- 参数
1
2
3
4
irection 是一个 3 个元素的数组,指定了光源的方向;
ambient 是环境光的强度,数值范围是 0 到 1;
specular 是炫光的系数;
blurRadius 是应用光线的范围。
1
2
3
paint.setMaskFilter(new EmbossMaskFilter(new float[]{0, 1, 1}, 0.2f, 8, 10));
// ...
canvas.drawBitmap(bitmap, 100, 100, paint);
Paint 之 PathEffect
PathEffect 在绘制之前修改几何路径,它可以实现划线,自定义填充效果和自定义笔触效果。PathEffect 虽然名字看起来是和 Path 相关的,但实际上它的效果可以作用于 Canvas 的各种绘制,例如 drawLine
, drawRect
,drawPath
等。
绘制路径时,pathPaint.setStyle(Paint.Style.STROKE) 画笔的风格要空心,否则,后果画出的不是线,而是一个不规则的区域
使用 PathEffect 来给图形的轮廓设置效果。对 Canvas 所有的图形绘制有效,也就是 drawLine() drawCircle() drawPath() 这些方法
注意: PathEffect 在部分情况下不支持硬件加速,需要关闭硬件加速才能正常使用:
Canvas.drawLine() 和 Canvas.drawLines() 方法画直线时,setPathEffect() 是不支持硬件加速的;
PathDashPathEffect 对硬件加速的支持也有问题,所以当使用 PathDashPathEffect 的时候,最好也把硬件加速关了
在 Android 中有 6 种 PathEffect,4 种基础效果,2 种叠加效果。
PathEffect | 简介 |
---|---|
CornerPathEffect | 圆角效果,将尖角替换为圆角。(STROKE 和 FILL) |
DashPathEffect | 虚线效果,用于各种虚线效果。(STROKE 和 FILL_AND_STROKE) |
PathDashPathEffect | Path 虚线效果,虚线中的间隔使用 Path 代替。 |
DiscretePathEffect | 让路径分段随机偏移。 |
SumPathEffect | 两个 PathEffect 效果组合,同时绘制两种效果。 |
ComposePathEffect | 两个 PathEffect 效果叠加,先使用效果 1,之后使用效果 2。 |
通过 setPathEffect 来设置效果 paint.setPathEffect(effect);
CornerPathEffect
CornerPathEffect 把所有拐角变成圆角。(STROKE 和 FILL)
CornerPathEffect 可以将线段之间的任何锐角替换为指定半径的圆角 (适用于 STROKE 或 FILL 样式)。
1
2
// radius 为圆角半径大小,半径越大,path 越平滑。
CornerPathEffect(radius);
- 使用 CornerPathEffect,可以实现圆角矩形效果
1
2
3
4
5
6
7
8
9
10
11
12
RectF rect = new RectF(0, 0, 600, 600);
float corner = 300;
// 使用 CornerPathEffect 实现类圆角效果
canvas.translate((1080 - 600) / 2, (1920 / 2 - 600) / 2);
paint.setPathEffect(new CornerPathEffect(corner));
canvas.drawRect(rect, paint);
// 直接绘制圆角矩形
canvas.translate(0, 1920 / 2);
paint.setPathEffect(null);
canvas.drawRoundRect(rect, corner, corner, paint);
左侧是使用 CornerPathEffect 将矩形的边角变圆润的效果,右侧则是直接绘制圆角矩形的效果。我们知道,在绘制圆角矩形时,如果圆角足够大时,那么绘制出来就会是圆或者椭圆。但是使用 CornerPathEffect 时,不论圆角有多大,它也不会变成圆形或者椭圆。
- CornerPathEffect 也可以让手绘效果更加圆润
一些简单的绘图场景或者签名场景中,一般使用 Path 来保存用户的手指轨迹,通过连续的 lineTo 来记录用户手指划过的路径,但是直接的 LineTo 会让转角看起来非常生硬,而使用 CornerPathEffect 效果则可以快速的让轨迹圆润起来。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class CornerPathEffectTestView extends View {
Paint mPaint = new Paint();
PathEffect mPathEffect = new CornerPathEffect(200);
Path mPath = new Path();
public CornerPathEffectTestView(Context context) {
this(context, null);
}
public CornerPathEffectTestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint.setStrokeWidth(20);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mPath.reset();
mPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
}
postInvalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制原始路径
canvas.save();
mPaint.setColor(Color.BLACK);
mPaint.setPathEffect(null);
canvas.drawPath(mPath, mPaint);
canvas.restore();
// 绘制带有效果的路径
canvas.save();
canvas.translate(0, canvas.getHeight() / 2);
mPaint.setColor(Color.RED);
mPaint.setPathEffect(mPathEffect);
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
}
DashPathEffect 虚线(STROKE 和 FILL_AND_STROKE)
DashPathEffect 用于实现虚线效果 (适用于 STROKE 或 FILL_AND_STROKE 样式)
1
public DashPathEffect(float[] intervals, float phase)
- intervals 是一个 float 数组,且其长度必须是偶数且>=2,控制实线和实线之后空白线的宽度
- phase 参数指定了绘制的虚线相对了起始地址(Path 起点)的取余偏移(对路径总长度)。将 View 向 “ 左 “ 偏移 phase
1
2
3
4
5
new DashPathEffect(new float[] { 8, 10, 8, 10}, 0); // 指定了绘制8px的实线,再绘制10px的透明,再绘制8px的实线,再绘制10px的透明,依次重复来绘制达到path对象的长度。
new DashPathEffect(new float[] { 8, 10, 8, 10}, 0); // 这时偏移为0,先绘制实线,再绘制透明。
new DashPathEffect(new float[] { 8, 10, 8, 10}, 8); // 这时偏移为8,先绘制了透明,再绘制了实线.(实线被偏移过去了)
1
2
3
4
DashPathEffect dashPathEffect1 = new DashPathEffect(new float[]{60, 60}, 0);
DashPathEffect dashPathEffect2 = new DashPathEffect(new float[]{60, 60}, 20);
DashPathEffect dashPathEffect3 = new DashPathEffect(new float[]{60, 60}, 40);
DashPathEffect dashPathEffect4 = new DashPathEffect(new float[]{60, 60}, 60);
PathDashPathEffect (STROKE 或 FILL_AND_STROKE 样式))
这个也是实现类似虚线效果,只不过这个虚线中显示的部分可以指定为一个 Path(适用于 STROKE 或 FILL_AND_STROKE 样式)。
1
2
3
4
5
// shape: Path 图形,参数中的 shape 只能是 FILL 模式,即便画笔是 STROKE 样式,shape 也只会是 FILL。
// advance: 图形占据长度
// phase: 相位差
// style: 转角样式
PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style);
示例:
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 画笔初始设置
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
RectF rectF = new RectF(0, 0, 50, 50);
// 方形
Path rectPath = new Path();
rectPath.addRect(rectF, Path.Direction.CW);
// 圆形 椭圆
Path ovalPath = new Path();
ovalPath.addOval(rectF, Path.Direction.CW);
// 子弹形状
Path bulletPath = new Path();
bulletPath.lineTo(rectF.centerX(), rectF.top);
bulletPath.addArc(rectF, -90, 180);
bulletPath.lineTo(rectF.left, rectF.bottom);
bulletPath.lineTo(rectF.left, rectF.top);
// 星星形状
PathMeasure pathMeasure = new PathMeasure(ovalPath, false);
float length = pathMeasure.getLength();
float split = length / 5;
float[] starPos = new float[10];
float[] pos = new float[2];
float[] tan = new float[2];
for (int i = 0; i < 5; i++) {
pathMeasure.getPosTan(split * i, pos, tan);
starPos[i * 2 + 0] = pos[0];
starPos[i * 2 + 1] = pos[1];
}
Path starPath = new Path();
starPath.moveTo(starPos[0], starPos[1]);
starPath.lineTo(starPos[4], starPos[5]);
starPath.lineTo(starPos[8], starPos[9]);
starPath.lineTo(starPos[2], starPos[3]);
starPath.lineTo(starPos[6], starPos[7]);
starPath.lineTo(starPos[0], starPos[1]);
Matrix matrix = new Matrix();
matrix.postRotate(-90, rectF.centerX(), rectF.centerY());
starPath.transform(matrix);
canvas.translate(360, 100);
// 绘制分割线 - 方形
canvas.translate(0, 100);
paint.setPathEffect(new PathDashPathEffect(rectPath, rectF.width() * 1.5f, 0, PathDashPathEffect.Style.TRANSLATE));
canvas.drawLine(0, 0, 1200, 0, paint);
// 绘制分割线 - 圆形
canvas.translate(0, 100);
paint.setPathEffect(new PathDashPathEffect(ovalPath, rectF.width() * 1.5f, 0, PathDashPathEffect.Style.TRANSLATE));
canvas.drawLine(0, 0, 1200, 0, paint);
// 绘制分割线 - 子弹型
canvas.translate(0, 100);
paint.setPathEffect(new PathDashPathEffect(bulletPath, rectF.width() * 1.5f, 0, PathDashPathEffect.Style.TRANSLATE));
canvas.drawLine(0, 0, 1200, 0, paint);
// 绘制分割线 - 星型
canvas.translate(0, 100);
paint.setPathEffect(new PathDashPathEffect(starPath, rectF.width() * 1.5f, 0, PathDashPathEffect.Style.TRANSLATE));
canvas.drawLine(0, 0, 1200, 0, paint);
- PathDashPathEffect.Style,这个参数用于处理 Path 图形在转角处的样式。
- TRANSLATE 在转角处对图形平移
- ROTATE 在转角处对图形旋转。
- MORPH 在转角处对图形变形。
DiscretePathEffect 把线条进行随机的偏离,让轮廓变得乱七八糟。乱七八糟的方式和程度由参数决定
1
2
3
// segmentLength: 分段长度
// deviation: 偏移距离
DiscretePathEffect(float segmentLength, float deviation);
DiscretePathEffect 具体的做法是,把绘制改为使用定长的线段来拼接,并且在拼接的时候对路径进行随机偏离。它的构造方法 DiscretePathEffect(float segmentLength, float deviation) 的两个参数中, segmentLength 是用来拼接的每个线段的长度, deviation 是偏离量。这两个值设置得不一样,显示效果也会不一样
SumPathEffect
SumPathEffect 用于合并两种效果,它相当于两种效果都绘制一遍。
1
2
// 两种效果相加
SumPathEffect(PathEffect first, PathEffect second);
ComposePathEffect
ComposePathEffect 也是合并两种效果,只不过先应用一种效果后,再次叠加另一种效果,因此交换参数最终得到的效果是不同的。
1
2
// 构造一个 PathEffect, 其效果是首先应用 innerpe 再应用 outerpe (如: outer(inner(path)))。
ComposePathEffect(PathEffect outerpe, PathEffect innerpe);
Paint 之 ColorFilter
ColorFilter
这个类,它的名字已经足够解释它的作用:为绘制设置颜色过滤。颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX()
方法会对每个像素都进行过滤后再绘制出来。
在 Paint
里设置 ColorFilter
,使用的是 Paint.setColorFilter(ColorFilter filter)
方法。 ColorFilter
并不直接使用,而是使用它的子类。它共有三个子类:LightingColorFilter
PorterDuffColorFilter
和 ColorMatrixColorFilter
。
ColorMatrix
在 Android 中图片是以 RGBA 像素点的形式加载到内存中的,修改这些像素信息需要一个叫做 ColorMatrix
类的支持,其定义了一个 4x5
的 float[]
类型的矩阵:
1
2
3
4
5
6
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
});
其中,第一行表示的 R(红色)的向量,第二行表示的 G(绿色)的向量,第三行表示的 B(蓝色)的向量,最后一行表示 A(透明度)的向量,这一顺序必须要正确不能混淆
这个矩阵不同的位置表示的 RGBA 值,其范围在 0.0F 至 2.0F 之间,1 为保持原图的 RGB 值。
每一行的第五列数字表示偏移值,何为偏移值?顾名思义当我们想让颜色更倾向于红色的时候就增大 R 向量中的偏移值,想让颜色更倾向于蓝色的时候就增大 B 向量中的偏移值,这是最最朴素的理解,但是事实上色彩偏移的概念是基于白平衡来理解的,什么是白平衡呢?说得简单点就是白色是什么颜色!如果大家是个单反爱好者或者会些 PS 就会很容易理解这个概念,在单反的设置参数中有个色彩偏移,其定义的就是白平衡的色彩偏移值,就是当你去拍一张照片的时候白色是什么颜色的,在正常情况下白色是(255, 255, 255, 255)但是现实世界中我们是无法找到这样的纯白物体的,所以在我们用单反拍照之前就会拿一个我们认为是白色的物体让相机记录这个物体的颜色作为白色,然后拍摄时整张照片的颜色都会依据这个定义的白色来偏移!而这个我们定义的 “ 白色 “(比如:255, 253, 251, 247)和纯白(255, 255, 255, 255)之间的偏移值(0, 2, 4, 8)我们称之为白平衡的色彩偏移。
矩阵 ColorMatrix 的一行乘以矩阵 MyColor 的一列作为矩阵 Result 的一行,这里 MyColor 的 RGBA 值我们需要转换为 [0, 1]
。那么我们依据此公式来计算下我们得到的 RGBA 值是否跟我们计算得出来的圆的 RGBA 值一样:
系统自带的 ColorFilter
在 Paint 里设置 ColorFilter ,使用的是 Paint.setColorFilter(ColorFilter filter)
方法。 ColorFilter 并不直接使用,而是使用它的子类。它共有 4 个子类:LightingColorFilter
PorterDuffColorFilter
ColorMatrixColorFilter
和 BlendModeColorFilter
。
LightingColorFilter
LightingColorFilter
是用来模拟简单的光照效果的。
构造方法:
1
LightingColorFilter(@ColorInt int mul, @ColorInt 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
如果你想去掉原像素中的红色,可以把它的 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
ColorFilter lightingColorFilter = new LightingColorFilter(0x00ffff, 0x000000);
paint.setColorFilter(lightingColorFilter);
你想让它的绿色更亮一些,就可以把它的 add 改为 0x003000 (绿色部分为 0x30 ),那么它的计算过程就是:
1
2
3
R' = R * 0xff / 0xff + 0x0 = R
G' = G * 0xff / 0xff + 0x30 = G + 0x30 // 绿色被加强
B' = B * 0xff / 0xff + 0x0 = B
效果是这样:
1
2
ColorFilter lightingColorFilter = new LightingColorFilter(0xffffff, 0x003000);
paint.setColorFilter(lightingColorFilter);
PorterDuffColorFilter
PorterDuffColorFilter
的作用是使用一个指定的颜色和一种指定的 PorterDuff.Mode
来与绘制对象进行合成。有一个 mode 为 SRC_ATOP
的子类 SimpleColorFilter
。
构造函数
1
PorterDuffColorFilter(@ColorInt int color, PorterDuff.Mode mode)
color 参数是指定的颜色, mode 参数是指定的 Mode
ColorMatrixColorFilter
ColorMatrixColorFilter 使用一个 ColorMatrix 来对颜色进行处理。 ColorMatrix 这个类,内部是一个 4x5 的矩阵:
1
2
3
4
[ 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] ,转换算法是这样的:
1
2
3
4
R’ = a*R + b*G + c*B + d*A + e;
G’ = f*R + g*G + h*B + i*A + j;
B’ = k*R + l*G + m*B + n*A + o;
A’ = p*R + q*G + r*B + s*A + t;
ColorMatrix
有一些自带的方法可以做简单的转换,例如可以使用 setSaturation(float sat) 来设置饱和度;
BlendModeColorFilter
Paint 之 Shader
在 Android 的绘制里使用 Shader ,并不直接用 Shader 这个类,而是用它的几个子类。具体来讲有 LinearGradient
RadialGradient
SweepGradient
BitmapShader
ComposeShader
这么几个
Tile.Mode
TileMode 边缘填充模式
如果 shader 的大小小于 view 的大小时如何绘制其他没有被 shader 覆盖的区域?用 TileMode
CLAMP 边缘拉伸,利用边缘的颜色,填充剩余部分
REPEAT 在水平和垂直两个方向上重复,相邻图像没有间隙,重复 shader
MIRROR 以镜像的方式在水平和垂直两个方向上重复,相邻图像有间隙,镜面 shader
LinearGradient 线性渐变
- LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile) 指定两个颜色之间的渐变
- LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile) 指定多个颜色之间的渐变
设置了 Shader 之后,绘制出了渐变颜色的圆。(其他形状以及文字都可以这样设置颜色。)
注意:在设置了 Shader 的情况下, Paint.setColor/ARGB() 所设置的颜色就不再起作用。
LinearGradient 参数
1
2
3
4
5
6
7
8
(x0,y0): 渐变起始点坐标
(x1,y1): 渐变结束点坐标
color0: 渐变开始点颜色,16进制的颜色表示,必须要带有透明度
color1: 渐变结束颜色
colors: 渐变数组
positions: 位置数组,position的取值范围[0,1], 作用是指定某个位置的颜色值,如果传null,渐变就线性变化。
tile: TileMode 边缘填充模式。用于指定控件区域大于指定的渐变区域时,空白区域的颜色填充方法。
CLAMP:
REPEAT:
MIRROR:
线性渐变方向
- 要实现
从上到下
需要设置 shader 开始结束点坐标为左上角到左下角或右上角到右下角坐标 - 要实现
从下到上
需要设置 shader 开始结束点坐标为左下角到左上角或右下角到右上角 - 要实现
从左到右
需要设置 shader 开始结束点坐标为左上角到右上角或者左下角到右下角 - 要实现
从右到左
需要设置 shader 开始结束坐标为右上角到左上角或者右下角到左下角 - 要实现对角 shader,需要设置开始结束点坐标
左上角到右下角
多颜色填充 colors,positions 数组参数讲解
positions 为 null 时,线性填充,和没有 positions 数组的构造函数效果一样。positions 数组中值为 0-1,0 表示开始绘制点,1 表示结束点,0.5 对应中间点等等。数组中位置信息对应颜色数组中的颜色。
1
2
3
int [] colors = {Color.RED,Color.GREEN, Color.BLUE};
float[] position = {0f, 0.3f, 1.0f};
// 上面position[0]对应数组中的第一个RED,0.3f的位置对应颜色中的GREEN,1.0f的位置对应颜色中的BLUE,所以从0-0.3的位置是从RED到GREEN的渐变,从0.3到1.0的位置的颜色渐变是GREEN到BLUE。
1
2
3
4
5
6
int [] colors = {Color.RED,Color.GREEN, Color.BLUE};
float[] position = {0f, 0.3f, 1.0f};
LinearGradient linearGradient = new LinearGradient(0,0,getMeasuredWidth(),0,colors,position, Shader.TileMode.CLAMP);
mPaint.setShader(linearGradient);
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
如果把 0.3 改成 0.7:
案例
1
2
3
4
Shader shader = new LinearGradient(100, 100, 500, 500, Color.parseColor("#E91E63"),
Color.parseColor("#2196F3"), Shader.TileMode.CLAMP);
paint.setShader(shader);
canvas.drawCircle(300, 300, 200, paint);
RadialGradient 辐射渐变
辐射渐变很好理解,就是从中心向周围辐射状的渐变。大概像这样:
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)
参数
- centerX centerY:辐射中心的坐标
- radius:辐射半径
- centerColor:辐射中心的颜色
- edgeColor:辐射边缘的颜色
- tileMode:辐射范围之外的着色模式
1
2
3
4
5
Shader shader = new RadialGradient(300, 300, 200, Color.parseColor("#E91E63"),
Color.parseColor("#2196F3"), Shader.TileMode.CLAMP);
paint.setShader(shader);
// ...
canvas.drawCircle(300, 300, 200, paint);
SweepGradient 扫描渐变
又是一个渐变。「扫描渐变」
SweepGradient(float cx, float cy, int color0, int color1)
- cx cy :扫描的中心
- color0:扫描的起始颜色
- color1:扫描的终止颜色
案例:
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
private fun drawSweepGradient(canvas: Canvas) {
var shader = SweepGradient(150F, 150F, Color.RED, Color.BLUE)
paint.shader = shader
canvas.drawCircle(150F, 150F, 100F, paint)
canvas.drawDot(150F, 150F)
canvas.save()
canvas.translate(350F, 0F)
paint.style = Paint.Style.FILL
canvas.drawCircle(150F, 150F, 100F, paint)
canvas.drawDot(150F, 150F)
canvas.restore()
canvas.save()
canvas.translate(0F, 350F)
shader = SweepGradient(150F, 150F, Color.RED, Color.BLUE)
paint.shader = shader
canvas.drawCircle(200F, 200F, 200F, paint)
canvas.drawDot(200F, 200F)
canvas.translate(600F, 0F)
shader = SweepGradient(150F, 150F, Color.RED, Color.BLUE)
paint.shader = shader
canvas.drawCircle(150F, 150F, 200F, paint)
canvas.drawDot(150F, 150F)
canvas.restore()
}
BitmapShader Bitmap 着色器
用 Bitmap
来着色(终于不是渐变了)。其实也就是用 Bitmap
的像素来作为图形或文字的填充。大概像这样:
1
2
3
4
5
6
7
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);
看着跟
Canvas.drawBitmap()
好像啊?事实上也是一样的效果。如果你想绘制圆形的Bitmap
,就别用drawBitmap()
了,改用drawCircle()
+BitmapShader
就可以了(其他形状同理)。
构造方法:
1
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
参数:
- bitmap:用来做模板的 Bitmap 对象
- tileX:横向的 TileMode
- tileY:纵向的 TileMode。
CLAMP:
MIRROR:
REPEAT:
ComposeShader 混合着色器
所谓混合,就是把两个 Shader
一起使用。
构造方法:
1
ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
参数:
- shaderA, shaderB:两个相继使用的 Shader
- mode: PorterDuff.Mode 两个 Shader 的叠加模式,即 shaderA 和 shaderB 应该怎样共同绘制。它的类型是 PorterDuff.Mode 。具体见 [[PorterDuff]] 章节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 第一个 Shader:头像的 Bitmap
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.batman);
Shader shader1 = new BitmapShader(bitmap1, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 第二个 Shader:从上到下的线性渐变(由透明到黑色)
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo);
Shader shader2 = new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// ComposeShader:结合两个 Shader
Shader shader = new ComposeShader(shader1, shader2, PorterDuff.Mode.SRC_OVER);
paint.setShader(shader);
// …
canvas.drawCircle(300, 300, 300, paint);
注意:上面这段代码中我使用了两个 BitmapShader
来作为 ComposeShader()
的参数,而 ComposeShader()
在硬件加速下是不支持两个相同类型的 Shader
的,所以这里也需要关闭硬件加速才能看到效果。
PorterDuff.Mode
PorterDuff.Mode
是用来指定两个图像共同绘制时的颜色策略的。它是一个 enum,不同的 Mode
可以指定不同的策略。「颜色策略」的意思,就是说把源图像绘制到目标图像处时应该怎样确定二者结合后的颜色,而对于 ComposeShader(shaderA, shaderB, mode)
这个具体的方法,就是指应该怎样把 shaderB
绘制在 shaderA
上来得到一个结合后的 Shader
。
没有听说过 PorterDuff.Mode
的人,看到这里很可能依然会一头雾水:「什么怎么结合?就……两个图像一叠加,结合呗?还能怎么结合?」你还别说,还真的是有很多种策略来结合。
最符合直觉的结合策略,就是我在上面这个例子中使用的 Mode
: SRC_OVER
。它的算法非常直观:就像上面图中的那样,把源图像直接铺在目标图像上。不过,除了这种,其实还有一些其他的结合方式。例如如果我把上面例子中的参数 mode
改为 PorterDuff.Mode.DST_OUT
,就会变成挖空效果:
而如果再把 mode
改为 PorterDuff.Mode.DST_IN
,就会变成蒙版抠图效果:
具体来说, PorterDuff.Mode
一共有 17 个,可以分为两类:
- Alpha 合成 (Alpha Compositing)
- 混合 (Blending)
Alpha 合成 Alpha Compositing
第一类,Alpha 合成,其实就是 「PorterDuff」 这个词所指代的算法。 「PorterDuff」 并不是一个具有实际意义的词组,而是两个人的名字(准确讲是姓)。这两个人当年共同发表了一篇论文,描述了 12 种将两个图像共同绘制的操作(即算法)。而这篇论文所论述的操作,都是关于 Alpha 通道(也就是我们通俗理解的「透明度」)的计算的,后来人们就把这类计算称为Alpha 合成 ( Alpha Compositing ) 。
源图像和目标图像:
Alpha 合成:
混合 Blending
第二类,混合,也就是 Photoshop 等制图软件里都有的那些混合模式(multiply
darken
lighten
之类的)。这一类操作的是颜色本身而不是 Alpha
通道,并不属于 Alpha
合成,所以和 Porter 与 Duff 这两个人也没什么关系,不过为了使用的方便,它们同样也被 Google 加进了 PorterDuff.Mode
里。
结论
从效果图可以看出,Alpha 合成类的效果都比较直观,基本上可以使用简单的口头表达来描述它们的算法(起码对于不透明的源图像和目标图像来说是可以的),例如 SRC_OVER
表示「二者都绘制,但要源图像放在目标图像的上面」,DST_IN
表示「只绘制目标图像,并且只绘制它和源图像重合的区域」。
而混合类的效果就相对抽象一些,只从效果图不太能看得出它们的着色算法,更看不出来它们有什么用。不过没关系,你如果拿着这些名词去问你司的设计师,他们八成都能给你说出来个 123。
所以对于这些 Mode
,正确的做法是:对于 Alpha 合成类的操作,掌握他们,并在实际开发中灵活运用;而对于混合类的,你只要把它们的名字记住就好了,这样当某一天设计师告诉你「我要做这种混合效果」的时候,你可以马上知道自己能不能做,怎么做。