Skip to content

Instantly share code, notes, and snippets.

@linsea
Created December 23, 2019 01:34
Show Gist options
  • Save linsea/fd28747938e07937e0d44c2bcb957b00 to your computer and use it in GitHub Desktop.
Save linsea/fd28747938e07937e0d44c2bcb957b00 to your computer and use it in GitHub Desktop.

Android Paint 详解

Paint 基础

设置颜色

paint.setColor(int color)
paint.setARGB(int a, int r, int g, int b)

然后使用Paint 来绘制基本图形, 如 canvas.drawRect() , canvas.drawLine() , canvas.drawText() .

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);

设置抗锯齿

抗锯齿默认是关闭的,如果需要抗锯齿,需要显式地打开: setAntiAlias (boolean aa)

设置风格

setStyle(Paint.Style style) 用来设置图形是线条风格还是填充风格的(也可以二者并用):

paint.setStyle(Paint.Style.FILL); // FILL 模式,填充
canvas.drawCircle(300, 300, 200, paint);

image

paint.setStyle(Paint.Style.FILL); // FILL 模式,填充
canvas.drawCircle(300, 300, 200, paint);

image

paint.setStyle(Paint.Style.FILL_AND_STROKE); // FILL_AND_STROKE 模式,填充 + 画线
canvas.drawCircle(300, 300, 200, paint);

image

设置线条形状

设置线条形状的通常有这几个方法:setStrokeWidth(float width), setStrokeCap(Paint.Cap cap), setStrokeJoin(Paint.Join join)

setStrokeWidth(float width) 设置线条宽度。单位为像素,默认值是 0。

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1);
canvas.drawCircle(150, 125, 100, paint);
paint.setStrokeWidth(5);
canvas.drawCircle(400, 125, 100, paint);
paint.setStrokeWidth(40);
canvas.drawCircle(650, 125, 100, paint);

image

设置线头的形状

setStrokeCap(Paint.Cap cap) 设置线头的形状。线头形状有三种:BUTT 平头、ROUND 圆头、SQUARE 方头。默认为 BUTT。当线条的宽度是 1 像素时,这三种线头的表现是完全一致的,全是 1 个像素的点;而当线条变粗的时候,它们就会表现出不同的样子:

image

设置拐角的形状

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

image

设置文字属性

绘制文字是使用 Canvas 的一组 drawText(..., paint) 方法. 但文字的属性是通过最后一个参数 paint 来进行设置的. 常用的方法是:

void setTextSize(float textSize) //设置字体大小
Typeface setTypeface(Typeface typeface) //设置字体类型
void setUnderlineText(boolean underlineText) //是否下划线

色彩优化

色彩优化作用都是让画面颜色过度时变得更加平滑, 不会显变化太快而太突兀.

设置图像的抖动

设置图像抖动更多的作用是在图像降低色彩深度绘制时,避免出现大片的色带与色块。一句代码就可以了: paint.setDither(true) . 实际效果如下: image

设置双线性过滤

图像在放大绘制的时候,默认使用的是最近邻插值过滤,这种算法简单,但会出现马赛克现象;而如果开启了双线性过滤,就可以让结果图像显得更加平滑。同样, 一句代码就行: paint.setFilterBitmap(true) . 实际效果如下: image

使用着色器(Shader)

着色器(Shader) 是计算机图形中的一个术语, 它是绘制颜色时, 使用什么颜色的一套方案, 一套规则, 一种算法. 给 Paint 设置着色器使用以下方法.

public Shader setShader(Shader shader)

设置 Shader 后, 除了绘制 bitmap , 绘制其他元素时使用的颜色都从此 Shader 中获取. 至于如何获取, 那是系统实现的. 我们只需要指定使用哪种 Shader . 这里不能直接使用 Shader 类, 它是一个没有实现具体效果的基类, 只能使用它的子类, Android 中 Shader 类有 5 个子类, 分别为: LinearGradient , RadialGradient , SweepGradient , BitmapShader , ComposeShader . 下面分别介绍.

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);

image

注意:在设置了 Shader 的情况下, Paint.setColor/ARGB() 所设置的颜色就不再起作用。

LinearGradient 的构造方法如下:

LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)

参数说明:
x0 y0 x1 y1:渐变的两个端点的位置
color0 color1: 端点的颜色
tile:端点范围之外的颜色铺开方式,它是枚举类型, 有3个可选值: CLAMP, MIRROR, REPEAT

CLAMP: 固定模式, 它会在端点之外延续时固定使用端点的颜色, 如下图所示:

CLAMP

MIRROR: 镜像模式, 如下图所示:

MIRROR

REPEAT: 重复模式, 如下图所示:

REPEAT

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);

CLAMP

构造方法:

RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)

