这期是 HenCoder 自定义绘制的第二期: Paint
。如果你没看过第一期,可以先去看一下第一期:
HenCoder Android 开发进阶:自定义 View 1-1 绘制基础
简介
上一期我已经简单说过, Canvas
的 drawXXX()
方法配合 Paint
的几个常用方法可以实现最常见的绘制需求;而如果你只会基本的绘制, Paint
的完全功能的掌握,能让你更进一步,做出一些更加细致、炫酷的效果。把 Paint
掌握之后,你几乎不再会遇到「iOS 组可以实现,但你却实现不了」的绘制效果。
由于依然是讲绘制的,所以这期就没有介绍视频了。绘制的内容一共需要讲大概 5~6 期才能讲完,也就是说你要看 5~6 期才能成为自定义绘制的高手。相对于上期的内容,这期的内容更为专项、深度更深。对于没有深入研究过 Paint
的人,这期是一个对 Paint
的诠释;而对于尝试过研究 Paint
但仍然对其中一些 API 有疑惑的人,这期也可以帮你解惑。
另外,也正由于这期的内容是更为专项的,所以建议你在看的时候,不必像上期那样把所有东西都完全记住,而是只要把内容理解了就好。这期的内容,只要做到「知道有这么个东西」,在需要用到的时候能想起来这个功能能不能做、大致用什么做就好,至于具体的实现,到时候拐回来再翻一次就行了。
好,下面进入正题。
Paint
的 API 大致可以分为 4 类:
- 颜色
- 效果
- drawText() 相关
- 初始化
下面我就对这 4 类分别进行介绍:
1 颜色
Canvas
绘制的内容,有三层对颜色的处理:
这图大概看看就行,不用钻研明白再往下看,因为等这章讲完你就懂了。
1.1 基本颜色
像素的基本颜色,根据绘制内容的不同而有不同的控制方式: Canvas
的颜色填充类方法 drawColor/RGB/ARGB()
的颜色,是直接写在方法的参数里,通过参数来设置的(上期讲过了); drawBitmap()
的颜色,是直接由 Bitmap
对象来提供的(上期也讲过了);除此之外,是图形和文字的绘制,它们的颜色就需要使用 paint
参数来额外设置了(下面要讲的)。
Paint
设置颜色的方法有两种:一种是直接用 Paint.setColor/ARGB()
来设置颜色,另一种是使用 Shader
来指定着色方案。
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);
setColor()
对应的 get 方法是getColor()
1.1.1.2 setARGB(int a, int r, int g, int b)
其实和 setColor(color)
都是一样一样儿的,只是它的参数用的是更直接的三原色与透明度的值。实际运用中,setColor()
和 setARGB()
哪个方便和顺手用哪个吧。
paint.setARGB(100, 255, 0, 0);
canvas.drawRect(0, 0, 200, 200, paint);
paint.setARGB(100, 0, 0, 0);
canvas.drawLine(0, 0, 200, 200, paint);
1.1.2 setShader(Shader shader) 设置 Shader
除了直接设置颜色, Paint
还可以使用 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);
设置了
Shader
之后,绘制出了渐变颜色的圆。(其他形状以及文字都可以这样设置颜色,我只是没给出图。)注意:在设置了
Shader
的情况下,Paint.setColor/ARGB()
所设置的颜色就不再起作用。
构造方法: 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
:辐射范围之外的着色模式。
CLAMP
:
MIRROR
:
REPEAT
:
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
。
CLAMP
:
MIRROR
:
REPEAT
:
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);
注意:上面这段代码中我使用了两个
BitmapShader
来作为ComposeShader()
的参数,而ComposeShader()
在硬件加速下是不支持两个相同类型的Shader
的,所以这里也需要关闭硬件加速才能看到效果。
构造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
参数: shaderA
, shaderB
:两个相继使用的 Shader
mode
: 两个 Shader
的叠加模式,即 shaderA
和 shaderB
应该怎样共同绘制。它的类型是 PorterDuff.Mode
。
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 合成,其实就是 「PorterDuff」 这个词所指代的算法。 「PorterDuff」 并不是一个具有实际意义的词组,而是两个人的名字(准确讲是姓)。这两个人当年共同发表了一篇论文,描述了 12 种将两个图像共同绘制的操作(即算法)。而这篇论文所论述的操作,都是关于 Alpha 通道(也就是我们通俗理解的「透明度」)的计算的,后来人们就把这类计算称为Alpha 合成 ( Alpha Compositing ) 。
看下效果吧。效果直接盗 Google 的官方文档了。
源图像和目标图像:
Alpha 合成:
第二类,混合,也就是 Photoshop 等制图软件里都有的那些混合模式(
multiply
darken
lighten
之类的)。这一类操作的是颜色本身而不是Alpha
通道,并不属于Alpha
合成,所以和 Porter 与 Duff 这两个人也没什么关系,不过为了使用的方便,它们同样也被 Google 加进了PorterDuff.Mode
里。效果依然盗 官方文档。
结论
从效果图可以看出,Alpha 合成类的效果都比较直观,基本上可以使用简单的口头表达来描述它们的算法(起码对于不透明的源图像和目标图像来说是可以的),例如
SRC_OVER
表示「二者都绘制,但要源图像放在目标图像的上面」,DST_IN
表示「只绘制目标图像,并且只绘制它和源图像重合的区域」。而混合类的效果就相对抽象一些,只从效果图不太能看得出它们的着色算法,更看不出来它们有什么用。不过没关系,你如果拿着这些名词去问你司的设计师,他们八成都能给你说出来个 123。
所以对于这些
Mode
,正确的做法是:对于 Alpha 合成类的操作,掌握他们,并在实际开发中灵活运用;而对于混合类的,你只要把它们的名字记住就好了,这样当某一天设计师告诉你「我要做这种混合效果」的时候,你可以马上知道自己能不能做,怎么做。另外:
PorterDuff.Mode
建议你动手用一下试试,对加深理解有帮助。
好了,这些就是几个 Shader
的具体介绍。
除了使用 setColor/ARGB()
和 setShader()
来设置基本颜色, Paint
还可以来设置 ColorFilter
,来对颜色进行第二层处理。
1.2 setColorFilter(ColorFilter colorFilter)
ColorFilter
这个类,它的名字已经足够解释它的作用:为绘制设置颜色过滤。颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX()
方法会对每个像素都进行过滤后再绘制出来。举几个现实中比较常见的颜色过滤的例子:
-
有色光照射:
-
有色玻璃透视:
-
胶卷:
在 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
一个「保持原样」的「基本 LightingColorFilter
」,mul
为 0xffffff
,add
为 0x000000
(也就是0),那么对于一个像素,它的计算过程就是:
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 ) ,那么它的计算过程就是:
R' = R * 0x0 / 0xff + 0x0 = 0 // 红色被移除
G' = G * 0xff / 0xff + 0x0 = G
B' = B * 0xff / 0xff + 0x0 = B
具体效果是这样的:
ColorFilter lightingColorFilter = new LightingColorFilter(0x00ffff, 0x000000);
paint.setColorFilter(lightingColorFilter);
表情忽然变得阴郁了
或者,如果你想让它的绿色更亮一些,就可以把它的 add
改为 0x003000
(绿色部分为 0x30 ),那么它的计算过程就是:
R' = R * 0xff / 0xff + 0x0 = R
G' = G * 0xff / 0xff + 0x30 = G + 0x30 // 绿色被加强
B' = B * 0xff / 0xff + 0x0 = B
效果是这样:
ColorFilter lightingColorFilter = new LightingColorFilter(0xffffff, 0x003000);
paint.setColorFilter(lightingColorFilter);
这样的表情才阳光
至于怎么修改参数来模拟你想要的某种具体光照效果,你就别问我了,还是跟你司设计师讨论吧,这个我不专业……
1.2.2 PorterDuffColorFilter
这个 PorterDuffColorFilter
的作用是使用一个指定的颜色和一种指定的 PorterDuff.Mode
来与绘制对象进行合成。它的构造方法是 PorterDuffColorFilter(int color, PorterDuff.Mode mode)
其中的 color
参数是指定的颜色, mode
参数是指定的 Mode
。同样也是 PorterDuff.Mode
,不过和 ComposeShader
不同的是,PorterDuffColorFilter
作为一个 ColorFilter
,只能指定一种颜色作为源,而不是一个 Bitmap
。
PorterDuff.Mode
前面已经讲过了,而 PorterDuffColorFilter
本身的使用是非常简单的,所以不再展开讲。
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
可以把要绘制的像素进行转换。对于颜色 [R, G, B, A] ,转换算法是这样的:
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)
来设置饱和度;另外你也可以自己去设置它的每一个元素来对转换效果做精细调整。具体怎样设置会有怎样的效果,我就不讲了(其实是我也不太会