Core Graphic
1. 开篇叨叨
能不叨叨就不叨叨。
2. 什么时候会触发视图绘制
* 调用setNeedsDisplay/setNeedsDisplayInRect |
其实可以推断后面的几项实际上也是通过调用setNeedsDisplay/setNeedsDisplayInRect来标记当前控件需要重新绘制。
这部分工作是在CPU中完成的,因此速度不如GPU绘制的效率高,因此尽可能避免使用绘制,多使用现有的控件组合来达到需求。
2. Core Graphic 简介
iOS支持两套图形API族:Core Graphics和OpenGL ES,而Core Graphics 是一套基于C的API框架,使用了Quartz作为绘图引擎, 和Open GL不同的是Core Graphics使用的是CPU进行绘制Open GL使用GPU进行绘制,使用Core Graphics 绘制一个图形一般是遵循以下步骤:
1. 创建/获取上下文 |
这里需要注意的是Core Graphics中带有Ref后缀的类,其实例对象可能有指向其他 Core Graphics “对象”的强引用指针,但是不能被ARC管理,所以创建了这些对象,使用完之后记得手动释放,否则会有内存泄漏的问题。并且但凡名字中带有create 或者 copy 的函数创建了一个 Core Graphics “对象”,就必须调用对应的Release函数并传入该对象的指针将其释放,这是刚开始的时候很容易犯的错误。
从上面的过程来看,这部分总共有三个部分需要重点掌握:
一个是绘制上下文,也就是绘制的内容绘制到哪里?如何屏蔽多个环境之间的差异。
一个是绘制坐标系统,也就是我们怎么定义绘制的位置和长度的概念,
另一个就是Core Graphic API,也就是Core Graphic能够支持哪些绘制方法。
3. 绘图上下文
我们绘图的时候是需要一个载体或者说输出目标用来显示绘图信息,并且决定绘制的东西输出到哪个地方,Core Graphics框架就使用图形上下文来描述这个载体,这些上下文以堆栈形式存放,我们绘制的时候都是往栈顶的图形上下文上绘制,每个图形上下文包括画笔颜色、文本颜色、当前字体、变形,以及绘制内容所存储的位置等。之所以需要使用Context是因为Core Graphics可以在多种设备上绘制,比如在手机屏幕上,这也是最为常见的,再必须还可以在PDF上绘制,也可以再图片上进行绘制,每种设备上都存在很大的差异,Core Graphics 使用Context将这部分差异给隔离开来。让绘制内容与绘制步骤与设备无关。
Core Graphics 目前支持如下几种绘图上下文:
1. Bitmap Graphics Context: 将RGB图像或者黑白图像绘制到一个位图对象中. |
* CGContextSaveGState/CGContextRestoreGState 与 UIGraphicsPushContext/UIGraphicsPopContext区别
想象一个场景,比如我们现在需要修改上下文并使其恢复原样。举个例子,我们现在有一个使用特定颜色绘制特定形状的函数。由于只能有一只画笔,因此在更改颜色后,就会影响调用函数的结果。为了避免这个副作用,你可以使用CGContextSaveGState和CGContextRestoreGState将上下文入栈和出栈。下面是一个很有说服力的例子:
[[UIColor redColor] setStroke]; //将线条颜色设置为红色 |
UIGraphicsPushContext并不能保存上下文的当前状态,而是完全切换上下文。假设你正在当前视图上下文中绘制什么东西,这时想要在位图上下文中绘制完全不同的东西。如果要使用UIKit来进行任意绘图,你会希望保存当前的UIKit上下文,包括所有已经绘制的内容,接着切换到一个全新的绘图上下文中。这就是UIGraphicsPushContext的功能。创建完位图后,再调用UIGraphicsPopContext将你的旧上下文出栈。这种情况很少见只会在要使用UIKit在新的位图上下文中绘图时才会发生。只要你使用的是Core Graphics函数,就不需要去执行上下文入栈和出栈,因为Core Graphics函数将上下文视作参数。
这是极其有用的常见操作,因为其常用性,苹果公司为我们创建了一个叫做UIGraphicsBeginImageContext的快捷方式。它负责将旧的上下文入栈、为新上下文分配内存、创建新的上下文、翻转坐标系统,并使其作为当前上下文使用。它替你完成了大部分的工作。
下图是Graphics Context与Graphics state的大致结构
4. 具体绘图方法
4.1 位图图片上下文
位图上下文的绘制不需要在drawRect:方法中进行,在一个普通的OC方法中就可以绘制。
关于UIGraphicsPushContext/UIGraphicsPopContext 与 UIGraphicsBeginImageContextWithOptions的区别用两个场景再说明:
当前正在使用CoreGraphics绘制图形A,想要使用UIKit绘制完全不同的图形B,此时希望保存当前绘图context及已绘制内容这时候需要用到UIGraphicsPushContext/UIGraphicsPopContext。
如果想在切换绘图context后,继续使用CoreGraphics绘图(而非UIKit),则不需要使用UIGraphicsPushContext/UIGraphicsPopContext。
位图上下文可以通过如下两种方式创建:
UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale); |
UIGraphicsBeginImageContext(CGSize size); |
一般推荐使用第一种方式来创建,第一种方式除了可以指定图片的大小外,还可以指定图片是否透明,以及图片的scale。需要注意的是上面两种方式都会在创建一个基于位图的上下文,并将其设置为当前上下文,所以后续操作如果需要使用到上下文对象就可以通过UIGraphicsGetCurrentContext来获取。
一般使用步骤如下:
// 获取图片 |
下面分别使用UIKit和CoreGraphics实现的一个例子:
使用UIKit实现
// 获取图片上下文 |
使用CoreGraphics实现
// 获取图片上下文 |
4.2 drawInRect
在系统调用drawInRect方法之前会创建一个新的Context,因此在drawInRect一般而言不需要创建新的Context,只需要通过UIGraphicsGetCurrentContext来获取即可。
使用UIKit实现
- (void) drawRect: (CGRect) rect { |
使用CoreGraphics实现
- (void) drawRect: (CGRect) rect { |
4.2 drawLayer:inContext
讲到这里得复习下Core Graphic的绘图方法的调用流程:
上图在介绍iOS渲染的时候已经讲解过了,我们这里再重新提一下第二个分支的过程,第二个分支开始的时候会创建一个新的backing store,然后开始走drawInContext,这时候会先看delegate是否实现了drawRect如果有则用drawRect,否则调用drawLayer:inContext:并将管理新建backing store的context传递出来,因此我们在绘制的时候只要使用传递出来的context就可以直接绘制到指定到layer
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { |
@interface ViewController () |
4. 绘制坐标系统
UIView 有几个比较重要的位置坐标属性:frame, bounds, position,center,anchorPoint
* frame 表示视图,图层的外部坐标,也就是当前UIView相对于父视图的坐标 |
frame是一个关联属性,是根据bounds,center和transform计算出来的,后面的任何一个值变化都会影响到frame的值。而frame值一旦改变也会对bounds,center和transform产生影响。同时也需要注意的是当图层做变换后,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域。这时候frame宽高和bounds有可能不一样了。
4.1 UIView坐标系
而在iOS的UIView中,统一使用左手坐标系,也就是坐标原点在左上角.
4.2 Quartz坐标系
Quartz(Core Graphics)坐标系使用的是右手坐标系,原点在左下角, 所以所有使用Core Graphics画图的坐标系都是右手坐标系,当使用CG的相关函数画图到UIView上的时候,需要注意CTM的Flip变换,要不然会出现界面上图形倒过来的现象。由于UIKit的提供的高层方法会自动处理CTM(比如UIImage的drawInRect方法),所以无需自己在CG的上下文中做处理。
当通过CGContextDrawImage绘制图片到一个context中时,如果传入的是UIImage的CGImageRef,因为UIKit和CG坐标系y轴相反,所以图片绘制将会上下颠倒。可以用下面几种方式来解决:
- 在绘制到context前通过矩阵垂直翻转坐标系
CGContextTranslateCTM(context, 0, height); |
- 使用UIImage的drawInRect函数,该函数内部能自动处理图片的正确方向
|
4.3 坐标转换
// 将像素point由point所在视图(方法调用者)转换到目标视图view中,返回在目标视图view中的像素值 |
- 使用convertPoint:toView:时,调用者应为covertPoint的父视图。即调用者应为point的父控件。toView即为需要转换到的视图坐标系,以此视图的左上角为(0,0)点。
- 使用convertPoint:fromView:时正好相反,调用者为需要转换到的视图坐标系。fromView为point所在的父控件。
- toView可以为nil。此时相当于toView传入self.view.window
这里特地将同个坐标的用方括号扩起来,方便理解,也就是说括号内的是在同一个坐标系上。
4.4 坐标关系
- 点是否在范围内的判断
需要判断点是否在某个范围内可以使用如下方法:
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; |
使用注意:
point必须为调用者的坐标系,即调用者的左上角为(0,0)的坐标系。
比如确定redView的中心点是否在blueView上:
//转换为blueView坐标系点 |
5. Core Graphic API
整个Core Graphic API 十分丰富,这里只是列举一部分比较核心的来介绍:
完整的可以查看官方文档:Core Graphic API
这里比较重要的是画圆弧的时候弧度的起始角度和结束角度,以及顺时针逆时针方向,我在网上找了一张图解释得很到位大家可以参照下。
深入理解UIBezierPath画圆弧addArcWithCenter
6. CGPath 和 UIBezierPath() 区别
CGPath是CoreGraphics库的类,而UIBezierPath是UIKit中的类. UIBezierPath是对CGPath的一种封装,可以很方便在二者之间进行转换,CGPath相对而言更底层,在速度性能上较UIBezierPath高,并且它具备UIBezierPath不具备的更高级的功能,但是UIBezierPath在使用上十分易用,所以除非在万不得己的情况下一般推荐使用UIBezierPath。