参数说明:
centerX centerY:辐射中心的坐标
radius:辐射半径
centerColor:辐射中心的颜色
edgeColor:辐射边缘的颜色
tileMode:辐射范围之外的着色模式

CLAMP: 固定模式, 它会在端点之外延续时固定使用端点的颜色, 如下图所示:

CLAMP

MIRROR: 镜像模式, 如下图所示:

MIRROR

REPEAT: 重复模式, 如下图所示:

REPEAT

SweepGradient 扫描渐变

Shader shader = new SweepGradient(300, 300, Color.parseColor("#E91E63"),
        Color.parseColor("#2196F3"));
paint.setShader(shader);
...
canvas.drawCircle(300, 300, 200, paint);

SweepGradient

构造方法:
SweepGradient(float cx, float cy, int color0, int color1)

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

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);

BitmapShader

构造方法:
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

参数:
bitmap:用来做模板的 Bitmap 对象
tileX:横向的 TileMode
tileY:纵向的 TileMode。

CLAMP: 固定模式, 它会在端点之外延续时固定使用端点的颜色, 如下图所示:

CLAMP

MIRROR: 镜像模式, 如下图所示:

MIRROR

REPEAT: 重复模式, 如下图所示:

REPEAT

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

构造方法:ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

参数:
shaderA, shaderB:两个相继使用的 Shader
mode: 两个 Shader 的叠加模式,即 shaderA 和 shaderB 应该怎样共同绘制。它的类型是 PorterDuff.Mode 。

PorterDuff.Mode 是用来指定两个图像共同绘制时的颜色策略的。它是一个 enum,不同的 Mode 可以指定不同的策略。颜色策略的意思,就是说把源图像绘制到目标图像处时应该怎样确定二者结合后的颜色,而对于 ComposeShader(shaderA, shaderB, mode) 这个具体的方法,就是指应该怎样把 shaderB 绘制在 shaderA 上来得到一个结合后的 Shader。

PorterDuff.Mode 一共有 17 种,参考官方文档, 可以分为两类:

  • Alpha 合成 (Alpha Compositing)
  • 混合 (Blending)

第一类,Alpha 合成,其实就是 PorterDuff 这个词所指代的算法。 PorterDuff 并不是一个具有实际意义的词组,而是两个人的名字(准确讲是姓)。这两个人当年共同发表了一篇论文,描述了 12 种将两个图像共同绘制的操作(即算法)。而这篇论文所论述的操作,都是关于 Alpha 通道(也就是我们通俗理解的透明度)的计算的,后来人们就把这类计算称为 Alpha 合成 ( Alpha Compositing ) 。

Alpha 合成 (Alpha Compositing) 种类

Alpha

第二类,混合,也就是 Photoshop 等制图软件里都有的那些混合模式(multiply darken lighten 之类的, 共有 5 种)。这一类操作的是颜色本身而不是 Alpha 通道, 至于颜色具体合成算法, 可以参考官方文档.

混合 (Blending) 种类

Blending

ColorFilter 颜色过滤

使用 Paint.setColorFilter(ColorFilter filter) 方法为 Paint 绘制设置颜色过滤。颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略,然后 Canvas.drawXXX() 方法会对每个像素都进行过滤后再绘制出来。 无法直接使用 ColorFilter 类本身, 只能使用它的子类, 系统为我们提供了 3 个子类供选择: LightingColorFilter , PorterDuffColorFilter , ColorMatrixColorFilter.

LightingColorFilter

LightingColorFilter 是用来模拟简单的光照效果的.

public LightingColorFilter(int colorMultiply, int colorAdd)
参数:
colorMultiply 和 colorAdd 都是和颜色值格式相同的 int 值,
其中 colorMultiply 用来和目标像素相乘,colorAdd 用来和目标像素相加

新颜色值的计算公式如下, 注意 alpha 通道的值没有参与运算, 它是不变的.

// colorMultiply.red 和 colorAdd.red 表示颜色的 red 通道分量
newRed = color.red * colorMultiply.red + colorAdd.red
newGreen = color.green * colorMultiply.green + colorAdd.green
newBlue = color.blue * colorMultiply.blue + colorAdd.blue

思考: 通过以上公式, 可知如何去掉原图的某通道的颜色, 或者增强某通道的颜色效果.
比如使原图去掉红色, 或者使原图中的红色更红.

对于原画如下的图片:

original

使用了如图中相应参数后的效果如下:

LightingColorFilter

PorterDuffColorFilter

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

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) 来设置饱和度;另外你也可以自己去设置它的每一个元素来对转换效果做精细调整。下面的示例显示了使用指定的 ColorMatrix 对图片作了处理后的效果. ColorMatrixColorFilter

