CoreGraphics之基础知识全面分析

文章结构

  • Context绘图上下文
  • Path路径
  • Patterns使用
  • Shadows阴影使用
  • Gradients渐变效果使用
  • Transparency Layers使用
  • CGLayers使用
  • 总结

Context绘图上下文

所有的绘制都是在context上下文中进行的,在ios系统用得最多的是以下三种。

  1. 位图上下文(A bitmap graphics context):一般用于绘制图片或者自定义控件。

一般当系统控件不能满足我们UI的需求时,都需要重载UIView的drawrect:方法,在我们执行任何绘制代码前,该方法自动配置好绘图上下文,我们只需要通过UIGraphicsGetCurrentContext方法就可以获取到当前绘图上下文用于绘制操作。

当不是在drawrect:方法中执行绘制操作,一般是实现一个接口来生成图片,需要我们自己生成绘图上下文,通过UIKit框架的UIGraphicsBeginImageContextWithOptions方法创建绘图上下文,然后通过UIGraphicsGetCurrentContext获取用于绘制操作。

还有一种比较底层的方法CGBitmapContextCreate用于创建绘图上下文,该方法原型如下所示:

CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo);

第一个参数表示开辟多大的内存给绘图上下文,一般至少bytesPerRow * height,但是为了不出错,传入NULL系统会自动计算,第二、第三个参数就是宽、高,第四个参数表示一个颜色多少比特,为了更好理解颜色组成,可以在这color and color space查看,第五个参数表示绘图上下文的一行有多少字节,第六个参数表示颜色空间。举个例子如果我们使用RGBA格式32-bit的颜色,A表示有透明度,则一个像素就4个字节,则每个颜色占8比特,一行就是宽度乘以4 个字节。示例代码如下:

CGContextRef myConctext(size_t width, size_t height){

    size_t bitsPerComponent = 8;
    size_t bytesPerRow = width * 4;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
    CGColorSpaceRelease(colorSpace);
    return context;
}

使用这种方式创建的绘图上下文,需要我们转换坐标系,因为Quartz 2D的坐标原点在左下角,而ios的用户坐标原点在左上角。先往y轴向上平移,后反转y轴,代码如下:

CGContextTranslateCTM(context, 0, 200);
CGContextScaleCTM(context, 1, -1);

说明:CGContextScaleCTM函数用于缩放坐标系,但是-1时候表示不缩放反转坐标系。
最后使用CGBitmapContextCreateImage函数获取绘制的图片。

当需要自己创建绘图上下文且满足需求的情况下建议使用UIGraphicsBeginImageContextWithOptions创建。

  1. PDF上下文(A PDF graphics context):用于生成pdf文件。
  2. 图层上下文(A layer context):用于离屏绘制( offscreen drawing)。

Path路径

说明:较简单,只记录一些使用技巧,不会涉及所有知识点。

  1. 画实线。
    会首尾相连的线,代码如下
CGContextSetStrokeColorWithColor(context, [UIColor purpleColor].CGColor);
CGContextSetLineWidth(context, 5);
CGContextMoveToPoint(context, 10, 10);
CGContextAddLineToPoint(context, 50, 10);
CGContextAddLineToPoint(context, 50, 20);
CGContextStrokePath(context);

其中CGContextMoveToPoint表示移动到改点开始绘制,CGContextAddLineToPoint表示添加一条线段到指定点,如果绘制多条首尾相连的线段就需要多次调用CGContextAddLineToPoint,代码繁琐不易读,有一个方法CGContextAddLines可以绘制首尾相连的多条线段,定义好元素是CGPoint的数组后一行代码搞定,代码如下:

CGPoint points[] = {{10, 10}, {50, 10}, {50, 20}};
CGContextSetStrokeColorWithColor(context, [UIColor purpleColor].CGColor);
CGContextSetLineWidth(context, 5);
CGContextAddLines(context, points, 3);
CGContextStrokePath(context);
  1. 画虚线。
    使用CGContextSetLineDash绘制虚线,该函数的参数说明,在API接口文档已经相当仔细,这里不细说了。

  2. 画线段。
    就是画完一条线段后,再调用CGContextMoveToPoint重新设置绘制起点,类似机械手。代码如下:

