目录
  1. 1. 简介
  2. 2. Paint 核心属性全解
    1. 2.1. 基本属性
      1. 2.1.1. 颜色设置
      2. 2.1.2. Style(绘制风格)
      3. 2.1.3. 描边相关
      4. 2.1.4. 抗锯齿与抖动
  3. 3. Shader:着色器
    1. 3.1. LinearGradient(线性渐变)
      1. 3.1.1. TileMode 三种平铺模式
    2. 3.2. RadialGradient(径向渐变 / 圆形渐变)
    3. 3.3. SweepGradient(扫描渐变 / 扇形渐变)
    4. 3.4. BitmapShader(位图着色器)
    5. 3.5. ComposeShader(混合着色器)
  4. 4. Path:路径
    1. 4.1. 基本线段
    2. 4.2. 矩形、圆角矩形、椭圆、圆
    3. 4.3. 弧线:arcTo
    4. 4.4. 贝塞尔曲线:quadTo 与 cubicTo
      1. 4.4.1. quadTo(二次贝塞尔曲线)
      2. 4.4.2. cubicTo(三次贝塞尔曲线)
    5. 4.5. PathMeasure:路径测量与动画
      1. 4.5.1. 路径追踪动画示例
    6. 4.6. PathEffect:路径效果
  5. 5. PorterDuff Xfermode:像素混合模式
    1. 5.1. 基本使用
    2. 5.2. 18 种混合模式分类
      1. 5.2.1. Alpha 合成模式(基础)
      2. 5.2.2. 混合模式(较少使用)
    3. 5.3. 实际应用场景
      1. 5.3.1. SRC_IN:圆角/圆形图片裁剪
      2. 5.3.2. DST_IN:遮罩效果
      3. 5.3.3. CLEAR:擦除效果(刮刮卡)
      4. 5.3.4. 离屏缓冲(saveLayer)的必要性
  6. 6. ColorFilter:颜色过滤器
    1. 6.1. LightingColorFilter(光照颜色过滤器)
    2. 6.2. PorterDuffColorFilter(PorterDuff 颜色过滤器)
    3. 6.3. ColorMatrix(颜色矩阵)
      1. 6.3.1. 常用 ColorMatrix 预设
  7. 7. MaskFilter:遮罩滤镜
    1. 7.1. BlurMaskFilter
  8. 8. Canvas 状态管理
    1. 8.1. save / restore 机制
    2. 8.2. clipRect / clipPath / clipOutRect
      1. 8.2.1. 裁剪的实用场景
  9. 9. 文本渲染
    1. 9.1. drawText 基础
    2. 9.2. Paint 文本相关属性
    3. 9.3. FontMetrics:精确控制文本位置
    4. 9.4. breakText:文本换行测量
    5. 9.5. StaticLayout:多行文本布局
    6. 9.6. DynamicLayout:可编辑文本布局
  10. 10. 完整示例一:音频波形可视化器
  11. 11. 完整示例二:圆角图片组件(BitmapShader 实现)
  12. 12. 完整示例三:刮刮卡效果(Xfermode CLEAR 模式)
  13. 13. 总结
    1. 13.1. 关键源码文件
UI进阶之Paint Canvas高级绘制

简介

Paint 和 Canvas 是 Android 2D 绘制的核心类。Canvas 代表画布,提供绘制方法;Paint 代表画笔,控制绘制的风格和效果。二者协同工作,可以实现从简单图形到复杂视觉效果的任意绘制。

本文从 AOSP 源码角度深入分析 Paint 和 Canvas 的高级特性:Shader(着色器)的六种类型、Path(路径)的贝塞尔曲线与 PathMeasure 动画、PorterDuff Xfermode 的 18 种混合模式、ColorFilter 的颜色矩阵变换、MaskFilter 的模糊效果,以及 Canvas 的状态管理和文本渲染。文末提供三个完整的实战示例:音频波形可视化器、使用 BitmapShader 的圆角图片组件、基于 Xfermode 的刮刮卡效果。

注意:源码分析基于 Android-30(Android 11),核心源码位于 frameworks/base/graphics/java/android/graphics/ 目录。

Paint 核心属性全解

基本属性

颜色设置

// 方式一:直接设置颜色
paint.setColor(0xFFFF0000); // ARGB 格式,红色

// 方式二:分别设置
paint.setARGB(255, 255, 0, 0);
paint.setAlpha(128); // 设置透明度,范围 0-255

setAlphasetColor 中嵌入的 alpha 值是叠加关系:最终的 alpha = colorAlpha * alpha / 255。

Style(绘制风格)

paint.setStyle(Paint.Style.FILL);    // 填充模式(默认)
paint.setStyle(Paint.Style.STROKE); // 描边模式
paint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充+描边
  • FILL:只填充图形内部,不绘制轮廓。
  • STROKE:只绘制图形轮廓,不填充内部。描边宽度由 setStrokeWidth 控制。
  • FILL_AND_STROKE:既填充又描边。描边宽度在轮廓线两侧各占一半。

描边相关