更多使用 ColorFilter 的示例, 可以参考这个库: https://github.com/chengdazhi/StyleImageView

设置 Xfermode

Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色。但通俗地说,其实就是要你以绘制的内容作为源图像,以 View 中已有的内容作为目标图像,选取一个 PorterDuff.Mode 作为绘制内容的颜色处理方案。

源图(将绘制的图)

Source image

目标图(已绘制到载体的图)

Destination image

按以下绘制顺序生成最终图:

Paint paint = new Paint();
canvas.drawBitmap(destinationImage, 0, 0, paint);

PorterDuff.Mode mode = // choose a mode
paint.setXfermode(new PorterDuffXfermode(mode));

canvas.drawBitmap(sourceImage, 0, 0, paint);

最终的叠加效果就是 ComposeShader 混合着色器 一节中展示的 17 种效果图.

PorterDuff.Mode 总结

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

API 用途
ComposeShader 混合两个 shader
PorterDuffColorFilter 增加一个单色的 ColorFilter
Xfermode 设置绘制内容和 View 中已有内容的混合计算方式

路径特效

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);

image

Android 中共有 6 种 PathEffect。PathEffect 分为两类,单一效果的 CornerPathEffect, DiscretePathEffect, DashPathEffect, PathDashPathEffect ,和组合效果的 SumPathEffect, ComposePathEffect

CornerPathEffect

CornerPathEffect 把所有拐角变成圆角

PathEffect pathEffect = new CornerPathEffect(20) //参数 radius 是圆角的半径

image

DiscretePathEffect

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

PathEffect pathEffect = new DiscretePathEffect(20, 5)

DiscretePathEffect 具体的做法是,把绘制改为使用定长的线段来拼接,并且在拼接的时候对路径进行随机偏离。它的构造方法 DiscretePathEffect(float segmentLength, float deviation) 的两个参数中, segmentLength 是用来拼接的每个线段的长度, deviation 是偏离量。

image

DashPathEffect

DashPathEffect 使用虚线来绘制线条。

PathEffect pathEffect = new DashPathEffect(new float[]{20, 10, 5, 10}, 0);

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

PathDashPathEffect

PathDashPathEffectDashPathEffect 多一个前缀 Path ,所以顾名思义,它是使用一个 Path 来绘制虚线

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

image

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

image

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);

image

ComposePathEffect

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

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

image 它的构造方法 ComposePathEffect(PathEffect outerpe, PathEffect innerpe) 中的两个 PathEffect 参数, innerpe 是先应用的, outerpe 是后应用的。所以上面的代码就是先偏离,再变虚线。

阴影特效

setShadowLayer(float radius, float dx, float dy, int shadowColor) 在之后的绘制内容下面加一层阴影。方法的参数里, radius 是阴影的模糊范围; dx dy 是阴影的偏移量; shadowColor 是阴影的颜色。如果要清除阴影层,使用 clearShadowLayer()

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

image

注意: 在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度。

模糊遮罩滤镜

setMaskFilter(MaskFilter maskfilter) 在绘制层上方的附加效果, 上一个方法 setShadowLayer() 是设置的在绘制层下方的附加效果相反. Android 系统内置了两种 MaskFilter : BlurMaskFilterEmbossMaskFilter

BlurMaskFilter

BlurMaskFilter 的构造方法为: BlurMaskFilter(float radius, Blur style), 参数 radius 指模糊的半径, style 是枚举, 有4种可选: NORMAL: 内外都模糊绘制; SOLID: 内部正常绘制,外部模糊; OUTER: 内部不绘制,外部模糊; INNER: 内部模糊,外部不绘制;

//关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE,null);
//红色的画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
//NORMAL: 内外都模糊绘制
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
canvas.drawRect(200,100,400,300,paint);
//SOLID: 内部正常绘制,外部模糊
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.SOLID));
canvas.drawRect(600,100,800,300,paint);
//INNER: 内部模糊,外部不绘制
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.OUTER));
canvas.drawRect(200,500,400,700,paint);
//OUTER: 内部不绘制,外部模糊
paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.INNER));
canvas.drawRect(600,500,800,700,paint);

image

EmbossMaskFilter

浮雕效果的 MaskFilter。构造方法:

EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)
direction 是一个 3 个元素的数组,指定了光源的方向;
ambient 是环境光的强度,数值范围是 0 到 1;
specular 是炫光的系数;
blurRadius 是应用光线的范围。
EmbossMaskFilter filter = new EmbossMaskFilter(
                        new float[]{1,5,1}, // direction of the light source
                        0.5f, // ambient light between 0 to 1
                        10, // specular highlights
                        7.5f // blur before applying lighting
                );

textView.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
textView.getPaint().setMaskFilter(filter);

应用浮雕效果前后的对比:

image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment