Android UI 绘制之Skia

        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来掌握。




     

     

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值