paint.setStrokeWidth(width);                    // 描边宽度,单位 px
paint.setStrokeCap(Paint.Cap.ROUND); // 线端点形状:BUTT / ROUND / SQUARE
paint.setStrokeJoin(Paint.Join.MITER); // 线段连接处:MITER / ROUND / BEVEL
paint.setStrokeMiter(miter); // MITER 连接处的斜接限制
  • Cap.BUTT:无端点装饰,直接截断。
  • Cap.ROUND:端点添加半圆形,多出 strokeWidth/2 的长度。
  • Cap.SQUARE:端点添加矩形,多出 strokeWidth/2 的长度。
  • Join.MITER:尖角连接。当夹角过小时,需要用 setStrokeMiter 限制。
  • Join.ROUND:圆弧连接。
  • Join.BEVEL:斜角连接。

抗锯齿与抖动

paint.setAntiAlias(true);   // 抗锯齿,使边缘平滑
paint.setDither(true); // 颜色抖动,用于低色深位图减少色带

抗锯齿通过边缘像素的半透明处理使线条看起来更平滑,但有轻微的性能开销。setDither 主要用于绘制颜色深度较低的位图,通过引入噪点来减少色带效应。

Shader:着色器

Shader 定义了如何在图形内部填充颜色。源码位于 frameworks/base/graphics/java/android/graphics/Shader.java,核心子类有六个。

LinearGradient(线性渐变)

/**
* x0, y0: 渐变起点
* x1, y1: 渐变终点
* color0: 起点颜色
* color1: 终点颜色
* tile: 平铺模式(CLAMP / REPEAT / MIRROR)
*/
Shader linearGradient = new LinearGradient(
0, 0, // 起点
width, 0, // 终点(水平渐变)
Color.RED, // 起点颜色
Color.BLUE, // 终点颜色
Shader.TileMode.CLAMP
);
paint.setShader(linearGradient);

多色渐变:

int[] colors = {Color.RED, Color.YELLOW, Color.GREEN, Color.BLUE};
float[] positions = {0f, 0.33f, 0.66f, 1f};
Shader multiColorGradient = new LinearGradient(
0, 0, width, 0, colors, positions, Shader.TileMode.CLAMP
);

positions 数组定义每个颜色的位置,取值范围 [0, 1]。如果不传,颜色均匀分布。

TileMode 三种平铺模式

CLAMP  (钳位):    |████████████| 超出区域用边缘颜色填充
REPEAT (重复): |████ ████ ███| 循环重复渐变
MIRROR (镜像): |████ ████ ███| 镜像翻转重复渐变

源码位置:frameworks/base/graphics/java/android/graphics/Shader.java

public enum TileMode {
CLAMP (0),
REPEAT (1),
MIRROR (2);

TileMode(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}

RadialGradient(径向渐变 / 圆形渐变)

/**
* centerX, centerY: 圆心
* radius: 渐变半径
*/
Shader radialGradient = new RadialGradient(
centerX, centerY, // 圆心坐标
radius, // 渐变半径
Color.WHITE, // 中心颜色
Color.BLUE, // 边缘颜色
Shader.TileMode.CLAMP
);

常用于实现阴影光晕、按钮高光等圆形渐变效果。

SweepGradient(扫描渐变 / 扇形渐变)

/**
* cx, cy: 扫描中心
* 从 3 点钟方向开始顺时针扫描
*/
Shader sweepGradient = new SweepGradient(
cx, cy, // 扫描中心
Color.RED, // 起始颜色(3 点钟方向)
Color.BLUE // 终点颜色(回到 3 点钟方向)
);

多色的扫描渐变:

int[] colors = {Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA};
Shader rainbowSweep = new SweepGradient(cx, cy, colors, null);

BitmapShader(位图着色器)

BitmapShader 使用位图作为填充图案,是最强大的 Shader 之一,常用于实现圆角头像、图片裁剪等效果。

/**
* bitmap: 源位图
* tileX / tileY: X/Y 方向的平铺模式
*/
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.avatar);
Shader bitmapShader = new BitmapShader(
bitmap,
Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP
);
paint.setShader(bitmapShader);

// 绘制圆形头像
canvas.drawCircle(cx, cy, radius, paint);

配合 Matrix 可以实现图片的缩放和偏移:

BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Matrix matrix = new Matrix();
float scale = Math.max(
(float) viewWidth / bitmap.getWidth(),
(float) viewHeight / bitmap.getHeight()
);
matrix.setScale(scale, scale);
matrix.postTranslate(
(viewWidth - bitmap.getWidth() * scale) / 2f,
(viewHeight - bitmap.getHeight() * scale) / 2f
);
shader.setLocalMatrix(matrix);

ComposeShader(混合着色器)

ComposeShader 将两个 Shader 按照指定的 PorterDuff 模式进行混合:

Shader shader1 = new BitmapShader(bitmap1, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Shader shader2 = new LinearGradient(0, 0, 0, height, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP);
Shader composeShader = new ComposeShader(shader1, shader2, PorterDuff.Mode.DST_IN);
paint.setShader(composeShader);

这可以实现”在图片上叠加渐变遮罩”等效果。

Path:路径

Path 是 Android 2D 绘制中最强大的图形描述工具。它支持直线、弧线、贝塞尔曲线等多种线段组合,可以描述任意复杂的二维图形。

基本线段

Path path = new Path();

// 移动到起点(不画线)
path.moveTo(100, 100);

// 画直线到目标点
path.lineTo(200, 200);

// 闭合路径(连接终点到起点)
path.close();

// 重置路径
path.reset();
// 或保留内部数据结构只清除内容(性能更好)
path.rewind();

矩形、圆角矩形、椭圆、圆

// 矩形
path.addRect(left, top, right, bottom, Path.Direction.CW);

// 圆角矩形
path.addRoundRect(left, top, right, bottom, rx, ry, Path.Direction.CW);
// 或使用 RectF
path.addRoundRect(rectF, radii, Path.Direction.CW); // radii 控制每个角的半径

// 椭圆 / 圆
path.addOval(rectF, Path.Direction.CW); // 如果 rectF 是正方形则为圆
path.addCircle(cx, cy, radius, Path.Direction.CW);

Path.Direction 控制路径的方向:

  • CW(Clockwise):顺时针。当使用 Path.FillType.EVEN_ODD 时影响填充结果。
  • CCW(Counter-Clockwise):逆时针。

弧线:arcTo

/**
* startAngle: 起始角度(3 点钟方向为 0,顺时针为正)
* sweepAngle: 扫过的角度
* forceMoveTo: 是否强制 moveTo 到弧线起点
*/
path.arcTo(rectF, startAngle, sweepAngle);
path.arcTo(rectF, startAngle, sweepAngle, forceMoveTo);

forceMoveTo = true:在弧线起点添加 moveTo,类似于创建一个独立的弧。forceMoveTo = false:从当前位置连线到弧线起点再画弧,保证路径连续。

贝塞尔曲线:quadTo 与 cubicTo

贝塞尔曲线是计算机图形学中最常用的曲线描述方式。Path 支持二次和三次贝塞尔曲线。

quadTo(二次贝塞尔曲线)

需要一个控制点和一个终点:

/**
* x1, y1: 控制点
* x2, y2: 终点
*/
path.moveTo(startX, startY);
path.quadTo(controlX, controlY, endX, endY);

二次贝塞尔曲线的参数方程:

B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2

其中 P0 是起点,P1 是控制点,P2 是终点,t 范围 [0, 1]。

cubicTo(三次贝塞尔曲线)

需要两个控制点和一个终点:

/**
* x1, y1: 控制点 1
* x2, y2: 控制点 2
* x3, y3: 终点
*/
path.moveTo(startX, startY);
path.cubicTo(cp1x, cp1y, cp2x, cp2y, endX, endY);

三次贝塞尔曲线的参数方程:

B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1 + 3(1-t) * t^2 * P2 + t^3 * P3

三次贝塞尔曲线比二次多一个控制点,可以描述更复杂的曲线形状(如 S 形曲线)。

PathMeasure:路径测量与动画

PathMeasure 是 Path 的测量工具,可以获取路径上任意位置的点坐标和切线方向,是实现路径动画的核心。

源码位置:frameworks/base/graphics/java/android/graphics/PathMeasure.java

// 创建 PathMeasure
PathMeasure pathMeasure = new PathMeasure(path, false); // forceClosed 是否强制闭合

// 获取路径总长度
float length = pathMeasure.getLength();

// 获取路径上某位置的点坐标和切线
float[] pos = new float[2];
float[] tan = new float[2];
boolean success = pathMeasure.getPosTan(distance, pos, tan);

// 获取路径片段
PathMeasure pathMeasure2 = new PathMeasure();
pathMeasure.getSegment(startDistance, stopDistance, dstPath, startWithMoveTo);

// 获取路径的变换矩阵
Matrix matrix = new Matrix();
pathMeasure.getMatrix(distance, matrix, PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);

路径追踪动画示例

利用 ValueAnimator 驱动 distance 参数,实现物体沿路径运动:

PathMeasure pathMeasure = new PathMeasure(path, false);
float pathLength = pathMeasure.getLength();
float[] pos = new float[2];
float[] tan = new float[2];

ValueAnimator animator = ValueAnimator.ofFloat(0f, pathLength);
animator.setDuration(3000);
animator.addUpdateListener(animation -> {
float distance = (float) animation.getAnimatedValue();
pathMeasure.getPosTan(distance, pos, tan);
// 更新物体位置和旋转角度
float degrees = (float) Math.toDegrees(Math.atan2(tan[1], tan[0]));
objectView.setTranslationX(pos[0]);
objectView.setTranslationY(pos[1]);
objectView.setRotation(degrees);
});
animator.start();

PathEffect:路径效果

PathEffect 用于修改 Path 的绘制外观(如虚线、圆角)。它是 Paint 的一个属性,设置后所有 Path 的绘制都会应用该效果。

// 虚线效果
PathEffect dashEffect = new DashPathEffect(
new float[]{20, 10}, // intervals: 实线长度20px, 空白长度10px
0 // phase: 起始偏移
);
paint.setPathEffect(dashEffect);

// 圆角效果
PathEffect cornerEffect = new CornerPathEffect(radius);

// 离散效果(模拟手绘线条)
PathEffect discreteEffect = new DiscretePathEffect(segmentLength, deviation);

// 组合效果
PathEffect composeEffect = new ComposePathEffect(dashEffect, cornerEffect);
// 或使用 SumPathEffect 叠加两个效果
PathEffect sumEffect = new SumPathEffect(dashEffect, cornerEffect);

PorterDuff Xfermode:像素混合模式

Xfermode 控制新绘制内容(src)与画布已有内容(dst)之间的像素混合规则。最常用的是 PorterDuff 的 18 种混合模式。

基本使用

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

源码位置:frameworks/base/graphics/java/android/graphics/PorterDuffXfermode.java

PorterDuff 的名字来源于 Thomas Porter 和 Tom Duff,他们在 1984 年发表了关于数字图像合成的基础论文。Android 的 PorterDuff 实现遵循这一经典算法。

18 种混合模式分类

Alpha 合成模式(基础)

  • CLEAR:[0, 0] — 清除所有像素,src 和 dst 都不可见。
  • SRC:[Sa, Sc] — 只显示 src。
  • DST:[Da, Dc] — 只显示 dst。
  • SRC_OVER:[Sa + (1 - Sa) * Da, Sc + (1 - Sa) * Dc] — src 在 dst 上方(默认行为)。
  • DST_OVER:[Da + (1 - Da) * Sa, Dc + (1 - Da) * Sc] — dst 在 src 上方。
  • SRC_IN:[Sa * Da, Sc * Da] — 只在 dst 区域内的 src 可见。常用于圆角裁剪。
  • DST_IN:[Da * Sa, Dc * Sa] — 只在 src 区域内的 dst 可见。常用于遮罩效果。
  • SRC_OUT:[Sa * (1 - Da), Sc * (1 - Da)] — 只在 dst 区域外的 src 可见。
  • DST_OUT:[Da * (1 - Sa), Dc * (1 - Sa)] — 只在 src 区域外的 dst 可见。
  • SRC_ATOP:[Da, Sc * Da + (1 - Sa) * Dc] — src 绘制在 dst 内部,dst 保留在 src 外部。
  • DST_ATOP:[Sa, Dc * Sa + (1 - Da) * Sc] — dst 绘制在 src 内部,src 保留在 dst 外部。
  • XOR:[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] — src 和 dst 重叠区域不可见。

混合模式(较少使用)

  • ADD — 饱和度相加。
  • MULTIPLY — 正片叠底。
  • SCREEN — 滤色。
  • OVERLAY — 叠加。
  • DARKEN — 变暗。
  • LIGHTEN — 变亮。

实际应用场景

SRC_IN:圆角/圆形图片裁剪

// 创建圆形遮罩
Bitmap maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas maskCanvas = new Canvas(maskBitmap);
Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
maskCanvas.drawCircle(width/2f, height/2f, width/2f, maskPaint);

// 使用 SRC_IN 裁剪原图
Paint xferPaint = new Paint();
int layerId = canvas.saveLayer(0, 0, width, height, null);
canvas.drawBitmap(srcBitmap, 0, 0, null);
xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(maskBitmap, 0, 0, xferPaint);
xferPaint.setXfermode(null);
canvas.restoreToCount(layerId);

DST_IN:遮罩效果

// 先绘制目标内容
canvas.drawBitmap(dstBitmap, 0, 0, null);
// 再绘制遮罩形状,使用 DST_IN
Paint xferPaint = new Paint();
xferPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(maskShapeBitmap, 0, 0, xferPaint);

CLEAR:擦除效果(刮刮卡)

// 手指滑动时清除上层内容,露出底层
Paint clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
clearPaint.setStyle(Paint.Style.STROKE);
clearPaint.setStrokeWidth(40);
clearPaint.setStrokeCap(Paint.Cap.ROUND);

// 在手指路径上绘制透明线条
canvas.drawPath(fingerPath, clearPaint);

离屏缓冲(saveLayer)的必要性

使用 Xfermode 时,通常需要先调用 canvas.saveLayer() 创建一个离屏缓冲(offscreen buffer),在该缓冲中完成混合后再绘制回主画布。如果不使用 saveLayer,PorterDuff 的操作对象是主画布上已有的所有内容,可能导致意外效果(例如 CLEAR 模式会清除整个 Activity 的背景)。

int saveCount = canvas.saveLayer(0, 0, width, height, null);
// ... Xfermode 绘制操作 ...
canvas.restoreToCount(saveCount);

ColorFilter:颜色过滤器

ColorFilter 用于在绘制时修改像素颜色。源码位置:frameworks/base/graphics/java/android/graphics/ColorFilter.java

LightingColorFilter(光照颜色过滤器)

/**
* mul: 颜色乘法因子(0xRRGGBB)
* add: 颜色加法因子(0xRRGGBB)
* 输出 = 输入 * mul + add
*/
ColorFilter filter = new LightingColorFilter(0xFFFFFF, 0x000000); // 不做任何改变
ColorFilter redFilter = new LightingColorFilter(0xFF0000, 0x000000); // 只保留红色通道
ColorFilter brighten = new LightingColorFilter(0xFFFFFF, 0x222222); // 整体变亮

详细计算:对于每个像素的 R、G、B 分量分别执行 out = in * mul + add(范围钳制在 [0, 255])。

PorterDuffColorFilter(PorterDuff 颜色过滤器)

ColorFilter filter = new PorterDuffColorFilter(
Color.RED, // 源颜色
PorterDuff.Mode.SRC_ATOP // 混合模式
);
paint.setColorFilter(filter);

将指定的颜色以 PorterDuff 模式混合到绘制的像素上。

ColorMatrix(颜色矩阵)

ColorMatrix 是一个 4x5 矩阵,可以实现任意的颜色变换:

[ R' ]   [ a  b  c  d  e ] [ R ]
[ G' ] [ f g h i j ] [ G ]
[ B' ] = [ k l m n o ] [ B ]
[ A' ] [ p q r s t ] [ A ]
[ 1 ]
ColorMatrix colorMatrix = new ColorMatrix();

// 去饱和度(灰度图)
colorMatrix.setSaturation(0f);

// 旋转色相
colorMatrix.setRotate(0, 180); // 红色通道旋转 180 度

// 缩放颜色
colorMatrix.setScale(1.5f, 1f, 1f, 1f); // 红色通道增强 50%

ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
paint.setColorFilter(filter);

常用 ColorMatrix 预设

// 灰度图
ColorMatrix grayMatrix = new ColorMatrix();
grayMatrix.setSaturation(0f);

// 反转颜色(底片效果)
float[] invert = {
-1f, 0f, 0f, 0f, 255f,
0f, -1f, 0f, 0f, 255f,
0f, 0f, -1f, 0f, 255f,
0f, 0f, 0f, 1f, 0f
};
ColorMatrix invertMatrix = new ColorMatrix(invert);

// 老照片效果(降低饱和度 + 添加暖色调)
ColorMatrix sepiaMatrix = new ColorMatrix();
sepiaMatrix.setSaturation(0f);
// 再叠加暖色偏移...

MaskFilter:遮罩滤镜

MaskFilter 在绘制完成后对像素应用额外的变换。目前 Android 只提供了 BlurMaskFilter。

BlurMaskFilter

/**
* radius: 模糊半径
* style:
* BlurMaskFilter.Blur.NORMAL — 内外都模糊
* BlurMaskFilter.Blur.SOLID — 外部模糊,内部实色
* BlurMaskFilter.Blur.OUTER — 仅外部模糊
* BlurMaskFilter.Blur.INNER — 仅内部模糊
*/
MaskFilter blurFilter = new BlurMaskFilter(10f, BlurMaskFilter.Blur.NORMAL);
paint.setMaskFilter(blurFilter);

注意:BlurMaskFilter 在硬件加速下不被支持(API 14+ 硬件加速时不生效)。如需在硬件加速下使用模糊效果,需要使用 RenderScript 或自定义 Shader。

Canvas 状态管理

save / restore 机制

Canvas 内部维护一个状态栈。每次 save() 会将当前的变换矩阵(Matrix)和裁剪区域(Clip)入栈,restore() 将其出栈恢复。

canvas.save();        // 保存状态到栈
canvas.translate(100, 100);
canvas.rotate(45);
canvas.drawRect(0, 0, 50, 50, paint); // 在旋转后的坐标系中绘制
canvas.restore(); // 恢复状态

// 此时坐标系回到 save 之前的状态
canvas.drawRect(0, 0, 50, 50, paint); // 在原始坐标系中绘制

save() 返回一个 saveCount(栈深度),可以传给 restoreToCount() 恢复到指定的保存点:

int saveCount = canvas.save();
canvas.save();
canvas.saveLayer(...);
// ... 各种操作 ...
canvas.restoreToCount(saveCount); // 恢复到最初状态

clipRect / clipPath / clipOutRect

裁剪区域限制了后续绘制操作的作用范围:

// 矩形裁剪
canvas.clipRect(left, top, right, bottom);
canvas.clipRect(rectF);
canvas.clipRect(rectF, Region.Op.INTERSECT); // 区域操作

// 路径裁剪
canvas.clipPath(circlePath);

// 排除区域(API 26+)
canvas.clipOutRect(left, top, right, bottom);
canvas.clipOutPath(path);

Region.Op 定义了裁剪区域的逻辑运算:

  • INTERSECT(默认):取交集。
  • DIFFERENCE:取差集(从当前裁剪区域中减去)。
  • REPLACE:替换。
  • UNION:取并集。
  • XOR:取对称差。
  • REVERSE_DIFFERENCE:反向差集。

裁剪的实用场景

// 实现圆形裁剪
Path circlePath = new Path();
circlePath.addCircle(centerX, centerY, radius, Path.Direction.CW);
canvas.clipPath(circlePath);
// 之后的所有绘制都只出现在圆形区域内
canvas.drawBitmap(bitmap, 0, 0, null);

// 排除某个区域不绘制
canvas.clipOutRect(excludeRect);
canvas.drawColor(overlayColor); // overlayColor 不会覆盖 excludeRect 区域

文本渲染

drawText 基础

canvas.drawText(text, x, y, paint);

xy 定义了文本基线的起点位置。注意 y 是基线(baseline)的 y 坐标,不是文本顶部的坐标。

Paint 文本相关属性

paint.setTextSize(spToPx(14));
paint.setTypeface(Typeface.DEFAULT_BOLD); // 字体
paint.setFakeBoldText(true); // 伪粗体
paint.setUnderlineText(true); // 下划线
paint.setStrikeThruText(true); // 删除线
paint.setTextSkewX(-0.25f); // 水平倾斜(斜体效果)
paint.setTextScaleX(1.2f); // 水平缩放
paint.setLetterSpacing(0.1f); // 字母间距(API 21+)
paint.setTextAlign(Paint.Align.CENTER); // 文本对齐方式:LEFT / CENTER / RIGHT

FontMetrics:精确控制文本位置

Paint.FontMetrics fm = paint.getFontMetrics();
// fm.top: 基线到字符顶部的最大距离(负值)
// fm.ascent: 基线到字符顶部的推荐距离(负值)
// fm.descent: 基线到字符底部的推荐距离(正值)
// fm.bottom: 基线到字符底部的最大距离(正值)
// fm.leading: 行间距

// 计算文本居中绘制时的 y 坐标
float textY = centerY - (fm.ascent + fm.descent) / 2f;

breakText:文本换行测量

int breakCount = paint.breakText(
text, // 源文本
start, // 起始位置
end, // 结束位置
measureForwards,// 是否正向测量
maxWidth, // 最大宽度
measuredWidth // 输出:实际测量的宽度
);

breakText 返回在给定宽度内可以放置的字符数。这在实现自动换行的文本绘制时非常有用。

StaticLayout:多行文本布局

对于需要换行的长文本,使用 StaticLayout 比手动调用 breakText 更便捷:

StaticLayout staticLayout = StaticLayout.Builder
.obtain(text, start, end, textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.build();

// 绘制
canvas.save();
canvas.translate(x, y);
staticLayout.draw(canvas);
canvas.restore();

// 获取行信息
int lineCount = staticLayout.getLineCount();
int lineTop = staticLayout.getLineTop(lineIndex);
int lineBottom = staticLayout.getLineBottom(lineIndex);

StaticLayout 内部处理了换行、对齐、行间距等细节。

DynamicLayout:可编辑文本布局

DynamicLayout 继承自 Layout,用于 EditText 等可编辑文本控件。当文本内容变化时,DynamicLayout 会增量更新布局信息,而非完全重建。

DynamicLayout dynamicLayout = new DynamicLayout(
text, textPaint, width,
Layout.Alignment.ALIGN_NORMAL,
lineSpacingMultiplier, lineSpacingExtra,
includePadding
);
// 当文本变化时
dynamicLayout.getText().replace(start, end, newText);

完整示例一:音频波形可视化器

该示例演示如何使用 Path、Paint 和动画属性绘制实时音频波形。

public class AudioWaveformView extends View {
private Paint mWavePaint;
private Path mWavePath;
private float[] mWaveData = new float[0];
private int mWaveColor = Color.parseColor("#FF4081");
private float mStrokeWidth;

public AudioWaveformView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWavePaint.setStyle(Paint.Style.STROKE);
mWavePaint.setStrokeWidth(dpToPx(2));
mWavePaint.setStrokeCap(Paint.Cap.ROUND);
mWavePaint.setStrokeJoin(Paint.Join.ROUND);
mWavePaint.setColor(mWaveColor);
mWavePath = new Path();
mStrokeWidth = dpToPx(2);
}

/**
* 更新波形数据,范围 [-1, 1]
*/
public void setWaveData(float[] data) {
mWaveData = data;
invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (mWaveData == null || mWaveData.length == 0) {
return;
}

int width = getWidth();
int height = getHeight();
float centerY = height / 2f;
float amp = height / 2f - mStrokeWidth; // 振幅,留出描边空间

// 构建波形路径 — 使用三次贝塞尔曲线使波形平滑
mWavePath.rewind();
mWavePath.moveTo(0, centerY);

float stepX = (float) width / (mWaveData.length - 1);
for (int i = 0; i < mWaveData.length - 1; i++) {
float x1 = i * stepX;
float y1 = centerY - mWaveData[i] * amp;
float x2 = (i + 1) * stepX;
float y2 = centerY - mWaveData[i + 1] * amp;

// 用二次贝塞尔曲线连接相邻采样点,实现平滑过渡
float midX = (x1 + x2) / 2f;
mWavePath.quadTo(x1, y1, midX, (y1 + y2) / 2f);
}

canvas.drawPath(mWavePath, mWavePaint);

// 绘制中线
Paint centerLinePaint = new Paint();
centerLinePaint.setColor(Color.parseColor("#33000000"));
centerLinePaint.setStrokeWidth(1);
canvas.drawLine(0, centerY, width, centerY, centerLinePaint);
}

private float dpToPx(float dp) {
return dp * getContext().getResources().getDisplayMetrics().density;
}
}

完整示例二:圆角图片组件(BitmapShader 实现)

这个示例展示了如何使用 BitmapShader 实现带边框的圆角矩形图片。

public class RoundImageView extends AppCompatImageView {
private Paint mBitmapPaint;
private Paint mBorderPaint;
private BitmapShader mBitmapShader;
private Matrix mShaderMatrix;

private float mCornerRadius;
private float mBorderWidth;
private int mBorderColor;

private RectF mBounds;
private Path mClipPath;

public RoundImageView(Context context, AttributeSet attrs) {
super(context, attrs);

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
mCornerRadius = ta.getDimension(R.styleable.RoundImageView_roundRadius, dpToPx(12));
mBorderWidth = ta.getDimension(R.styleable.RoundImageView_borderWidth, 0);
mBorderColor = ta.getColor(R.styleable.RoundImageView_borderColor, Color.TRANSPARENT);
ta.recycle();

mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBitmapPaint.setFilterBitmap(true);

mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setColor(mBorderColor);

mShaderMatrix = new Matrix();
mBounds = new RectF();
mClipPath = new Path();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBounds.set(0, 0, w, h);
updateClipPath();
updateShader();
}

private void updateClipPath() {
mClipPath.reset();
mClipPath.addRoundRect(mBounds, mCornerRadius, mCornerRadius, Path.Direction.CW);
}

private void updateShader() {
Drawable drawable = getDrawable();
if (drawable == null) return;

Bitmap bitmap = drawableToBitmap(drawable);
if (bitmap == null) return;

mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

// 计算缩放矩阵使图片居中并铺满
float scale;
float dx = 0, dy = 0;
float viewWidth = getWidth() - mBorderWidth * 2;
float viewHeight = getHeight() - mBorderWidth * 2;

if (bitmap.getWidth() * viewHeight > viewWidth * bitmap.getHeight()) {
// 按高度缩放
scale = viewHeight / (float) bitmap.getHeight();
dx = (viewWidth - bitmap.getWidth() * scale) * 0.5f;
} else {
// 按宽度缩放
scale = viewWidth / (float) bitmap.getWidth();
dy = (viewHeight - bitmap.getHeight() * scale) * 0.5f;
}

mShaderMatrix.setScale(scale, scale);
mShaderMatrix.postTranslate(
(int)(dx + 0.5f) + mBorderWidth,
(int)(dy + 0.5f) + mBorderWidth
);
mBitmapShader.setLocalMatrix(mShaderMatrix);
mBitmapPaint.setShader(mBitmapShader);
}

@Override
protected void onDraw(Canvas canvas) {
// 在 clipPath 内绘制图片
canvas.save();
canvas.clipPath(mClipPath);
super.onDraw(canvas);
canvas.restore();

// 绘制边框
if (mBorderWidth > 0) {
float halfBorder = mBorderWidth / 2f;
RectF borderRect = new RectF(
mBounds.left + halfBorder,
mBounds.top + halfBorder,
mBounds.right - halfBorder,
mBounds.bottom - halfBorder
);
Path borderPath = new Path();
borderPath.addRoundRect(
borderRect,
mCornerRadius - halfBorder,
mCornerRadius - halfBorder,
Path.Direction.CW
);
canvas.drawPath(borderPath, mBorderPaint);
}
}

@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
updateShader();
invalidate();
}

@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
updateShader();
invalidate();
}

private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, width, height);
drawable.draw(canvas);
return bitmap;
}

private float dpToPx(float dp) {
return dp * getContext().getResources().getDisplayMetrics().density;
}
}

完整示例三:刮刮卡效果(Xfermode CLEAR 模式)

利用 PorterDuff.Mode.CLEAR 实现刮刮卡效果:手指在灰色涂层上滑动时擦除涂层,露出底层的内容(如抽奖结果)。

public class ScratchCardView extends View {
private Paint mOverlayPaint;
private Paint mClearPaint;
private Paint mTextPaint;

private Bitmap mOverlayBitmap;
private Canvas mOverlayCanvas;
private Path mFingerPath;

private String mPrizeText = "恭喜中奖!";
private boolean mRevealed = false;

public ScratchCardView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
// 涂层画笔
mOverlayPaint = new Paint();
mOverlayPaint.setColor(Color.parseColor("#CCCCCC"));

// 清除画笔(Xfermode CLEAR)
mClearPaint = new Paint();
mClearPaint.setAlpha(0);
mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mClearPaint.setStyle(Paint.Style.STROKE);
mClearPaint.setStrokeWidth(dpToPx(36));
mClearPaint.setStrokeCap(Paint.Cap.ROUND);
mClearPaint.setStrokeJoin(Paint.Join.ROUND);
mClearPaint.setAntiAlias(true);

// 奖品文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(spToPx(24));
mTextPaint.setColor(Color.parseColor("#FF5722"));
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setFakeBoldText(true);

mFingerPath = new Path();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

// 创建涂层 Bitmap
mOverlayBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mOverlayCanvas = new Canvas(mOverlayBitmap);
mOverlayCanvas.drawColor(Color.parseColor("#CCCCCC"));
// 可以添加提示文字
Paint hintPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
hintPaint.setTextSize(spToPx(16));
hintPaint.setColor(Color.WHITE);
hintPaint.setTextAlign(Paint.Align.CENTER);
hintPaint.setAlpha(180);
mOverlayCanvas.drawText("刮开有惊喜", w / 2f, h / 2f + spToPx(8), hintPaint);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 绘制底层的奖品内容
Paint bgPaint = new Paint();
bgPaint.setColor(Color.WHITE);
canvas.drawRect(0, 0, getWidth(), getHeight(), bgPaint);

float centerY = getHeight() / 2f;
Paint.FontMetrics fm = mTextPaint.getFontMetrics();
float textY = centerY - (fm.ascent + fm.descent) / 2f;
canvas.drawText(mPrizeText, getWidth() / 2f, textY, mTextPaint);

// 绘制涂层(手指划过区域已被清除)
canvas.drawBitmap(mOverlayBitmap, 0, 0, null);

// 在离屏缓冲中绘制手指路径(CLEAR 模式)
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
canvas.drawPath(mFingerPath, mClearPaint);
canvas.restoreToCount(layerId);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mFingerPath.reset();
mFingerPath.moveTo(x, y);
// 在 overlay bitmap 上也绘制,持续持久化擦除效果
mOverlayCanvas.drawPath(mFingerPath, mClearPaint);
break;

case MotionEvent.ACTION_MOVE:
mFingerPath.lineTo(x, y);
mOverlayCanvas.drawPath(mFingerPath, mClearPaint);
break;

case MotionEvent.ACTION_UP:
mFingerPath.lineTo(x, y);
mOverlayCanvas.drawPath(mFingerPath, mClearPaint);
// 检查已擦除比例
checkRevealedPercentage();
break;
}
invalidate();
return true;
}