CGContextSetStrokeColorWithColor(context, [UIColor purpleColor].CGColor);
CGContextSetLineWidth(context, 5);
CGContextMoveToPoint(context, 0, 10);
CGContextAddLineToPoint(context, size.width, 10);
CGContextMoveToPoint(context, 0, 30);
CGContextAddLineToPoint(context, size.width, 30);
CGContextStrokePath(context);

如果绘制多条线段看起来就密密麻麻了,可以使用CGContextStrokeLineSegments函数较为简洁,代码如下:

CGPoint points[] = {{0, 10}, {size.width, 10}, {0, 30},{size.width, 30}};
CGContextSetStrokeColorWithColor(context, [UIColor purpleColor].CGColor);
CGContextStrokeLineSegments(context, points, 4);
CGContextStrokePath(context);
  1. 选择性填充绘制
    使用even-odd rule奇偶原则选择性绘制,该原则简单理解就是画一条线横穿所有路径,奇数个路径需要填充。想细看该原则说明在这even-odd rule的fill a path部分。如果我们要绘制如下效果

    填充第一个圆和第三个圆。代码如下:
CGContextSetRGBFillColor(context, 1, 0, 0, 1);
CGContextSetRGBStrokeColor(context, 0, 1, 0, 1);
//CGContextClipToRect(context, CGRectMake(50, 50, size.width - 50 * 2, size.height - 50 * 2));
CGContextAddEllipseInRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextAddEllipseInRect(context, CGRectMake(50, 50, size.width - 50 * 2, size.height - 50 * 2));
CGContextAddEllipseInRect(context, CGRectMake(60, 60, size.width - 60 * 2, size.height - 60 * 2));
CGContextDrawPath(context, kCGPathEOFillStroke);

其中CGContextDrawPath函数可以实现stroke画线和fill填充功能,使用方便,第二个参数又可以指定相应绘制规则。当然你也可以画三个圆,然后指定不同颜色实现同样效果,明显第一种方法会简便些。

说明:对于画线,还有很多线段设置属性,比如开始和结束时候线段形状、连接处形状等,都比较简单,不再赘述。

Patterns使用

一个pattern就是重复绘制的操作,可以 像使用color一样使用pattern,Quartz会吧绘图上下文切割成一个个pattern cell,然后调用你指定的回调函数绘制每一个cell,对于需要重复绘制同样的图形是很有用的。patterns分为color pattern和stencil pattern。

  1. 设置pattern color space,至关重要,如果不设置则无法绘制pattern。
    示例代码:
//configure color space
    CGColorSpaceRef colorSpace = CGColorSpaceCreatePattern(NULL);
    CGContextSetFillColorSpace(context, colorSpace);
    CGColorSpaceRelease(colorSpace);

在使用CGColorSpaceCreatePattern函数时,如果是绘制color pattern则传入NULL即可,如果是绘制stencil pattern则需要传入color space。

  1. 创建pattern
    通过以下接口创建
CGPatternRef __nullable CGPatternCreate(void * __nullable info,
CGRect bounds, CGAffineTransform matrix, CGFloat xStep, CGFloat yStep,
CGPatternTiling tiling, bool isColored,
const CGPatternCallbacks * cg_nullable callbacks)

参数说明:第一个参数对应于最后一个参数CGPatternCallbacks回调函数的第一个参数,如果需要额外数据用于绘制pattern cell,则可以通过该参数传递,否则穿NULL即可;第二个代表每个pattern cell的尺寸;第三参数映射变换,如果不需要则传入CGAffineTransformIdentity;第四第五个参数如果每个cell之间需要有间隔,则需要传入cell宽或者高加上间隔距离;第六个参数最好看文档原文说明;第七个参数表示是否是绘制color pattern;最后一个参数是一个回调函数,每绘制一个cell会自动调用一次;所以还需要实现一个回调函数CGPatternCallbacks用于绘制cell。
示例代码

CGPatternRef pattern = CGPatternCreate(NULL,
                                           CGRectMake(0, 0, patternCellW, patternCellH),
                                           CGAffineTransformIdentity,
                                           patternCellW + 5,
                                           patternCellH + 5,
                                           kCGPatternTilingConstantSpacing,
                                           YES,
                                           &patternCallBacks);
  1. 设置pattern
    通过CGContextSetFillPattern设置填充,还有一个CGContextSetStrokePattern用于设置,这里用到前者。
  2. 使用pattern
    就像设置了颜色后填充对应区域一样使用,使用CGContextFillRect填充。

