简介
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);
paint.setARGB(255, 255, 0, 0); paint.setAlpha(128);
|
setAlpha 与 setColor 中嵌入的 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); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeJoin(Paint.Join.MITER); paint.setStrokeMiter(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(线性渐变)
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(径向渐变 / 圆形渐变)
Shader radialGradient = new RadialGradient( centerX, centerY, radius, Color.WHITE, Color.BLUE, Shader.TileMode.CLAMP );
|
常用于实现阴影光晕、按钮高光等圆形渐变效果。
SweepGradient(扫描渐变 / 扇形渐变)
Shader sweepGradient = new SweepGradient( cx, cy, Color.RED, Color.BLUE );
|
多色的扫描渐变:
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 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);
path.addRoundRect(rectF, radii, Path.Direction.CW);
path.addOval(rectF, Path.Direction.CW); path.addCircle(cx, cy, radius, Path.Direction.CW);
|
Path.Direction 控制路径的方向:
- CW(Clockwise):顺时针。当使用 Path.FillType.EVEN_ODD 时影响填充结果。
- CCW(Counter-Clockwise):逆时针。
弧线:arcTo
path.arcTo(rectF, startAngle, sweepAngle); path.arcTo(rectF, startAngle, sweepAngle, forceMoveTo);
|
forceMoveTo = true:在弧线起点添加 moveTo,类似于创建一个独立的弧。forceMoveTo = false:从当前位置连线到弧线起点再画弧,保证路径连续。
贝塞尔曲线:quadTo 与 cubicTo
贝塞尔曲线是计算机图形学中最常用的曲线描述方式。Path 支持二次和三次贝塞尔曲线。
quadTo(二次贝塞尔曲线)
需要一个控制点和一个终点:
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(三次贝塞尔曲线)
需要两个控制点和一个终点:
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 = new PathMeasure(path, false);
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}, 0 ); paint.setPathEffect(dashEffect);
PathEffect cornerEffect = new CornerPathEffect(radius);
PathEffect discreteEffect = new DiscretePathEffect(segmentLength, deviation);
PathEffect composeEffect = new ComposePathEffect(dashEffect, cornerEffect);
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);
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);
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);
canvas.restoreToCount(saveCount);
|
ColorFilter:颜色过滤器
ColorFilter 用于在绘制时修改像素颜色。源码位置:frameworks/base/graphics/java/android/graphics/ColorFilter.java。
LightingColorFilter(光照颜色过滤器)
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);
colorMatrix.setScale(1.5f, 1f, 1f, 1f);
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
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();
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);
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);
|
文本渲染
drawText 基础
canvas.drawText(text, x, y, paint);
|
x 和 y 定义了文本基线的起点位置。注意 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); paint.setTextAlign(Paint.Align.CENTER);
|
FontMetrics:精确控制文本位置
Paint.FontMetrics fm = paint.getFontMetrics();
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); }
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) { 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"));
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);
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);
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); 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) { 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 自定义绘制的两个核心类,掌握它们的高级特性后可以实现几乎任何视觉效果:
- Shader:决定绘制内容的填充图案。LinearGradient、RadialGradient、SweepGradient 实现渐变,BitmapShader 实现图片填充,ComposeShader 实现组合效果。
- Path:描述任意复杂的二维路径。quadTo/cubicTo 实现贝塞尔曲线,PathMeasure 实现路径测量和追踪动画。
- Xfermode:控制新旧像素的混合。SRC_IN 用于裁剪,DST_IN 用于遮罩,CLEAR 用于擦除。注意配合 saveLayer 使用。
- ColorFilter:修改绘制像素的颜色。LightingColorFilter 实现亮度调整,ColorMatrix 实现饱和度、色相变换。
- 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 |