/**
* 检测已擦除的像素比例,超过阈值则自动全部揭开
*/
private void checkRevealedPercentage() {
if (mRevealed) return;

int totalPixels = mOverlayBitmap.getWidth() * mOverlayBitmap.getHeight();
int transparentPixels = 0;
int[] pixels = new int[totalPixels];
mOverlayBitmap.getPixels(pixels, 0, mOverlayBitmap.getWidth(), 0, 0,
mOverlayBitmap.getWidth(), mOverlayBitmap.getHeight());

for (int pixel : pixels) {
if (pixel == 0) { // 完全透明
transparentPixels++;
}
}

float revealedPercent = (float) transparentPixels / totalPixels;
if (revealedPercent > 0.5f) {
// 超过 50% 已擦除,自动揭开全部涂层
mRevealed = true;
mOverlayBitmap.eraseColor(Color.TRANSPARENT);
invalidate();
}
}

/**
* 重置刮刮卡
*/
public void reset(String prizeText) {
mPrizeText = prizeText;
mRevealed = false;
mFingerPath.reset();
if (mOverlayCanvas != null) {
mOverlayCanvas.drawColor(Color.parseColor("#CCCCCC"));
}
invalidate();
}

private float dpToPx(float dp) {
return dp * getContext().getResources().getDisplayMetrics().density;
}

