Canvas中的绘图师讲解与实战——Android高级UI
目录
一、前言
二、如何画好一幅图
三、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);
效果图
红色为原图,蓝色为旋转后绘制的图。
image
(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);
效果图
红色为原图,蓝色为旋转后绘制的图。
image
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);
效果图
红色为原图,蓝色为缩放后绘制的图。
image
3、skew 斜切
public void skew(float sx, float sy)
描述: 进行 x 轴和 y轴 的拉伸。
拉伸规则 当一个点为(x, y)时,进行斜切变换(sx, sy),得到的结果 (rx, ry)
- rx = x + sx * y;
- 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);
效果图
红色为原图,蓝色为斜切后绘制的图。
image可以使用上面的 “拉伸规则” ,将红色框的点带入便可得到蓝色框对应的点。
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);
效果图
红色为原图,蓝色为移动后绘制的图。
image
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 的范围。
image(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 的范围。
image两个函数效果完全一样,只是前一个函数将两个坐标点封装在 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得来) 的数为止。第一条线为上面的线,第二条线为下面的线。
image4、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);
效果图
橘色部分则为弧线部分,紫色则为矩形范围(为了方便查看才绘出)。
image(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);
效果图
橘色为圆弧,紫色为矩形范围
image5、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 的区别:
- 精度不同:RectF 四个点为浮点数,Rect 四个点为整型
- 所包含的方法不完全相同。
举个例子
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 讲解前,我们需要先说明状态值,他控制着我们要保存什么信息。
- MATRIX_SAVE_FLAG:保存图层的 Matrix矩阵信息
- CLIP_SAVE_FLAG:保存裁剪信息
- HAS_ALPHA_LAYER_SAVE_FLAG:保存该图层的透明度
- FULL_COLOR_LAYER_SAVE_FLAG:完全保留该图层颜色
- CLIP_TO_LAYER_SAVE_FLAG:创建图层时,会把canvas(所有图层)裁剪到参数指定的范围,如果省略这个flag将导致图层开销巨大,性能不好。
- 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
一样会保存进状态栈,然后通过 restore
或 restoreToCount
进行恢复。不同的是该方法会创建一个新的图层。
这里创建的图层,我们可以类比为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
相同的是会存进状态栈和创建一个图层,然后通过 restore
或 restoreToCount
进行恢复。不同的是创建的图层是具有透明度的,而透明度由 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());
}
输出结果
我们从代码和输出结果可以得出以下几个结论:
- 初始状态下,状态栈中便有一个默认的状态;
- 在不创建图层的情况下,所有操作都是作用于默认图层;
- 使用
restoreToCount(x)
进行恢复,会连同x层出栈;
一图胜千言:
将上面的代码转换成图,就如下效果
image
五、实战——时钟与指针
1、效果图
imagegithub地址:传送门
2、编程思路
我们先拆解下这幅图,其实构成的为三部分:
- 一个圆圈
- 刻度
- 指针
我们逐一解决:
(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吧😄
欢迎加我微信,我们可以进行更多更有趣的交流
image