Canvas中的绘图师讲解与实战——Android高级UI

5,753 阅读14分钟

目录
一、前言
二、如何画好一幅图
三、Canvas的图形API
四、画布保存状态API
五、实战——时钟与指针
六、写在最后

一、前言

在上一篇文章中,我们只是分享了裁剪类型的API,今天接着分享绘图部分API。话不多说,老规矩,先上实战图。

时钟与指针

二、如何画好一幅图

我们在上一篇文章中讲到了,绘制一幅图的工具和坐标系。我们继续思考,在现实中使用一张纸绘制时,我们会对这张纸进行旋转一定角度来方便自己绘制,有时为了绘制一些细节,会进行放大,有时也会进行移动这张纸。而这些操作,在canvas中也有各自对应的操作。

1、rotate 旋转

(1)第一个rotate函数

public void rotate(float degrees)

描述: 以原点为旋转中心,旋转画布 degrees 角度。正数为顺时针旋转,负数为逆时针旋转。

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.rotate(30);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为旋转后绘制的图。

(2)第二个rotate函数

public final void rotate(float degrees, float px, float py)

描述: 以 (px, py) 为旋转中心,将画布旋转 degrees 角度。正数为顺时针旋转,负数为逆时针旋转。

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.rotate(30, 200, 300);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为旋转后绘制的图。

2、scale 缩放

(1)第一个scale函数

public void scale(float sx, float sy)

描述 : 以原点进行缩放画布,x轴缩放 sx 倍,y轴缩放 sy 倍。

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.scale(0.5f,0.33f);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为缩放后绘制的图。 (2)第二个scale函数

public final void scale(float sx, float sy, float px, float py)

描述: 以 (px, py) 进行缩放画布,x轴缩放 sx 倍,y轴缩放 sy 倍。

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.scale(0.5f, 0.33f, 200, 300);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为缩放后绘制的图。

3、skew 斜切

public void skew(float sx, float sy)

描述: 进行 x 轴和 y轴 的拉伸。

拉伸规则 当一个点为(x, y)时,进行斜切变换(sx, sy),得到的结果 (rx, ry)

  1. rx = x + sx * y;
  2. ry = y + sy * x;

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.skew(1, 0.5f);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为斜切后绘制的图。

可以使用上面的 “拉伸规则” ,将红色框的点带入便可得到蓝色框对应的点。

4、translate 偏移

public void translate(float dx, float dy)

描述: 将画布水平移动 dx 个像素, 垂直移动 dy 个像素。

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

canvas.translate(100, 200);

mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为移动后绘制的图。

5、setMatrix 矩阵

public void setMatrix(@Nullable Matrix matrix)

描述: 将矩阵作用于画布。

举个例子

mPaint.setColor(Color.RED);
canvas.drawRect(mRectF, mPaint);

mMatrix.preTranslate(getWidth() / 2, getHeight() / 2);
mMatrix.preScale(2, 1);

canvas.setMatrix(mMatrix);
mPaint.setColor(Color.BLUE);
canvas.drawRect(mRectF, mPaint);

效果图

红色为原图,蓝色为使用矩阵后绘制的图。 值得一提

矩阵的内容比较多,这里只是略带一提,如果想见识见识他的真正威力,可以看看在小盆友另一篇博文放荡不羁SVG讲解与实战实战中的使用,具体代码请进传送门

三、Canvas的图形API

1、drawCircle 画圆

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

描述: 在坐标为 (cx,cy) 的地方绘制半径为 radius 的圆。

举个例子

// 在 原点处 画半径为100的圆
canvas.drawCircle(0, 0, 100, mPaint);

效果图

2、drawOval 画椭圆

(1)第一个drawOval函数

public void drawOval(@NonNull RectF oval, @NonNull Paint paint)

描述:oval 的矩形范围内,绘制椭圆。

举个例子

RectF mRectF = new RectF();
mRectF.left = -150;
mRectF.top = -150;
mRectF.right = 400;
mRectF.bottom = 150;

canvas.drawOval(mRectF, mPaint);

效果图

橘色部分则为我们绘制的椭圆,而紫色框(为了方便观看而绘制出来)则是我们的 oval 的范围。

(2)第二个drawOval函数

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

描述:左上(left,top) 和 右下(right,bottom) 形成的矩形范围内,绘制椭圆。

值得注意的是,这个方法只能在 API21 以上的版本 才能使用,所以建议使用第一个函数。

举个例子

canvas.drawOval(-150, -150, 400, 150, mPaint);

效果图

橘色部分则为我们绘制的椭圆,而紫色框(为了方便观看而绘制出来)则是我们的 oval 的范围。

两个函数效果完全一样,只是前一个函数将两个坐标点封装在 Rect 中,而后一函数展示在函数参数中。

3、drawLine 画线

(1)drawLine函数

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

描述: 在坐标 (startX, startY) 和 (stopX, stopY) 中绘制一条直线。

举个例子

canvas.drawLine(-200, -200,0, 0, mPaint);

效果图

(2)第一个drawLines函数

public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)

描述: pts数组中每四个数构成一条直线,每四个数中前两个为起始坐标,后两个为终止坐标。如果不够四个数,则这一组不进行绘制。

举个例子

private float[] pts = new float[]{
            0, -400, 200, -400, // 构成上面的线
            -300, 0, -300, 300, // 构成左边的线
            0, 400, 300, 400    // 构成右边的线
    };

canvas.drawLines(pts, mPaint);

效果图

(3)第二个drawLines函数(带偏移)

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

描述: 该方法比上一个方法多加两个参数,即偏移量和数量。偏移量offset为一时,则从pts的下标为1的地方开始进行读数,count则决定了多少个数。

举个例子

private float[] pts = new float[]{
            0, -400, 200, -400, 
            -300, 0, -300, 300, 
            0, 400, 300, 400    
    };

canvas.drawLines(pts, 2, 8, mPaint);

效果图

pts数组中,从下标为2的数字开始,每四个数构成一条线,直到下标为 10 (由8+2得来) 的数为止。第一条线为上面的线,第二条线为下面的线。

4、drawArc 画弧

(1)第一个drawArc函数

public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint)

描述: 在 oval矩形范围内,绘制 从startAngle角度 到 sweepAngle角度的圆弧。

参数说明: 1)oval:圆弧所绘的矩形范围区域。 2)startAngle:起始角度。0度时,指向为坐标系中的x轴正半轴。 3)sweepAngle:基于 startAngle 角度,扫过的角度范围,正数则按顺时针方向,负数则按逆时针方向。 4)useCenter:弧的两端是否要连接中心点。true连接中心点,false不连接中心点。 5)paint:画笔。

举个例子

RectF mRectF = new RectF();
mRectF.left = -150;
mRectF.top = -150;
mRectF.right = 400;
mRectF.bottom = 150;

canvas.drawArc(mRectF, 0, 120, true, mPaint);

效果图

橘色部分则为弧线部分,紫色则为矩形范围(为了方便查看才绘出)。

(2)第二个drawArc函数

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

描述: 该方法和上一方法功能完全一样,只是用四个 float 表示 矩形的端点。

举个例子

canvas.drawArc(-150, -150, 400, 150, 0, 120, false, mPaint);

效果图

橘色为圆弧,紫色为矩形范围

5、drawPoint 画点

(1)drawPoint函数

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

描述: 在坐标为 (x,y) 处绘制点

举个例子

mPaint.setColor(mColor1);
mPaint.setStrokeWidth(dpToPx(5));
canvas.drawPoint(100, 100, mPaint);

效果图 (2)第一个drawPoints函数

public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)

描述: pts数组中每两个数构成一个坐标(前者为x,后者为y),并在该坐标处点。

举个例子

private float[] pts = new float[]{
        0, -400,
        200, -400,
        -300, 0
};

mPaint.setColor(mColor2);
mPaint.setStrokeWidth(dpToPx(5));
canvas.drawPoints(pts, mPaint);

效果图 (3)第二个drawPoints函数(带偏移)

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

描述: 这个方法和上面的方法大致相同,唯一区别在于从下标为offset开始读取坐标,读取长度个数为count。

举个例子

private float[] pts = new float[]{
        0, -400,
        200, -400,
        -300, 0
};

mPaint.setColor(mColor2);
mPaint.setStrokeWidth(dpToPx(5));
canvas.drawPoints(pts, 1, pts.length - 1, mPaint);

效果图

6、drawRect 画矩形

(1)drawRect函数

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

描述: 在 rect 的范围内绘制矩形,两个方法的唯一区别在于第一个参数类型分别为 RectF 和 Rect。

RectF 和 Rect 的区别:

  1. 精度不同:RectF 四个点为浮点数,Rect 四个点为整型
  2. 所包含的方法不完全相同。

举个例子

RectF mRectF = new RectF();
mRectF.left = -150;
mRectF.top = -150;
mRectF.right = 400;
mRectF.bottom = 150;

canvas.drawRect(mRectF, mPaint);

效果图

(2)drawRect函数

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

描述: 在 (left,top) 和 (right,bottom) 形成的矩形范围内绘制矩形。

举个例子

canvas.drawRect(-150, -150, 400, 150, mPaint);

效果图

7、drawRoundRect 画圆角矩形

(1)第一个drawRoundRect函数

public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)

描述: 在 rect 范围内,绘制圆角矩形。

参数说明: 1)rx:水平方向的半径,下图中的橘色部分 2)ry:竖直方向的半径,下图中的红色部分 举个例子

canvas.drawRoundRect(mRectF, 80, 100, mPaint);

效果图

(2)第二个drawRoundRect函数

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

描述: 与上述的方法功能完全相同,只是绘制范围由四个浮点数进行确定。

举个例子

canvas.drawRoundRect(-150, -150, 400, 150, 100, 50, mPaint);

效果图

8、drawColor 给画布点颜色

(1)第一个drawColor函数

public void drawColor(@ColorInt int color)

描述: 给画布绘制color颜色值。

举个例子

canvas.drawColor(Color.parseColor("#ffffff"));

比较简单就不上效果图了。

(2)第二个drawColor函数

public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode) 

描述: 给画布绘制颜色,会与之前的图形有 mode 的作用。

举个例子

Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);

Matrix mMatrix = new Matrix();
mMatrix.setScale(0.25f, 0.25f);

canvas.drawBitmap(mBitmap, mMatrix, mPaint);
canvas.drawColor(Color.parseColor("#88880000"),
                PorterDuff.Mode.DST_OVER);

效果图 值得一提

我们所介绍的第一个 drawColor(@ColorInt int color) 函数,其实最后使用了 PorterDuff.Mode.SRC_OVER 模式。

至于 PorterDuff.Mode 的具体使用,请看小盆友的另一篇博文:图像操纵大师Xfermode讲解与实战

9、drawRGB 给画布点颜色

(1)drawRGB函数

public void drawRGB(int r, int g, int b)

描述: 给画布绘制颜色,按照 红(r),绿(g),蓝(b) 三原色进行组合

举个例子

canvas.drawARGB(255, 217, 142);

(2)drawARGB函数

public void drawARGB(int a, int r, int g, int b)

描述: 给画布绘制颜色,按照 透明度(a),红(r),绿(g),蓝(b) 三原色进行组合

举个例子

canvas.drawARGB(200, 255, 217, 142);

10、drawPath 绘制路径

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

描述: 将 路径path 绘制在画布上。

举个例子 这个方法使用的地方非常之多,例如我们绘制一个 “心” 形

mPaint.setColor(mColor1);
mPaint.setStyle(Paint.Style.FILL);
// 路径的构建,移步github
canvas.drawPath(mPath, mPaint);

效果图 值得一提

心形路径的构建使用了 贝塞尔曲线,对 贝塞尔曲线 有兴趣的童鞋,可以移步小盆友的另一篇博文:自带美感的贝塞尔曲线原理与实战

四、画布保存状态API

1、状态值

在进行 API 讲解前,我们需要先说明状态值,他控制着我们要保存什么信息。

  1. MATRIX_SAVE_FLAG:保存图层的 Matrix矩阵信息
  2. CLIP_SAVE_FLAG:保存裁剪信息
  3. HAS_ALPHA_LAYER_SAVE_FLAG:保存该图层的透明度
  4. FULL_COLOR_LAYER_SAVE_FLAG:完全保留该图层颜色
  5. CLIP_TO_LAYER_SAVE_FLAG:创建图层时,会把canvas(所有图层)裁剪到参数指定的范围,如果省略这个flag将导致图层开销巨大,性能不好。
  6. ALL_SAVE_FLAG:保存所有信息

敲黑板了!!! 虽然罗列了这么多,但1-5的FLAG已经全部被遗弃,只剩 ALL_SAVE_FLAG 这个FLAG

2、save

public int save()

描述: 这个函数用于保存图层状态,保存此刻的 canvas 画布的所有状态(例如:原点位置,旋转角度,一切我们对canvas的操作都被保存)。

3、saveLayer

// saveFlags 只能是 Canvas.ALL_SAVE_FLAG
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
            @Saveflags int saveFlags)
      
// saveFlags 只能是 Canvas.ALL_SAVE_FLAG      
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags)

// API21及以上才可使用
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint)
// API21及以上才可使用
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) 

描述: 该方法与 save 一样会保存进状态栈,然后通过 restorerestoreToCount 进行恢复。不同的是该方法会创建一个新的图层

这里创建的图层,我们可以类比为PS中的图层概念,存在意义是不会影响到其他图层的数据。例如我们在XFermode的博文中的刮刮卡的实战中,就有用到这一概念,否则我们需要看到的图片也会被一同清除。

4、saveLayerAlpha

// saveFlags 只能是 Canvas.ALL_SAVE_FLAG
public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags)
      
// saveFlags 只能是 Canvas.ALL_SAVE_FLAG  
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
            @Saveflags int saveFlags)

// API21及以上才可使用
public int saveLayerAlpha(@Nullable RectF bounds, int alpha) 
// API21及以上才可使用
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha)

描述:saveLayer 相同的是会存进状态栈和创建一个图层,然后通过 restorerestoreToCount 进行恢复。不同的是创建的图层是具有透明度的,而透明度由 alpha 决定,范围为 0-255。

5、恢复

// 恢复
public void restore()

// 恢复至指定的 状态栈层数
public void restoreToCount(int saveCount)

描述: 这两个方法,是将上面三种方法保存的函数进行恢复。而区别在于 restore 每次从状态栈中恢复拿出一个状态恢复,而 restoreToCount恢复到指定的状态栈层数(该层也会被出栈),这个 saveCount 参数在上面三种类型的方法调用后都会进行返回各自对应的层数。

6、小结

先举个例子汇总一下这几个方法:

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

    log(canvas);

    int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(),
            mPaint, Canvas.ALL_SAVE_FLAG);
    log(canvas);

    canvas.save();
    log(canvas);

    canvas.saveLayer(0, 0, getWidth(), getHeight(),
            mPaint, Canvas.ALL_SAVE_FLAG);
    log(canvas);

    canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(),
            50, Canvas.ALL_SAVE_FLAG);
    log(canvas);

    canvas.translate(getWidth() / 2, getHeight() / 2);
    canvas.drawRect(mRect, mPaint);

    canvas.restore();
    log(canvas);

    canvas.restoreToCount(layer);
    log(canvas);

}

private void log(Canvas canvas) {
    Log.i("canvas", "canvas count:" + canvas.getSaveCount());
}

输出结果 我们从代码和输出结果可以得出以下几个结论:

  1. 初始状态下,状态栈中便有一个默认的状态;
  2. 在不创建图层的情况下,所有操作都是作用于默认图层;
  3. 使用 restoreToCount(x) 进行恢复,会连同x层出栈;

一图胜千言:

将上面的代码转换成图,就如下效果

五、实战——时钟与指针

1、效果图

github地址:传送门

2、编程思路

我们先拆解下这幅图,其实构成的为三部分:

  1. 一个圆圈
  2. 刻度
  3. 指针

我们逐一解决:

(1)一个圆圈

这个我们信手拈来,canvas就有绘制圆的 API,我们在第三节的一小点就讲到了

canvas.drawCircle(0, 0, width / 2, mPaint);

(2)刻度

对于刻度,其实有两种画法:

  • 第一种:是听起来比较 “高大上” ,使用三角函数算出每个刻度的起始坐标和终止坐标,然后进行绘制。
  • 第二种:较为机灵,使用我们在 第二小节的第一点 介绍的 rotate 进行一点点的旋转画布,然后绘制线。

(3)指针

我们需要先构建下图中蓝色的路径作为指针,由一段圆弧和两条线构成。 构建思路:

第一步:在红色的矩形内,绘制圆弧(使用了第三小节第四点) 第二步:从圆弧的左点绘制线到图中红色顶点 第三部:从红色顶点绘制线到圆弧右点,最后关闭路径path

具体代码如下:

mPointerPath.moveTo(mPointerRadius, 0);
// 第一步
mPointerPath.addArc(mPointerRectF, 0, 180);
// 第二步
mPointerPath.lineTo(0, -width / 4);
// 第三步
mPointerPath.lineTo(mPointerRadius, 0);
mPointerPath.close();

(4)开启旋转

我们只需要通过属性动画,让指针动起来即可。而指针的旋转只需要通过让画布旋转即可,也就是用到第二小节第一点的rotate

canvas.save();
canvas.rotate(mCurAngle);

... 省略创建指针

mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mPointerColor);
canvas.drawPath(mPointerPath, mPaint);
canvas.restore();

时钟与指针 完整代码:传送门

六、写在最后

这次介绍的是canvas最为基础的API操作,但其实越为基础的东西,越容易被忽略也越是进阶中最需要的部分。这次写的时间耗时较久,主要是API较多,写demo和截图比较频繁。

如果你觉得文章对你有所帮助,请给我一个赞并关注我吧。如果发现有那些欠妥的地方,请留言区与我讨论,我们共同进步。

高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