NOTE:目前在项目中使用pattern较少,不过理解下也挺好。

Shadows阴影使用

有两个API可以设置阴影

void CGContextSetShadowWithColor(CGContextRef cg_nullable c,
    CGSize offset, CGFloat blur, CGColorRef __nullable color);
 void CGContextSetShadow(CGContextRef cg_nullable c, CGSize offset,
    CGFloat blur);

第一个使用指定颜色作为阴影颜色;第二使用黑色作为阴影颜色,且颜色透明度为1/3。过于简单就不演示了

Gradients渐变效果使用

渐变因素:简单讲就是颜色间的过渡,包括不同颜色间和同种颜色而透明度不同的颜色过渡,严格点讲颜色组成相同但是透明度不同也是两种不同颜色。
渐变效果:沿坐标轴方向渐变和雷达类型渐变(两个圆之间的渐变)。原文如下:

An axial gradient (also called a linear gradient) varies along an axis between two defined end points. All points that lie on a line perpendicular to the axis have the same color value.
A radial gradient is a fill that varies radially along an axis between two defined ends, which typically are both circles. Points share the same color value if they lie on the circumference of a circle whose center point falls on the axis. The radius of the circular sections of the gradient are defined by the radii of the end circles; the radius of each intermediate circle varies linearly from one end to the other.

Quartz提供了两种数据类型实现渐变效果,即CGShadingRef和CGGradientRef,前者使用起来稍微麻烦,但是能实现更多炫酷的效果,后者实现起来容易些,但是想要更复杂的效果就比较难,本文讲解最多的是后者,够用也方便使用。

  1. CGGradientRef渐变
  • 沿坐标轴渐变,核心代码如下:

void CGContextDrawLinearGradient(CGContextRef cg_nullable c,
CGGradientRef cg_nullable gradient, CGPoint startPoint, CGPoint endPoint,
CGGradientDrawingOptions options);

逆向推导就知道需要什么类型数据,第一个参数是上下文不用多说;startPoint和endPoint表示的是渐变的区域和方向,这个和CAGradientLayer的startPoint、endPoint不一样,前者构建的CGPoint类型是表示的具体像素,而后者表示的是单位长度,代码见demo;最后一个参数表示渐变是否延展出我们指定的startPoint和endPoint位置;第二个参数就是要构建CGGradientRef对象,看下原型:

CGGradientRef __nullable CGGradientCreateWithColorComponents(
CGColorSpaceRef cg_nullable space, const CGFloat * cg_nullable components,
const CGFloat * __nullable locations, size_t count);
CGGradientRef __nullable CGGradientCreateWithColors(
CGColorSpaceRef __nullable space, CFArrayRef cg_nullable colors,
const CGFloat * __nullable locations);

两个接口都差不多,只是构建颜色时候有点区别。首先都需要构建colorSpace,不懂什么是colorSpace的可以看color and color Space,构建components颜色组成,先看代码:

CGFloat components[] = {1,0,0,1,
                        1,0,0,0};

假设我们构建的最常用的color space是kCGColorSpaceGenericRGB,则数组中每4个数代表一个颜色组成,即RGBA,分别代表三原色红、绿、蓝,第四个数是透明度,我们只实现两个颜色间的渐变,所以该数组中需要8个数,且每个数都在0~1间,当然也可以实现多个,记住元素是4的倍数就行。

locations指定颜色位置。

其实这些函数在API里都有使用说明,而且很简单,demo在文章最后,看代码是最好的表达方式

  • 雷达类型的渐变,核心代码如下:

void CGContextDrawRadialGradient(CGContextRef cg_nullable c,
CGGradientRef cg_nullable gradient, CGPoint startCenter, CGFloat startRadius,
CGPoint endCenter, CGFloat endRadius, CGGradientDrawingOptions options)

就不一一介绍参数了,同样需要创建CGGradientRef核心对象,然后 指定两个圆的圆心和半径,如果想要同心圆渐变,圆心相同,半径不同即可,如下图:



