如果说 SkCanvas
是画布,是所有绘图操作的提供者的话,那么 SkSurface
就是画布的容器,我们称之为表面,它负责管理画布对应的像素数据。这些像素数据可以是在内存中创建的,也可以是在 GPU 显存中创建的。
创建一个空白表面
如何创建和使用 Skia 的 SkSurface
对象,如下代码所示:
SkImageInfo imgInfo = SkImageInfo::MakeN32Premul(800, 600);
sk_sp<SkSurface> surface = SkSurfaces::Raster(imgInfo);
SkSurfaces::Raster
方法执行后,应用程序将在内存中分配指定数量的像素。
若需要使用 SkSurface
对象管理的像素,可以通过如下方法来完成工作:
SkPixmap pixmap;
surface->peekPixels(&pixmap);
auto addr = pixmap.addr();
pixmap.addr()
用于获取像素数据的内存地址,一般情况下,开发者不会使用这个地址来改变具体的某个像素的值。
因为内存中存储的像素数据是被格式化过的,并不是A,R,G,B颜色分量依次排列的数值。像素数据在内存中的排布方式与 SkSurface
的颜色空间有关。
通过如下代码,你可以获取某个具体位置的颜色:
SkColor color = pixmap.getColor(0, 0); //画布上位置 0,0 的颜色
根据现有像素数据创建表面
如果你已经拥有了像素数据,就可以直接基于你的像素数据创建 SkSurface
对象,如下代码所示:
std::vector<SkColor> surfaceMemory;
surfaceMemory.resize(w * h);
SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);
sk_sp<SkSurface> surface = SkSurfaces::WrapPixels(info, &surfaceMemory.front(), w * sizeof(SkColor));
上面代码中使用 std::vector<SkColor>
来存储像素数据。
每个像素就是一个 SkColor
类型的数据(SkColor就是 uint32_t
)。
像素容器的大小是 w * h
,(二维数组的列数 w 和行数 h ,与窗口的宽和高相同)
SkSurfaces::WrapPixels
方法基于像素数据创建 SkSurface
对象。
其中 &surfaceMemory.front()
用于获取容器内第一个像素的地址,
w * sizeof(SkColor)
是每行数据的大小。
同样道理,如果你有一个 SkBitmap
对象,就可以根据这个对象的像素数据创建表面:
SkBitmap bitmap;
bitmap.allocN32Pixels(w, h);
sk_sp<SkSurface> surface = SkSurfaces::WrapPixels(bitmap.pixmap());
上述代码使用 SkBitmap
对象的 allocN32Pixels
方法为此对象创建了指定大小的内存空间用于存储像素数据。
SkBitmap
对象的 pixmap
方法负责获取 SkBitmap
对象的像素数据的内存地址。
重置表面像素数据
如果你想批量把一个表面的像素数据全部设置为某个颜色值。
就可以用以下两个办法达到这个目的:
auto canvas = surface->getCanvas();
canvas->clear(0x66778899); //方法1
canvas->drawColor(0x22FF8899); //方法2
SkPaint paintObj;
canvas->drawPaint(paintObj); //方法3
这都是通过画布对象 SkCanvas
完成的。
也可以在源头设置所有像素的值,如下代码所示
std::vector<SkColor> surfaceMemory(w * h, 0xffff00); //初始化像素数组时,即设置好像素颜色
surfaceMemory.resize(w * h,0xffff00); //更改像素容器大小时,重置整个容器的像素颜色
覆盖表面像素数据
如果你已经有了一组像素数据,需要把这些像素写入目标表面,可以通过如下方法完成:
void surfaceWritePixels(SkSurface *surface)
{
std::vector<SkColor> srcMem(200 * 200, 0xff00ffff);
SkBitmap dstBitmap;
dstBitmap.setInfo(SkImageInfo::MakeN32Premul(200, 200));
dstBitmap.setPixels(&srcMem.front());
surface->writePixels(dstBitmap, 100, 100);
}
在这段代码中,使用 SkBitmap
类型包装了像素数据。
SkBitmap
对象的 setPixels
方法会把 srcMem
管理的像素地址拷贝到 SkBitmap
对象中(只复制了地址)
SkSurface
对象的 writePixels
方法会把 dstBitmap
对象管理的像素数据复制到自己管理的内存数据中。
复制是从 100,100
的位置开始的,由于 dstBitmap
只管理了 200×200
的像素数据,所以复制工作执行到坐标 300,300
就结束了。
程序运行的结果如下图所示:
表面与画布的互访
通过 SkSurface
对象的 getCanvas
方法得到一个与此表面有关的画布。
也可以通过 SkCanvas
对象的 getSurface
方法得到一个与此画布有关的表面。
但需要注意的是,SkCanvas
对象的 getSurface
方法并不是总能得到与之对应的 SkSurface
对象。
比如:
auto canvas = SkCanvas::MakeRasterDirect(info, &surfaceMemory.front(), 4 * w);
auto surface = canvas->getSurface();
这个 canvas
对象是手动创建的,它不依赖任何surface
,它的 getSurface
方法的返回值就是一个空指针。
虽然这个 canvas
没有与之对应的 surface
,可以通过如下方法创建一个:
SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);
auto surface = canvas->makeSurface(info);
需要注意的是,这样做会导致 canvas
对应的像素数据被复制到 surface
内(内存占用加倍)
所以要么先创建surface
再通过surface
得到canvas
,要么仅使用canvas
,不使用surface
。
尽量不要通过canvas
去创建surface
。
当一个应用中存在多个surface
的时候,往往需要合并这些surface
,
有很多办法可以完成这项任务,其中之一就是把一个 surface
绘制到另一个 canvas
中,如下代码所示:
surface->draw(canvas.get(), 0, 0);
这行代码会把 surface
管理的像素数据,绘制到 canvas
画布上(从坐标 0,0
位置开始绘制),这就达到了合并两个 surface
的效果。