自定义View之Canvas.drawXXX()

前言

本篇主要记录Canvas 的绘制类方法: drawXXX() 。

关于画笔、范围裁切和几何变换的详细介绍,放到之后的篇章中具体展开。

预备知识

坐标系

Android坐标系

将屏幕的左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,原点向下是Y轴正方向。

并且提供了View.getLocationInWindow()、View.getLocationOnScreen()方法来获取view在屏幕中的坐标。详见Android获得控件在屏幕中的坐标

Android坐标系

视图坐标系

相对父布局而言。

视图坐标系

如上图所示有如下几类:

  • 自身宽高(getHeight()、getWidth())
  • 相对父容器坐标(getTop()、getLeft()、getRight()、getBottom())
  • MotionEvent提供的方法(getX()、getY()、getRawX()、getRawY(),其中前两个是相对于父容器,后两个是相对于屏幕)

Canvas坐标系

Canvas坐标系指的是Canvas本身的坐标系,Canvas坐标系有且只有一个,且是唯一不变的,其坐标原点在View的左上角,从坐标原点向右为x轴的正半轴,从坐标原点向下为y轴的正半轴。

绘图坐标系

Canvas的drawXXX方法中传入的各种坐标指的都是绘图坐标系中的坐标,而非Canvas坐标系中的坐标。默认情况下,绘图坐标系与Canvas坐标系完全重合,即初始状况下,绘图坐标系的坐标原点也在View的左上角,从原点向右为x轴正半轴,从原点向下为y轴正半轴。但不同于Canvas坐标系,绘图坐标系并不是一成不变的,可以通过调用Canvas的translate方法平移坐标系,可以通过Canvas的rotate方法旋转坐标系,还可以通过Canvas的scale方法缩放坐标系,而且需要注意的是,translate、rotate、scale的操作都是基于当前绘图坐标系的,而不是基于Canvas坐标系,一旦通过以上方法对坐标系进行了操作之后,当前绘图坐标系就变化了,以后绘图都是基于更新的绘图坐标系了。也就是说,真正对我们绘图有用的是绘图坐标系而非Canvas坐标系。

drawXX系列

常见的drawXX有:drawColor()、drawCircle()、drawRect()、drawPoint()、drawOval()、drawLine()、drawRoundRect()、drawArc()、drawPath()、drawText()、drawBitmap()

接下来就逐个介绍一下Canvas的各个drawXX方法的效果。

一般这些方法在protected void onDraw(Canvas canvas)方法中进行绘制。

drawColor(@ColorInt int color)

在画布上填充(画)一层指定的颜色。
如:

1
2
3
4
5
6
    @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// canvas.drawColor(Color.parseColor("#FF0000"));
canvas.drawColor(Color.RED);
}

另一方法:drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode)

指定了PorterDuff模式,该参数的具体说明在下面章节中详细说明。这里简单提一下:该参数是设置绘画的重叠效果。

drawColor(@ColorInt int color)内部是指定为PorterDuff.Mode.SRC_OVER,这种模式的效果就是会覆盖之前的内容。

示例(盗用下别人的图):

1
drawColor(Color.parse("#88880000"); // 半透明红色

drawColor

绘制颜色的还有两个方法drawARGB(int a, int r, int g, int b)drawRGB(int r, int g, int b)

这两个方法分别是通过传入ARGB、RGB 颜色值,进行颜色填充。其中各参数的取值范围为[0,255]。

可以看到,这两个方法内部最终是调用了drawColor()方法。

1
2
3
4
5
6
7
public void drawARGB(int a, int r, int g, int b) {
drawColor(Color.argb(a, r, g, b));
}

public void drawRGB(int r, int g, int b) {
drawColor(Color.rgb(r, g, b));
}

drawCircle(float cx, float cy, float radius, @NonNull Paint paint)

以(cx,cy)坐标为圆心,radius为半径,用paint画笔画一个圆。

其中cx、cy、radius用以确定圆的位置,而第四个参数paint则是控制所画圆的效果。

比如:

一:Paint.setColor(@ColorInt int color)

设置画笔颜色

1
2
paint.setColor(Color.RED); // 设置为红色
canvas.drawCircle(300, 300, 200, paint);

paint_red

二:Paint.setStyle(Style style)

设置画笔样式,系统提供了3种:

  • Paint.Style.FILL :0,填充模式,是paint的默认模式
    Paint.Style.STROKE :1,画线模式
    Paint.Style.FILL_AND_STROKE :2,填充完后画线

所以drawCircle时,paint为STROKE样式,则画出来的是一个圆环。

1
2
paint.setStyle(Paint.Style.STROKE); // Style 修改为画线模式
canvas.drawCircle(300, 300, 200, paint);

paint_stroke

:本篇未涉及到画布的几何变换(translaterotatescaleskew),所以这边的坐标系都以当前view左上角为原点,坐标原点向右为x轴的正半轴,从坐标原点向下为y轴的正半轴,即 Canvas坐标系

三:Paint.setStrokeWidth(float width)

为STROKE模式或FILL_AND_STROKE模式设置线条的宽度。

1
2
3
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20); // 线条宽度为 20 像素
canvas.drawCircle(300, 300, 200, paint);

paint_stroke_width

四: Paint.setAntiAlias(boolean aa)

抗锯齿。让图形和文字的边缘更加平滑。

也可在 new Paint() 的时候加上一个 Paint.ANTI_ALIAS_FLAG 参数。

1
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

锯齿出现的主要原因是屏幕的分辨率过低,使得绘制的边缘出现颗粒化。抗锯齿效果是修改图形边缘处的像素颜色,从而让图形在肉眼看来具有更加平滑的感觉

/paint_anti_alias

drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)

用paint绘制指定大小的矩形。矩形的大小形状由left、top、right、bottom四个参数决定。矩形的实心、空心等属性由paint决定。


重载方法:

  • drawRect(@NonNull RectF rect, @NonNull Paint paint)
  • drawRect(@NonNull Rect r, @NonNull Paint paint)

这两个方法通过第一个参数确定所绘制的矩形的4边位置。RectF和Rect内部都维护了left、top、right、bottom四个参数。区别是RectF内的是float类型,Rect内的是int类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RectF implements Parcelable {
public float left;
public float top;
public float right;
public float bottom;
……
}

public final class Rect implements Parcelable {
public int left;
public int top;
public int right;
public int bottom;
……
}

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint)

绘制圆角矩形。

drawRect()类似,不过drawRoundRect()在paint前多传了两个参数用以确定四个圆角的弧度。

rx、ry分别与left、top、right、bottom四边形成四个矩形绘制椭圆,此椭圆的一边形成圆角矩形的圆角。

drawRect()类似,该方法还有一个重载方法drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint),通过RectF确定四边。

1
2
3
4
5
6
7
8
9
10
11
12
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStrokeWidth(6);
canvas.drawRoundRect(100, 100, 200, 300, 100, 200, paint);
canvas.drawRoundRect(300, 100, 600, 600, 100, 200, paint);
}

drawRoundRect

drawPoint(float x, float y, @NonNull Paint paint)

绘制点。

x,y确定点的位置。paint控制点的属性。

drawPoint()中常用的Paint属性有:

  • Paint.setStrokeWidth(width):设置点的大小
  • Paint.setStrokeCap(cap):设置点的形状
    • Paint.Cap.ROUND :圆头,画线的时候会长于原始长度补半圆形
    • Paint.Cap.SQUARE:方头,画线的时候会长于原始长度补方形
    • Paint.Cap.BUTT:方头,画线的时候会等于原始长度,平头

注:Paint.setStrokeCap(cap) 可以设置点的形状,但这个方法并不是专门用来设置点的形状的,而是一个设置线条端点形状的方法。端点有圆头 (ROUND)、平头 (BUTT) 和方头 (SQUARE) 三种

drawPoint()类似还有两个画点的方法:

  • drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)
  • drawPoints(@Size(multiple = 2) float[] pts, int offset, int count, @NonNull Paint paint)

这两个方法的作用是批量画点。

通过pts数组(数组长度是2的倍数)传入点的位置,两个为一组。

第二个方法中offset表示跳过pts数组中的前offset项开始记count个数,绘制count/2个点。

比如:

1
2
3
4
float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};
// 绘制四个点:(50, 50) (50, 100) (100, 50) (100, 100)
canvas.drawPoints(points, 2 /* 跳过两个数,即前两个 0 */,
8 /* 一共绘制 8 个数(4 个点)*/, paint);

drawPoints_offset

drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)

绘制椭圆。

当left、top、right、bottom四边围成的是一个正方形时,绘制出来的是圆形。

重载方法:drawOval(@NonNull RectF oval, @NonNull Paint paint)

drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint)

绘制一条直线。(startX,startY)为绘制起点,(stopX,stopY)为绘制终点,如果线条很粗,则从startY对半延伸。

绘制多条直线:

  • drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count, @NonNull Paint paint)
  • drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)

这两个方法与drawPoints()类似,不过drawLines()中的pts数组长度必须是4的倍数,用以确定每条线的起点与终点。

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

绘制弧线,用一个椭圆来描述弧形。

left, top, right, bottom 描述的是这个弧形所在的椭圆;

startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度);

sweepAngle 是弧形划过的角度;

useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。

1
2
3
4
5
6
paint.setStyle(Paint.Style.FILL); // 填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint); // 绘制扇形
canvas.drawArc(200, 100, 800, 500, 20, 100, false, paint); // 绘制弧形
paint.setStyle(Paint.Style.STROKE); // 画线模式
canvas.drawArc(200, 100, 800, 500, 130, 40, true, paint); // 绘制封口的扇形边线
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint); // 绘制不封口的弧形

drawArc

drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

绘制文字。

text 是绘制的文字内容,(x,y)是绘制的起点位置(是文本对应的baseline处,并非绘制文本的左上角),paint用以控制绘制文本的属性(比如:Paint.setTextSize(textSize),可以设置文字的大小;Paint.setTypeface(Typeface typeface),设置字体)。

FontMetrics

重载方法:

  • drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint)
  • drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint)
  • drawText(@NonNull CharSequence text, int start, int end, float x, float y, @NonNull Paint paint)

这三个方法通过参数index、count或start、end去截取text中的内容进行绘制。

绘制文本的还有两个方法:

  • drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path, float hOffset, float vOffset, @NonNull Paint paint)
  • rawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset, float vOffset, @NonNull Paint paint)

这两个方法将文本绘制在path路径上。好比是之前的baseline是条直线,现在的baseline是path这条线。

hOffset 和 vOffset是文字相对于 Path 的水平偏移量和竖直偏移量,利用它们可以调整文字的位置。

关于drawText()配合paint的其他效果,将在下篇文章中具体展开。

drawPath(@NonNull Path path, @NonNull Paint paint)

绘制path路径(自定义图像),将path内容绘制出来。

path中所涉及到的内容比较复杂,这节也会单独抽出来详细讲解path的用途。

这里简单说一下path在drawPath()中可以做哪些事:线段、二次曲线、三次曲线。

drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)

绘制 Bitmap 对象,将Bitmap的内容绘制在指定位置(left,top)。(left,top)为绘制Bitmap对象的左上角。

重载方法:

  • drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst, @Nullable Paint paint)
  • drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst, @Nullable Paint paint)
  • drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint)

前两个方法的唯一区别是dst参数的类型,这与drawRect的重载方法中的类似,这里就一起讲解其作用。

前两个方法中:

  • src 用于裁剪bitmap,drawBitmap()方法只会绘制src所限定的区域

  • dst 用于确定bitmap绘制的位置,通过src所限定的区域内的bitmap内容会显示在dst区域内,并且会通过缩放铺满dst区域。通过修改dst值,可以实现图像的移动及缩放。

第三个方法传入的matrix来绘制bitmap。drawBitmap()的其他重载方法内部是使用了Canvas当前的matrix来进行几何变换。常用的方法有pre/postTranslate/Rotate/Scale/Skew()。

关于几何变换,会在之后的篇章中详细介绍,尤其是对pre和post的几个方法组合调用先后关系的理解。

关于drawBitmap的方法还有一个:
drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
@NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
@Nullable Paint paint)

实现对 Bitmap 的各种扭曲。

方法drawBitmapMesh中的Mesh翻译过来即:网格。

将一个图片横向、纵向均匀切割成 n 份,就会形成一个「网格」,我把所有网格线的交点称为「顶点」。

正常情况下,顶点是均匀分布的。当我们改变了顶点的位置时,系统会拿偏移后的顶点坐标,和原来的坐标进行对比,通过一套算法,将图片进行扭曲,像这样:

drawBitmapMesh

方法参数说明:

  • bitmap - 需要转换的位图
  • meshWidth - 横向的格数, 0的时候不会绘制
  • meshHeight - 纵向的格数,0的时候不会绘制
  • verts - 网格顶点坐标数组,记录扭曲后图片各顶点的坐标,数组大小为 (meshWidth+1) (meshHeight+1) 2 + vertOffset
  • vertOffset - 从第几个顶点开始对位图进行扭曲,通常传 0
  • colors - 设置网格顶点的颜色,该颜色会和位图对应像素的颜色叠加,数组大小为 (meshWidth+1) * (meshHeight+1) + colorOffset,可以传 null
  • colorOffset - 从第几个顶点开始转换颜色,通常传 0
  • paint - 「画笔」,可以传 null

需要说明一下的是,可以用 colors 这个参数来实现阴影的效果,但在 API 18 以下开启了硬件加速,colors 这个参数是不起作用的

具体的使用可以查看这篇文章浪起来!使用 drawBitmapMesh 实现仿真水波纹效果

官方介绍

