Android上层的作图几乎都通过Canvas实例来完成,追踪Canvas代码发现其实Canvas更多是一种接口的包装。如drawPaints ,drawPoints,drawRect,drawBitmap,而这些绘制接口是由SKia引擎来完成通过JNI来提供个Java层使用,如下图所显。
图1:Canvas绘图结构
Canvas是个2D的概念,在skia中有定义。我们可以把这个Canvas理解成系统提供给我们的一块内存区域(但实际上它只是一套绘图API,真正的内存是下面的Bitmap,Skia 提供一bitmap对象)。Graphic Buffer就是surface对应的内存,可以将这块surface上的buffer对应个Bitmap,当做一个显示设备(device)。
在之前的UI绘制博文中描述了通过独立线程在surface上可以绘制UI,通过lock来获取surface对应的数据buffer,unlockandpost来通知Surfaceflinger来输出显示。通过这样的机制,我们可以locksurface获取buffer并当做skia bitmap的内存,通过Canva的绘制API可以完成2D图形的绘制,如下图:
图2: 通过skia来绘制UI结构
本文结合一实际项目例子来介绍通过skia绘制UI的方法。
项目简介:移植之前一款linux平台软件,该软件涉及到浏览器的绘制和OSD的绘制。浏览器层与OSD层支持叠加,绘制2D图形。该款软件移植到Android平台后需要遵循Android窗口机制。根据这些要求,很显然会考虑将图形统一绘制到surfaceView的surfacebuffer上,浏览器层与OSD成统一用Skia的bitmap来提供图形绘制设备,最后叠加到surface上输出显示。
在介绍实现方法之前,先简单介绍一下SKia。Skia是一个2D绘图引擎,在Android源码 external/skia 目录里,可能各个厂家平台的实现会有一些差异,但Skia提供的绘图接口都是统一的。使用 Skia 的 API 进行图形绘制时主要会用到一下几个类:其实现代码主要在 src/core 目录下
SkBitmap 用来设置像素;
SkCanvas 写入位图;
SkPaint 设置颜色和样式;
SkRect 用来绘制矩形。
SkCanvas记录着整个设备的绘画状态,而设备上面绘制的对象的状态又是由SkPaint类来记录的,SkPaint类作为参数,传递给不同SkCanvas类的成员函数drawXXXX().(比如:drawPoints, drawLine, drawRect, drawCircle)。SkPaint类里记录着如颜色(color), 字体(typeface), 文字大小(textSize), 文字粗细(strokeWidth), 渐变(gradients, patterns)等。
SkCanvas类的主要成员函数:
> 构造函数,给定一个Bitmap或者Device,在给定的这个对象上进行画图,Device可以为空。
SkCanvas(const SkBitmap& bitmap);
SkCanvas(SkDevice* device = NULL);
> save, saveLayer, saveLayerAlpha, restore, 这4个函数用于保存和恢复显示矩阵,剪切,过滤堆栈,不同函数有不同的附加功能。
> 移位,缩放,旋转,变形函数。
translate(SkiaScalar dx, SkiaScalar dy);
scale(SkScalar sx, SkScalar sy);
rotate(SkScalar degrees);
skew(SkScalar sx, SkScalar sy);
> 指定具体矩阵,进行相应的变换的函数,以上4个方法都可以通过定义特定的矩阵,再调用此函数实现。
cancat(const SkMatrix& matrix);
> 图像剪辑,把指定的区域显示出来。
clipRect(SkRect&...);
clipPath(SkPath&...);
clipRegion(SkRegion&...);
> 在当前画布内画图,有以下多种画图方式:
drawARGB(u8 a, u8 r, u8 g, u8 b....) 给定透明度以及红,绿,兰3色,填充整个可绘制区域。
drawColor(SkColor color...) 给定颜色color, 填充整个绘制区域。
drawPaint(SkPaint& paint) 用指定的画笔填充整个区域。
drawPoint(...)/drawPoints(...) 根据各种不同参数绘制不同的点。
drawLine(x0, y0, x1, y1, paint) 画线,起点(x0, y0), 终点(x1, y1), 使用paint作为画笔。
drawRect(rect, paint) 画矩形,矩形大小由rect指定,画笔由paint指定。
drawRectCoords(left, top, right, bottom, paint), 给定4个边界画矩阵。
drawOval(SkRect& oval, SkPaint& paint) 画椭圆,椭圆大小由oval矩形指定。
drawCicle(cx, cy, radius, paint), 给定圆心坐标和半径画圆。
drawArcSkRect& oval...) 画弧线,用法类似于画椭圆。
drawRoundRect(rect, rx, ry, paint) 画圆角矩形,x, y方向的弧度用rx, ry指定。
drawPath(path, paint) 路径绘制,根据path指定的路径绘制路径。
drawBitmap(SkBitmap& bitmap, left, top, paint = NULL) 绘制指定的位图, paint可以为空。
drawBitmapRect(bitmap, src, dest, paint=NULL), 绘制给定位图的一部分区域,此区域由src指定,然后把截取的部分位图绘制到dest指定的区域,可能进行缩放。
drawBitmapMatrix(bitmap, matrix, paint=NULL), 功效同上,可以通过给定矩阵来进行裁剪和缩放变换。
drawSprite(bitmap, left, top, paint=NULL), 绘制位图,不受当前变换矩阵影响。
drawText(void* text, byteLength, x, y, paint), 以(x,y)为起始点写文字,文字存储在text指针内,长度有byteLength指定。
drawPosText(...) 功能同上,不过每个文字可以单独指定位置。
drawPosTextH(...) 功能同上,不过由一个变量指定了当前所有文字的统一Y坐标,即在同一条水平线上以不同的间隔写字。
drawTextOnPathHV, drawTextOnPath, drawTextOnPath, 以不同方式在给点定的path上面绘制文字。
drawPicture(SkPicture& picture) 在画布上绘制图片,比较高效的绘图函数。
drawShape(SkShape*) 在画布上绘制指定形状的图像。
drawVertices(...) 绘制点,可以有纹理,颜色,等附加选项。
当然,Skia还提供一些特性功能,因为本人在项目中并未实际用到,也没深入研究,要用的时候再说吧。
使用 Skia 绘图的步骤:
1) 定义一个位图 32 位像素并初始化 SkBitmap bitmap;
2) 分配位图所占的空间
3) 指定输出设备
4) 设备绘制的风格
再回头来分析前面项目描述的需求,可以考虑用以下机制:
1. 封装图形层Layer,用一个skia的bitmap来存储UI数据,用Canvas将UI绘制到bitmap上去
2. 应用程序可以创建多个Layer,通过Layer来绘制图形
3. 要刷新时时将多个Layer叠加到surface上,由SurfaceFlinger来输出显示。
Layer层简单封装如下:
class Layer{
public:
int create(int width, int height, int colorMode); //创建一个Layer
int destroy(void); //销毁
int lock(void); //lock 在绘制UI之前需要锁上,防止多线程重入问题,当然再绘制之前需要查询锁定状态。
int unlock(void);
bool getLockStatus(void);
int getAddress(void **addr, int *pitch); //获取到Layer中对应的内存地址,修改内存数据绘制UI
int drawRect(int x, int y, int w, int h, unsigned int color); //绘制矩形
int drawLine(int x1, int y1, int x2, int y2, unsigned int color); //绘制直线
int copyFrom(Layer *src, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH);// 从一个指定区域拷贝到另一区域
int blitFrom(Layer *src, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH, SkXfermode::Mode mode);
SkBitmap *getBitMap();
private:
SkBitmap *m_bitmap; //bitmap提供绘制图形的buffer
bool m_lockFlag; //用于线程保
};
提取Layer几个关键API的实现来描述Skia的使用,这些API都是封装的skiaAPI,要详细了解Skia使用最后深入读下源码。
int Layer::create(int width, int height, int colorMode)
{
SkBitmap::Config config;
m_bitmap = new SkBitmap; //<span style="font-family:SimSun;">创建一个layer就是创建一个bitmap,用来当绘制设备</span>
config = convertYXColorMode(colorMode);
if ((!m_bitmap) || (config == SkBitmap::kNo_Config))
return -1;
m_bitmap->setConfig(config, width, height);
m_bitmap->allocPixels(); //根据指定的宽高和颜色模式分配内存空间
m_bitmap->eraseColor(0);
m_lockFlag = 0;
return 0;
}
int YXSurface::drawRect(int x, int y, int w, int h, unsigned int color)
{
SkPaint p;
SkCanvas canvas;
if (!m_bitmap)
return -1;
canvas.setBitmapDevice(*m_bitmap); <span style="font-family:SimSun;">//通过SkCanvas 来绘制矩形,绘制设备就是Layer层创建的bitmap。</span>
p.setColor(color);
canvas.drawRectCoords(x, y, x+w, y+h, p);
return 0;
}
其他API不再举例分析,都是通过skia的接口来实现对应的UI绘制。最后再介绍一下,这样Layer的数据如何通过surfaceFlinger输出显示。
封装Displayer,内部包含了Android Surface,把Layer中bitmap的数据blit到surface上即可,通过surface Android Lock 就能获取到对应的输出设备bitmap。
<span style="font-size:14px;">class Displayer {
public:
Displayer();
~Displayer();
......
int lockSurface(void); // 在之前的博文介绍了,lockSurface 可以获取显示buffer。
int unlockSurface(void);
......
int setDisplaySurface(int surfaceHandle); //设置Surface,将surfaceVieW中获取的surface设置下来当输出surface。
int Displayer::blitFrom(Layer *srclayer, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH, SkXfermode::Mode mode);
Surface::SurfaceInfo m_surfaceInfo;
private:
sp<Surface> m_surface;
sp<SurfaceComposerClient> m_surfaceComposerClient;
sp<SurfaceControl> m_surfaceControl;
SkCanvas* m_skcanvas;
... ...
};</span>
int Displayer::lockSurface(void)
{
ssize_t bpr;
SkBitmap::Config config;
SkBitmap bitmap;
if (m_surface->lock(&m_surfaceInfo) != OK) {
printe((char*)"android surface lock error!\n");
return -1;
}
bpr = m_surfaceInfo.s * android::bytesPerPixel(m_surfaceInfo.format);
config = convertPixelFormat(m_surfaceInfo.format);
bitmap.setConfig(config, m_surfaceInfo.w, m_surfaceInfo.h, bpr);
if (m_surfaceInfo.format == PIXEL_FORMAT_RGBX_8888) {
bitmap.setIsOpaque(true);
}
if ((m_surfaceInfo.w > 0) && (m_surfaceInfo.h > 0)) {
bitmap.setPixels(m_surfaceInfo.bits);
} else {
// be safe with an empty bitmap.
bitmap.setPixels(NULL);
}
m_skcanvas->setBitmapDevice(bitmap);
return 0;
}
int Displayer::unlockSurface(void)
{
m_surface->unlockAndPost();
return 0;
}
int Displayer::blitFrom(Layer *srclayer, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH, SkXfermode::Mode mode)
{
SkPaint p;
SkIRect srcRect;
SkRect dstRect;
SkDevice *pDevice;
SkBitmap *srcBitmap;
if ((m_skcanvas == 0) || (srcSurface == 0))
return -1;
pDevice = m_skcanvas->getDevice();
srcBitmap = srclayer->getBitMap();
if (!pDevice || !srcBitmap)
return -1;
const SkBitmap& dstBitmap = pDevice->accessBitmap(true);
srcRect.setXYWH(srcX, srcY, srcW, srcH);
dstRect.setXYWH(dstX, dstY, dstW, dstH);
p.setXfermodeMode(mode);
if(srcW != dstW || srcH!=dstH){
p.setFilterBitmap(true);
}
m_skcanvas->drawBitmapRect(*srcBitmap, &srcRect, dstRect, &p);
return 0;
}
上述三个接口可以看出,要将各个Layer的数据叠加到Displayer中去显示,就是将Displayer中surfaceLock起来,获取到对应的buffer,并配置到显示设备bitmap中。然后通过blitFrom方法将Layer中源数据绘制到Displayer 中,支持缩放。最后只要通过Displayer中的unlock方法就能有surfaceFlinger来输出显示。
博文只是给出了一种native 使用skia绘制UI的实现方案示例。skia的详细用法还是需要阅读对应API来掌握。