我的demo里没有实现同心圆渐变,但是可以修改下代码就能看到这种效果,可以下载下来玩玩。

  1. CGShadingRef渐变
    这个demo里也有从文档摘下来的代码,想深入了解可以自己下载并看相关Apple文档学习,这个使用比较麻烦的就是构建CGFunctionRef对象。

Transparency Layers使用

一个transparency layer就是把两个或者两个以上的对象组合成一个整体,然后给这个整体对象添加某个属性,用得最多的就是给对象添加阴影。
假设给三个圆添加阴影,不使用transparency layer情况下,效果:



我们想要的效果是:


trans_layer1.gif

使用transparency layer就能达到这种效果。

使用方法:在你需要的地方添加CGContextBeginTransparencyLayer和CGContextEndTransparencyLayer方法,然后在中间添加你自己想要的绘制操作,这里是画三个圆。

CGLayers使用

适合使用CGLayers的场景:

  • 高质量的离屏渲染
  • 重复绘制
  • 缓冲
    文档原文如下:

Layers are suited for the following:
High-quality offscreen rendering of drawing that you plan to reuse. For example, you might be building a scene and plan to reuse the same background. Draw the background scene to a layer and then draw the layer whenever you need it. One added benefit is that you don’t need to know color space or device-dependent information to draw to a layer.
Repeated drawing. For example, you might want to create a pattern that consists of the same item drawn over and over. Draw the item to a layer and then repeatedly draw the layer, as shown in Figure 12-1. Any Quartz object that you draw repeatedly—including CGPath, CGShading, and CGPDFPage objects—benefits from improved performance if you draw it to a CGLayer. Note that a layer is not just for onscreen drawing; you can use it for graphics contexts that aren’t screen-oriented, such as a PDF graphics context.
Buffering. Although you can use layers for this purpose, you shouldn’t need to because the Quartz Compositor makes buffering on your part unnecessary. If you must draw to a buffer, use a layer instead of a bitmap graphics context.

使用步骤:

  1. 使用一个CGContextRef绘图上下文去创建一个CGLayerRef对象,相应函数CGLayerCreateWithContext。
  2. 获取CGLayer的上下文,相应函数CGLayerGetContext。
  3. 在CGLayer上下文中绘制自己想要的内容。
  4. 把CGLayer绘制到目标绘图上下文中,相应函数CGContextDrawLayerInRect和CGContextDrawLayerAtPoint。

注意:把CGLayer绘制到目标绘图上下文时,目标绘图上下文可以和用于初始化CGLayerRef对象的上下文一样,也可以不一样,但是会受到用于初始化CGLayerRef对象时的上下文的约束。一般我们都把CGLayer对象绘制在和其初始化时候一样的绘图上下文中。
原文如下:

Note: You are not required to draw a layer to the same graphics context that you use to initialize the layer. However, if you draw the layer to another graphics context, any limitations of the original graphics context are imposed on your drawing.

来实现一个例子,如下图:


分析下思路:里面重复图案挺多的,可以使用CGLayer实现。首先红白相间条纹,最简单的就是背景颜色绘制成白色,然后我们只需要创建一个背景是红色的CGLayer对象,然后重复绘制就得到红白相间的效果;再画一个星星的背景区域为蓝色;最后创建一个内容是一个星星的CALayer对象,然后重复绘制,便完成这幅图。最麻烦的就是绘制星星,需要计算好每个点的位置。

这里就不贴代码了,理解思路是最重要的,如果喜欢可以下载我的demo。

整个CoreGraphics的使用代码都在一个工程里,每个文件夹命名就是相对应的使用知识点,欢迎下载交流Demo。如有问题可以给我留言

总结

  • 能使用UIKit满足的就使用UIKit的API,因为有些API也是对底层的API(比如CoreGraphics框架)的高度封装,使用起来简单方便。
  • 文中提到好多“绘图上下文”这个词,简单理解就是一个画布。
  • 有些设置是影响全局的,如果你想恢复到之前的画布状态可以使用CGContextSaveGState和CGContextRestoreGState,不知道哪些设置会影响画布的全局属性的,可以看Graphics States
  • 使用coreGraphics框架时,绘制某个对象,可以指定这个对象的位置,也可以移动这个画布(使用CGContextTranslateCTM),类似缝纫机。我在写CGLayer的演示demo中这两种方法都使用了,可以下载demo看看。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342