返回类型 Canvas.drawXXX()方法简介
void drawARGB(int a, int r, int g, int b)Fill the entire canvas’ bitmap (restricted to the current clip) with the specified ARGB color, using srcover porterduff mode.
void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)Draw the specified arc, which will be scaled to fit inside the specified oval.
void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)Draw the specified arc, which will be scaled to fit inside the specified oval.
void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)Draw the bitmap using the specified matrix.
void drawBitmap(int[] colors, int offset, int stride, float x, float y,int width, int height, boolean hasAlpha, Paint paint)This method was deprecated in API level 21. Usage with a hardware accelerated canvas requires an internal copy of color buffer contents every time this method is called. Using a Bitmap avoids this copy, and allows the application to more explicitly control the lifetime and copies of pixel data.
void drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, int height, boolean hasAlpha, Paint paint)This method was deprecated in API level 21. Usage with a hardware accelerated canvas requires an internal copy of color buffer contents every time this method is called. Using a Bitmap avoids this copy, and allows the application to more explicitly control the lifetime and copies of pixel data.
void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)Draw the specified bitmap, scaling/translating automatically to fill the destination rectangle.
void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)Draw the specified bitmap, scaling/translating automatically to fill the destination rectangle.
void drawBitmap(Bitmap bitmap, float left, float top, Paint paint)Draw the specified bitmap, with its top/left corner at (x,y), using the specified paint, transformed by the current matrix.
void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)Draw the bitmap through the mesh, where mesh vertices are evenly distributed across the bitmap.
void drawCircle(float cx, float cy, float radius, Paint paint)Draw the specified circle using the specified paint.
void drawColor(int color)Fill the entire canvas’ bitmap (restricted to the current clip) with the specified color, using srcover porterduff mode.
void drawColor(int color, PorterDuff.Mode mode)Fill the entire canvas’ bitmap (restricted to the current clip) with the specified color and porter-duff xfermode.
void drawLine(float startX, float startY, float stopX, float stopY, Paintpaint)Draw a line segment with the specified start and stop x,y coordinates, using the specified paint.
void drawLines(float[] pts, int offset, int count, Paint paint)Draw a series of lines.
void drawLines(float[] pts, Paint paint)
void drawOval(float left, float top, float right, float bottom, Paintpaint)Draw the specified oval using the specified paint.
void drawOval(RectF oval, Paint paint)Draw the specified oval using the specified paint.
void drawPaint(Paint paint)Fill the entire canvas’ bitmap (restricted to the current clip) with the specified paint.
void drawPatch(NinePatch patch, RectF dst, Paint paint)
void drawPatch(NinePatch patch, Rect dst, Paint paint)
void drawPath(Path path, Paint paint)Draw the specified path using the specified paint.
void drawPicture(Picture picture, RectF dst)Draw the picture, stretched to fit into the dst rectangle.
void drawPicture(Picture picture)Save the canvas state, draw the picture, and restore the canvas state.
void drawPicture(Picture picture, Rect dst)Draw the picture, stretched to fit into the dst rectangle.
void drawPoint(float x, float y, Paint paint)Helper for drawPoints() for drawing a single point.
void drawPoints(float[] pts, Paint paint)Helper for drawPoints() that assumes you want to draw the entire array
void drawPoints(float[] pts, int offset, int count, Paint paint)Draw a series of points.
void drawPosText(String text, float[] pos, Paint paint)This method was deprecated in API level 16. This method does not support glyph composition and decomposition and should therefore not be used to render complex scripts. It also doesn’t handle supplementary characters (eg emoji).
void drawPosText(char[] text, int index, int count, float[] pos, Paintpaint)This method was deprecated in API level 16. This method does not support glyph composition and decomposition and should therefore not be used to render complex scripts. It also doesn’t handle supplementary characters (eg emoji).
void drawRGB(int r, int g, int b)Fill the entire canvas’ bitmap (restricted to the current clip) with the specified RGB color, using srcover porterduff mode.
void drawRect(float left, float top, float right, float bottom, Paintpaint)Draw the specified Rect using the specified paint.
void drawRect(Rect r, Paint paint)Draw the specified Rect using the specified Paint.
void drawRect(RectF rect, Paint paint)Draw the specified Rect using the specified paint.
void drawRoundRect(RectF rect, float rx, float ry, Paint paint)Draw the specified round-rect using the specified paint.
void drawRoundRect(float left, float top, float right, float bottom,float rx, float ry, Paint paint)Draw the specified round-rect using the specified paint.
void drawText(CharSequence text, int start, int end, float x, float y,Paint paint)Draw the specified range of text, specified by start/end, with its origin at (x,y), in the specified Paint.
void drawText(String text, float x, float y, Paint paint)Draw the text, with origin at (x,y), using the specified paint.
void drawText(char[] text, int index, int count, float x, float y, Paintpaint)Draw the text, with origin at (x,y), using the specified paint.
void drawText(String text, int start, int end, float x, float y, Paintpaint)Draw the text, with origin at (x,y), using the specified paint.
void drawTextOnPath(String text, Path path, float hOffset, float vOffset,Paint paint)Draw the text, with origin at (x,y), using the specified paint, along the specified path.
void drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)Draw the text, with origin at (x,y), using the specified paint, along the specified path.
void drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)Draw a run of text, all in a single direction, with optional context for complex text shaping.
void drawTextRun(CharSequence text, int start, int end, int contextStart,int contextEnd, float x, float y, boolean isRtl, Paint paint)Draw a run of text, all in a single direction, with optional context for complex text shaping.
void drawVertices(Canvas.VertexMode mode, int vertexCount, float[] verts,int vertOffset, float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, int indexOffset, int indexCount, Paintpaint)Draw the array of vertices, interpreted as triangles (based on mode).

参考

Android中的坐标系统

Android 开发进阶 UI 部分 1-1:绘制基础

浪起来!使用 drawBitmapMesh 实现仿真水波纹效果

坚持原创技术分享,您的支持是对我最大的鼓励!