private float spToPx(float sp) {
return sp * getContext().getResources().getDisplayMetrics().scaledDensity;
}
}

总结

Paint 和 Canvas 是 Android 自定义绘制的两个核心类,掌握它们的高级特性后可以实现几乎任何视觉效果:

  1. Shader:决定绘制内容的填充图案。LinearGradient、RadialGradient、SweepGradient 实现渐变,BitmapShader 实现图片填充,ComposeShader 实现组合效果。
  2. Path:描述任意复杂的二维路径。quadTo/cubicTo 实现贝塞尔曲线,PathMeasure 实现路径测量和追踪动画。
  3. Xfermode:控制新旧像素的混合。SRC_IN 用于裁剪,DST_IN 用于遮罩,CLEAR 用于擦除。注意配合 saveLayer 使用。
  4. ColorFilter:修改绘制像素的颜色。LightingColorFilter 实现亮度调整,ColorMatrix 实现饱和度、色相变换。
  5. MaskFilter:对绘制结果应用模糊等后期滤镜。

性能注意事项:避免在 onDraw 中创建对象;硬件加速下部分 API(如 BlurMaskFilter)不可用;合理使用 saveLayer 控制 Xfermode 的作用范围。

关键源码文件

文件 路径
Paint.java frameworks/base/graphics/java/android/graphics/Paint.java
Canvas.java frameworks/base/graphics/java/android/graphics/Canvas.java
Shader.java frameworks/base/graphics/java/android/graphics/Shader.java
Path.java frameworks/base/graphics/java/android/graphics/Path.java
PathMeasure.java frameworks/base/graphics/java/android/graphics/PathMeasure.java
PorterDuffXfermode.java frameworks/base/graphics/java/android/graphics/PorterDuffXfermode.java
PorterDuff.java frameworks/base/graphics/java/android/graphics/PorterDuff.java
ColorFilter.java frameworks/base/graphics/java/android/graphics/ColorFilter.java
ColorMatrix.java frameworks/base/graphics/java/android/graphics/ColorMatrix.java
MaskFilter.java frameworks/base/graphics/java/android/graphics/MaskFilter.java
打赏
  • 微信
  • 支付宝

评论