Paint 属性分类
- 颜色
- 效果
- drawText() 相关
- 初始化
1 颜色
Canvas 绘制的内容,有三层对颜色的处理:
1.1 基本颜色
1.1.1 直接设置颜色
1.1.1.1 setColor(int color)
paint.setColor(Color.parseColor("#009688"));
canvas.drawRect(30, 30, 230, 180, paint);
paint.setColor(Color.parseColor("#FF9800"));
canvas.drawLine(300, 30, 450, 180, paint);
paint.setColor(Color.parseColor("#E91E63"));
canvas.drawText("HenCoder", 500, 130, paint);
1.1.1.2 setARGB(int a, int r, int g, int b)
和 setColor(color) 都是一样一样儿的,只是它的参数用的是更直接的三原色与透明度的值。实际运用中,setColor() 和 setARGB() 哪个方便和顺手用哪个吧。
1.1.2 setShader(Shader shader) 设置 Shader
- 着色器不是 Android 独有的,它是图形领域里一个通用的概念,它和直接设置颜色的区别是,着色器设置的是一个颜色方案,或者说是一套着色规则。
- 当设置了 Shader 之后,Paint 在绘制图形和文字时就不使用 setColor/ARGB() 设置的颜色了,而是使用 Shader 的方案中的颜色。
- 在 Android 的绘制里使用 Shader ,并不直接用 Shader 这个类,而是用它的几个子类。具体来讲有 LinearGradient RadialGradient SweepGradient BitmapShader ComposeShader 这么几个:
1.1.2.1 LinearGradient 线性渐变
设置两个点和两种颜色,以这两个点作为端点,使用两种颜色的渐变来绘制颜色。
就像这样:
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);
构造方法:
LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile) 。
参数:
x0 y0 x1 y1:渐变的两个端点的位置
color0 color1 是端点的颜色
tile:端点范围之外的着色规则,类型是 TileMode。TileMode 一共有 3 个值可选: CLAMP, MIRROR 和 REPEAT。CLAMP (夹子模式???算了这个词我不会翻)会在端点之外延续端点处的颜色;MIRROR 是镜像模式;REPEAT 是重复模式。具体的看一下例子就明白。
CLAMP:
MIRROR:
REPEAT:
1.1.2.2 RadialGradient 辐射渐变
辐射渐变很好理解,就是从中心向周围辐射状的渐变
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);
构造方法:
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)。
参数:
centerX centerY:辐射中心的坐标
radius:辐射半径
centerColor:辐射中心的颜色
edgeColor:辐射边缘的颜色
tileMode:辐射范围之外的着色模式。
TileMode 类型和LinearGradient的TitleMode是一样的
1.1.2.3 SweepGradient 扫描渐变
扫描渐变
Shader shader = new SweepGradient(300, 300, Color.parseColor("#E91E63"),
Color.parseColor("#2196F3"));
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 200, paint);
构造方法:
SweepGradient(float cx, float cy, int color0, int color1)
参数:
cx cy :扫描的中心
color0:扫描的起始颜色
color1:扫描的终止颜色
1.1.2.4 BitmapShader
用 Bitmap 来着色(终于不是渐变了)。其实也就是用 Bitmap 的像素来作为图形或文字的填充
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 就可以了(其他形状同理)。
构造方法:
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
参数:
bitmap:用来做模板的 Bitmap 对象
tileX:横向的 TileMode
tileY:纵向的 TileMode。
1.1.2.5 ComposeShader 混合着色器
两个 Shader 一起使用
// 第一个 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);
ComposeShader() 在硬件加速下是不支持两个相同类型的 Shader 的,所以这里也需要关闭硬件加速才能看到效果。
构造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
参数:
shaderA, shaderB:两个相继使用的 Shader
mode: 两个 Shader 的叠加模式,即 shaderA 和 shaderB 应该怎样共同绘制。它的类型是 PorterDuff.Mode
PorterDuff.Mode效果,讲解
1.2 setColorFilter(ColorFilter colorFilter)
为绘制设置颜色过滤。颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX() 方法会对每个像素都进行过滤后再绘制出来。
举几个现实中比较常见的颜色过滤的例子:
-
有色光照射:
image.png -
有色玻璃透视:
-
胶卷:
在 Paint 里设置 ColorFilter ,使用的是 Paint.setColorFilter(ColorFilter filter) 方法。
ColorFilter 并不直接使用,而是使用它的子类。它共有三个子类:LightingColorFilter PorterDuffColorFilter 和 ColorMatrixColorFilter。
1.2.1 LightingColorFilter
这个 LightingColorFilter 是用来模拟简单的光照效果的。
LightingColorFilter 的构造方法是 LightingColorFilter(int mul, int add) ,参数里的 mul 和 add 都是和颜色值格式相同的 int 值,其中 mul 用来和目标像素相乘,add 用来和目标像素相加:
R' = R * mul.R / 0xff + add.R
G' = G * mul.G / 0xff + add.G
B' = B * mul.B / 0xff + add.B
效果:
1.2.2 PorterDuffColorFilter
作用是使用一个指定的颜色和一种指定的 PorterDuff.Mode 来与绘制对象进行合成。
它的构造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode) 其中的 color 参数是指定的颜色, mode 参数是指定的 Mode。同样也是 PorterDuff.Mode ,不过和 ComposeShader 不同的是,PorterDuffColorFilter 作为一个 ColorFilter,只能指定一种颜色作为源,而不是一个 Bitmap。
1.2.3 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 可以把要绘制的像素进行转换。
效果:
1.3 setXfermode(Xfermode xfermode)
- "Xfermode" 其实就是 "Transfer mode",用 "X" 来代替 "Trans" 是一些美国人喜欢用的简写方式
- 以绘制的内容作为源图像,以 View 中已有的内容作为目标图像,选取一个 PorterDuff.Mode 作为绘制内容的颜色处理方案。
效果:
创建 Xfermode 的时候其实是创建的它的子类 PorterDuffXfermode。而事实上,Xfermode 也只有这一个子类。所以在设置 Xfermode 的时候不用多想,直接用 PorterDuffXfermode 吧。
「只有一个子类???什么设计?」
实在更早的 Android 版本中,Xfermode 还有别的子类,但别的子类现在已经 deprecated 了,如今只剩下了 PorterDuffXfermode。所以目前它的使用看起来好像有点啰嗦,但其实是由于历史遗留问题。
Xfermode 注意事项
1. 使用离屏缓冲(Off-screen Buffer)
- Canvas.saveLayer()
saveLayer() 可以做短时的离屏缓冲。使用方法很简单,在绘制代码的前后各加一行代码,在绘制之前保存,绘制之后恢复:
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);
2. 控制好透明区域
使用 Xfermode 来绘制的内容,除了注意使用离屏缓冲,还应该注意控制它的透明区域不要太小,要让它足够覆盖到要和它结合绘制的内容,否则得到的结果很可能不是你想要的。
2 效果
效果类的 API ,指的就是抗锯齿、填充/轮廓、线条宽度等等这些。
2.1 setAntiAlias (boolean aa) 设置抗锯齿
抗锯齿在上一节已经讲过了,话不多说,直接上图:
另外,除了 setAntiAlias(aa) 方法,打开抗锯齿还有一个更方便的方式:构造方法。创建 Paint 对象的时候,构造方法的参数里加一个 ANTI_ALIAS_FLAG 的 flag,就可以在初始化的时候就开启抗锯齿。
2.2 setStyle(Paint.Style style)
setStyle(style) 也在上一节讲过了,用来设置图形是线条风格还是填充风格的(也可以二者并用):
paint.setStyle(Paint.Style.FILL); // FILL 模式,填充
canvas.drawCircle(300, 300, 200, paint);
STROKE
FILL_AND_STROKE
2.3 线条形状
设置线条形状的一共有 4 个方法:setStrokeWidth(float width), setStrokeCap(Paint.Cap cap), setStrokeJoin(Paint.Join join), setStrokeMiter(float miter) 。
2.3.1 setStrokeWidth(float width)
2.3.2 setStrokeCap(Paint.Cap cap)
设置线头的形状。线头形状有三种:BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT。
2.3.3 setStrokeJoin(Paint.Join join)
设置拐角的形状。有三个值可以选择:MITER 尖角、 BEVEL 平角和 ROUND 圆角。默认为 MITER。
2.3.4 setStrokeMiter(float miter)
这个方法是对于 setStrokeJoin() 的一个补充,它用于设置 MITER 型拐角的延长线的最大值。所谓「延长线的最大值」,是这么一回事:
当线条拐角为 MITER 时,拐角处的外缘需要使用延长线来补偿:
而这种补偿方案会有一个问题:如果拐角的角度太小,就有可能由于出现连接点过长的情况。比如这样:
所以为了避免意料之外的过长的尖角出现, MITER 型连接点有一个额外的规则:当尖角过长时,自动改用 BEVEL 的方式来渲染连接点。例如上图的这个尖角,在默认情况下是不会出现的,而是会由于延长线过长而被转为 BEVEL 型连接点:
2.4 色彩优化
Paint 的色彩优化有两个方法: setDither(boolean dither) 和 setFilterBitmap(boolean filter) 。它们的作用都是让画面颜色变得更加「顺眼」,但原理和使用场景是不同的。
2.4.1 setDither(boolean dither)
设置图像的抖动。
所谓抖动(注意,它就叫抖动,不是防抖动,也不是去抖动,有些人在翻译的时候自作主张地加了一个「防」字或者「去」字,这是不对的),是指把图像从较高色彩深度(即可用的颜色数)向较低色彩深度的区域绘制时,在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法。
例子:
比如向 1 位色彩深度的区域中绘制灰色,由于 1 位深度只包含黑和白两种颜色,在默认情况下,即不加抖动的时候,只能选择向上或向下选择最接近灰色的白色或黑色来绘制,那么显示出来也只能是一片白或者一片黑。而加了抖动后,就可以绘制出让肉眼识别为灰色的效果了:
眯着眼看这幅图
2.4.2 setFilterBitmap(boolean filter)
设置是否使用双线性过滤来绘制 Bitmap 。
图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。效果依然盗维基百科的图:
2.5 setPathEffect(PathEffect effect)
使用 PathEffect 来给图形的轮廓设置效果。对 Canvas 所有的图形绘制有效,也就是 drawLine() drawCircle() drawPath() 这些方法。大概像这样:
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。
2.5.1 CornerPathEffect
把所有拐角变成圆角。
PathEffect pathEffect = new CornerPathEffect(20);
它的构造方法 CornerPathEffect(float radius) 的参数 radius 是圆角的半径。
2.5.2 DiscretePathEffect
把线条进行随机的偏离,让轮廓变得乱七八糟。乱七八糟的方式和程度由参数决定。
PathEffect pathEffect = new DiscretePathEffect(20, 5);
DiscretePathEffect 具体的做法是,把绘制改为使用定长的线段来拼接,并且在拼接的时候对路径进行随机偏离。它的构造方法 DiscretePathEffect(float segmentLength, float deviation) 的两个参数中, segmentLength 是用来拼接的每个线段的长度, deviation 是偏离量。这两个值设置得不一样,显示效果也会不一样,具体的你自己多试几次就明白了,这里不再贴更多的图。
2.5.3 DashPathEffect
使用虚线来绘制线条。
它的构造方法 DashPathEffect(float[] intervals, float phase) 中, 第一个参数 intervals 是一个数组,它指定了虚线的格式:数组中元素必须为偶数(最少是 2 个),按照「画线长度、空白长度、画线长度、空白长度」……的顺序排列,例如上面代码中的 20, 5, 10, 5 就表示虚线是按照「画 20 像素、空 5 像素、画 10 像素、空 5 像素」的模式来绘制;第二个参数 phase 是虚线的偏移量。
2.5.4 PathDashPathEffect
这个方法比 DashPathEffect 多一个前缀 Path ,所以顾名思义,它是使用一个 Path 来绘制「虚线」。具体看图吧:
它的构造方法 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:变体
2.5.5 SumPathEffect
这是一个组合效果类的 PathEffect 。它的行为特别简单,就是分别按照两种 PathEffect 分别对目标进行绘制。
PathEffect dashEffect = new DashPathEffect(new float[]{20, 10}, 0);
PathEffect discreteEffect = new DiscretePathEffect(20, 5);
pathEffect = new SumPathEffect(dashEffect, discreteEffect);
...
canvas.drawPath(path, paint);
2.5.6 ComposePathEffect
这也是一个组合效果类的 PathEffect 。不过它是先对目标 Path 使用一个 PathEffect,然后再对这个改变后的 Path 使用另一个 PathEffect。
PathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 10);
paint.setPathEffect(pathEffect);
...
canvas.drawCircle(300, 300, 200, paint);
它的构造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 中的两个 PathEffect 参数, innerpe 是先应用的, outerpe 是后应用的。所以上面的代码就是「先偏离,再变虚线」。而如果把两个参数调换,就成了「先变虚线,再偏离」。至于具体的视觉效果……我就不贴图了,你自己试试看吧!
总结
上面这些就是 Paint 中的 6 种 PathEffect。它们有的是有独立效果的,有的是用来组合不同的 PathEffect 的,功能各不一样。
注意: PathEffect 在有些情况下不支持硬件加速,需要关闭硬件加速才能正常使用:
Canvas.drawLine() 和 Canvas.drawLines() 方法画直线时,setPathEffect() 是不支持硬件加速的;
PathDashPathEffect 对硬件加速的支持也有问题,所以当使用 PathDashPathEffect 的时候,最好也把硬件加速关了。
2.6 setShadowLayer(float radius, float dx, float dy, int shadowColor)
在之后的绘制内容下面加一层阴影。
paint.setShadowLayer(10, 0, 0, Color.RED);
...
canvas.drawText(text, 80, 300, paint);
效果就是上面这样。方法的参数里, radius 是阴影的模糊范围; dx dy 是阴影的偏移量; shadowColor 是阴影的颜色。
如果要清除阴影层,使用 clearShadowLayer() 。
注意:
- 在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。
- 如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。
2.7 setMaskFilter(MaskFilter maskfilter)
上一个方法 setShadowLayer() 是设置的在绘制层下方的附加效果;而这个 MaskFilter 和它相反,设置的是在绘制层上方的附加效果。
到现在已经有两个 setXxxFilter(filter) 了。前面有一个 setColorFilter(filter) ,是对每个像素的颜色进行过滤;而这里的 setMaskFilter(filter) 则是基于整个画面来进行过滤。
MaskFilter 有两种: BlurMaskFilter 和 EmbossMaskFilter。
2.7.1 BlurMaskFilter
模糊效果的 MaskFilter。
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
...
canvas.drawBitmap(bitmap, 100, 100, paint);
它的构造方法 BlurMaskFilter(float radius, BlurMaskFilter.Blur style) 中, radius 参数是模糊的范围, style 是模糊的类型。一共有四种:
- NORMAL: 内外都模糊绘制
- SOLID: 内部正常绘制,外部模糊
- INNER: 内部模糊,外部不绘制
- OUTER: 内部不绘制,外部模糊(什么鬼?)
2.7.2 EmbossMaskFilter
浮雕效果的 MaskFilter。
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 是应用光线的范围。
2.8 获取绘制的 Path
根据 paint 的设置,计算出绘制 Path 或文字时的实际 Path。
2.8.1 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 里。
2.8.2 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)
文字的绘制,虽然是使用 Canvas.drawText() 方法,但其实在下层,文字信息全是被转化成图形,对图形进行绘制的。 getTextPath() 方法,获取的就是目标文字所对应的 Path 。这个就是所谓「文字的 Path」。
这两个方法, getFillPath()
和 getTextPath()
,就是获取绘制的 Path
的方法。之所以把它们归类到「效果」类方法,是因为它们主要是用于图形和文字的装饰效果的位置计算,比如自定义的下划线效果。
到此为止, Paint
的第二类方法——效果类,就也介绍完了。
3 drawText() 相关
Paint 有些设置是文字绘制相关的,即和 drawText() 相关的。
比如设置文字大小:
比如设置文字间隔:
比如设置各种文字效果:
除此之外,Paint 还有很多与文字绘制相关的设置或计算的方法,非常详细。不过由于太详细了,相关方法太多了(Paint 超过一半的方法都是 drawText() 相关的,算不算多?),如果放在这里讲它们的话,内容会显得有点过量。所以这一节我就不讲它们了,把它们放在下一节里单独讲。
4 初始化类
这一类方法很简单,它们是用来初始化 Paint 对象,或者是批量设置 Paint 的多个属性的方法。
4.1 reset()
重置 Paint 的所有属性为默认值。相当于重新 new 一个,不过性能当然高一些啦。
4.2 set(Paint src)
把 src 的所有属性全部复制过来。相当于调用 src 所有的 get 方法,然后调用这个 Paint 的对应的 set 方法来设置它们。
4.3 setFlags(int flags)
批量设置 flags。相当于依次调用它们的 set 方法。例如:
paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
这行代码,和下面这两行是等价的:
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
...
canvas.drawBitmap(bitmap, 100, 100, paint);
参考:
https://hencoder.com/ui-1-2/
注意:本文全部摘自上面的链接,本人只是对上面链接内容的自我抽取,符合本人阅读
注意
最后再强调一遍:这期的内容没必要全部背会,只要看懂、理解,记住有这么个东西就行了。以后在用到的时候,再拐回来翻一翻就行了。