Skia 的核心类---深入画布SkCanvas
SkCanvas 是 Skia 的核心类之一,我们可以把它看作画布,它提供了用于绘制图形的接口。
移动画布
有一些特殊的时候,我们需要按元素相对另一个元素的定位来绘制元素(比如:子元素相对父元素的位置进行定位)
Skia 本身并不提供元素的父子关系管理,布局管理是开发者自己的事儿。
在这些时候,我们往往希望移动画布的位置,而不是把元素的相对坐标转换为绝对坐标。
Skia 为我们提供了移动画布的工具方法,如下代码所示:
// #include "include/core/SkCanvas.h"void translateCanvas(SkCanvas* canvas) {auto rect = SkRect::MakeXYWH(10, 10, 80, 80);SkPaint paint;paint.setColor(0xff00ffff);canvas->drawRect(rect, paint);canvas->translate(90, 90);paint.setColor(0xffff00ff);canvas->drawRect(rect, paint);canvas->translate(90, 90);paint.setColor(0xffffff00);canvas->drawRect(rect, paint);
}
SkCanvas 对象的 translate
方法负责移动画布,
它的两个参数分别是画布在 x方向
和在 y方向上
移动的距离。
在这段代码中,我们第一次绘制矩形时,画布的位置并没有移动,矩形被绘制在 10,10
的位置处。
第二次绘制矩形时,画布的位置被向右、向下分别移动了 90 像素,
所以,即使没有改变矩形的 x,y 坐标,最终矩形被绘制在 100,100
处(90+10)
紧接着,把画布 在现有的位置的基础上 向右、向下又分别移动了90像素,
所以,第三次绘制的矩形实际坐标应该是 200,200
(90+10+90+10)。
程序最终运行的结果如下图所示:
旋转画布
除了在垂直和水平方向移动画布之外,Skia 还支持旋转画布,如下代码所示:
void rotateCanvas(SkCanvas* canvas) {auto rect = SkRect::MakeXYWH(w/2-50, h/2-100, 100, 200);SkPaint paint;paint.setColor(0xff00ffff);canvas->drawRect(rect, paint);canvas->rotate(45, w / 2, h / 2);paint.setColor(0xffffff00);canvas->drawRect(rect, paint);
}
在这段代码中,我们首先在窗口的中心画了一个蓝色矩形,
然后让画布绕着窗口中心旋转了 45 度。
SkCanvas 对象的 rotate
方法的第一个参数为 旋转角度(正数顺时针,负数为逆时针)
,后两个参数是旋转中心坐标
。
画布旋转完成后我们又用黄色笔刷绘制了一遍这个矩形。
最终程序运行结果如下:
缩放画布
除了移动和旋转画布外,Skia还支持画布缩放,如下代码所示:
void scaleCanvas(SkCanvas* canvas) {auto rect = SkRect::MakeXYWH(10, 10, 100, 100);SkPaint paint;paint.setColor(0xff00ffff);canvas->drawRect(rect, paint);canvas->scale(2, 2);paint.setColor(0xffffff00);canvas->drawRect(rect, paint);
}
这段代码我们使用 SkCanvas
对象的 scale
方法把画布放大了两倍(分别从x轴方向和y轴方向都放大了两倍),
示例中先绘制的矩形(蓝色)大小为 100*100
,画布放大后,绘制同样的矩形(黄色)大小即变成了 200*200
。
值得注意的是随着画布的放大,画布的坐标体系也跟着放大了,黄色矩形的位置也从 10,10
变成了 20,20
。
最终程序运行结果如下图所示:
画布矩阵变换
如果想一次性为画布应用多种变换状态,那么你可以使用 SkCanvas
的矩阵变换的能力,如下代码所示:
void matrixCanvas(SkCanvas* canvas) {SkMatrix matrix;matrix.postTranslate(50, 50); //移动matrix.postRotate(8); //顺时针旋转 8 度matrix.postScale(1.5, 1.5); //放大canvas->concat(matrix);auto rect = SkRect::MakeXYWH(0, 0, 100, 100);SkPaint paint;paint.setAntiAlias(true);paint.setColor(0xff00ffff);canvas->drawRect(rect, paint);
}
此示例中用到了一个新类型 SkMatrix
(矩阵),它用于处理各种类型的二维变换,包括平移、旋转、缩放和倾斜等。
本示例中使用一个 SkMatrix
对象记录了多种变换(移动、旋转和放大),最后一次性的把这多种变换应用到画布上(这个工作是通过 SkCanvas 对象的 concat
方法完成的)。
最终程序运行结果如下图所示:
倾斜画布与保存画布状态
移动、旋转和缩放画布都是画布的基本能力,除了这些能力之外,Skia 的画布还具备倾斜能力,此能力常用于绘制元素的阴影效果。
示例代码如下所示:
void skewCanvas(SkCanvas* canvas) {auto rect = SkRect::MakeXYWH(60, 60, 80, 80);SkPaint paint;paint.setAntiAlias(true);paint.setColor(0xff00ffff);canvas->drawRect(rect, paint);canvas->save();canvas->translate(60, 140);canvas->skew(1, 0);paint.setColor(0xffff00ff); //粉色paint.setStroke(false);rect = SkRect::MakeXYWH(0, 0, 80, 80);canvas->drawRect(rect, paint);canvas->restore();canvas->translate(140, 60);canvas->skew(0, 1);paint.setColor(0xffffff00); //黄色paint.setStroke(false);rect = SkRect::MakeXYWH(0, 0, 80, 80);canvas->drawRect(rect, paint);
}
这段代码的运行结果如下图所示:
这段代码有以下三点需要注意:
保存与恢复画布状态
无论是移动画布,旋转画布还是倾斜画布,我们都希望 在改变画布状态之后再恢复画布的状态。
这时就需要用到
SkCanvas
的save
方法和restore
方法。save
方法用于保存画布的当前状态,restore
方法用于把画布恢复到之前的状态。在上面示例代码中,调用
SkCanvas
的save
方法时,画布还没有被移动和拉伸,是原始状态。之后的代码对画布执行了移动和拉伸操作。
直到调用
SkCanvas
的restore
方法后,画布状态就恢复到了原始状态。在此之后无论执行
translate
方法还是skew
方法,都是在一个原始状态的画布上执行的。如果不执行
restore
方法,那么对画布的移动和拉伸操作就是在现有画布状态上执行的。拉伸画布
示例结果看起来像立方体,其蓝色面是一个正常的矩形,粉色面和黄色面是画布在两次倾斜后绘制的内容。
SkCanvas
的skew
方法负责倾斜画布,这个方法接收两个参数,第一个参数是画布在
x 轴上的倾斜的大小
,第二个参数是画布在y 轴上的倾斜的大小
。值得注意的是,在倾斜画布之前,需要先移动画布,以
确定画布在哪个点开始倾斜
。以粉色面来看,画布从正方形的左下角(坐标:60, 140 )开始,在 x 轴上倾斜了 1。
以黄色面来看,画布从正方形的右上角(坐标:140, 60 )开始,在 y 轴上倾斜了 1。
画布倾斜之后,再在画布上绘制原有的矩形(正方形),矩形就变成了平行四边形了。
多次保存画布状态与恢复
实际应用中,可能会连续保存多次画布状态,然后再根据程序的运行情况确定恢复到哪一次保存的画布状态。
类似这种需求,你可以通过如下代码实现:
auto count = canvas->getSaveCount(); // count == 1
canvas->save();
//...
canvas->save();
//...
count = canvas->getSaveCount(); //count == 3
canvas->restoreToCount(0); //恢复到最初的状态
count = canvas->getSaveCount(); //count == 1
代码中 getSaveCount
方法用于获取当前画布持有的状态的数量。restoreToCount
方法用于恢复到指定的画布状态。
保存画布图层
除了上面介绍的保存画布状态之外,还可以把画布状态保存到一个 画布图层
中,
同时还可以为这个 画布图层
设置一个 SkPaint
对象,来控制接下来绘制工作的效果。
如下代码所示:
void saveCanvasLayer(SkCanvas* canvas) {auto rect = SkRect::MakeXYWH(20, 20, 100, 100);SkPaint paint;paint.setAntiAlias(true);paint.setColor(0xff00ffff);canvas->drawRect(rect, paint);SkRect bounds = SkRect::MakeLTRB(0, 0, 160, 160);SkPaint layerPaint;layerPaint.setAlphaf(0.5f);canvas->saveLayer(&bounds, &layerPaint);rect = SkRect::MakeXYWH(80, 80, 200, 200);paint.setColor(0xffff00ff);canvas->drawRect(rect, paint);canvas->restore();
}
这段代码的运行结果如下图所示:
在这段代码中先绘制了一个蓝色矩形作为参照,
接着使用 SkCanvas 对象的 saveLayer
方法保存了画布状态。
这个方法的第一个参数是指定的区域,第二个参数是一个 SkPaint
对象。
可以简单的认为 saveLayer
方法执行完之后,画布的绘图区域变成 bounds
指定的区域。
所有超出这个区域的内容都不会被绘制到最终的画布上。
restore
方法除了恢复画布状态之外,还负责把 saveLayer
之后绘制的内容绘制到最终的画布上。
如果不执行 restore
方法,最终画布上将不会出现那个紫色矩形的。
另外,紫色矩形宽高都是 200 ,明显大于蓝色矩形,但实际结果却小于蓝色矩形,这是因为紫色矩形只有左上角一小部分被绘制到最终画布上了。
值得注意的是,绘制这些内容到最终画布上时,使用了 SkPaint
对象,也就是说,所有内容都是以半透明的方式绘制到最终画布上的。
这非常有用,因为 SkPaint
对象可以携带很多效果,比如各种 filter
和 shader
,设置了这个 paint 对象之后,在 layer 上绘制的任何内容都会附加这个 paint 对象携带的效果。