1. 开篇叨叨

能不叨叨就不叨叨。

2. 什么时候会触发视图绘制
* 调用setNeedsDisplay/setNeedsDisplayInRect 
* 遮挡当前视图的其他视图被移动或删除时
* 设置视图的hidden属性,改变视图的显示状态
* 视图滚出屏幕,然后再重新回到屏幕上

其实可以推断后面的几项实际上也是通过调用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. 创建/获取上下文
2. 创建路径并添加到上下文中。
3. 进行绘图内容的设置(画笔颜色、粗细、填充区域颜色、阴影、连接点形状等)
4. 开始绘图
5. 释放路径

这里需要注意的是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图像或者黑白图像绘制到一个位图对象中.
2. PDF Graphics Context: PDF图形上下文可以帮助开发者创建PDF文件,将内容绘制进PDF文件中.
3. Window Graphics Context: 用于将内容绘制到OS系统中的窗口上
4. Layer Context: 用于将内容绘制在Layer图层上
5. Printer Graphics Context: 用于将内容绘制在打印输出源上

* CGContextSaveGState/CGContextRestoreGState 与 UIGraphicsPushContext/UIGraphicsPopContext区别

想象一个场景,比如我们现在需要修改上下文并使其恢复原样。举个例子,我们现在有一个使用特定颜色绘制特定形状的函数。由于只能有一只画笔,因此在更改颜色后,就会影响调用函数的结果。为了避免这个副作用,你可以使用CGContextSaveGState和CGContextRestoreGState将上下文入栈和出栈。下面是一个很有说服力的例子:

[[UIColor redColor] setStroke];                         //将线条颜色设置为红色
CGContextSaveGState(UIGraphicsGetCurrentContext()); //将带有红色线条颜色的上下文保存到上下文的状态堆栈
[[UIColor blackColor] setStroke]; //将当前上下文堆栈中的线条颜色设置为黑色
CGContextRestoreGState(UIGraphicsGetCurrentContext()); //恢复上一次保留的堆栈节点
UIRectFill(CGRectMake(10, 10, 100, 100)); //这时候绘制的是红色的线条

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);
这里需要注意的是scale可以用[UIScreen mainScreen].scale来获取,但实际上设为0后,系统就会自动设置正确的比例了。
UIGraphicsBeginImageContext(CGSize size);
相当于UIGraphicsBeginImageContextWithOptions的opaque参数为NO,scale因子为1.0

一般推荐使用第一种方式来创建,第一种方式除了可以指定图片的大小外,还可以指定图片是否透明,以及图片的scale。需要注意的是上面两种方式都会在创建一个基于位图的上下文,并将其设置为当前上下文,所以后续操作如果需要使用到上下文对象就可以通过UIGraphicsGetCurrentContext来获取。

一般使用步骤如下:

// 获取图片
UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:nil]];
// 1.开启图形上下文,并将ImageContext放置到栈顶
UIGraphicsBeginImageContext(image.size);
// 2.获取到当前栈顶的图形上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 3. 使用Core Graphics API 在当前上下文中绘制

// ........

// 4.从上下文中获取图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 5.关闭图形上下文
UIGraphicsEndImageContext();
//返回图片
return newImage;

下面分别使用UIKit和CoreGraphics实现的一个例子:

使用UIKit实现

// 获取图片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 绘图
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
// 从图片上下文中获取绘制的图片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图片上下文
UIGraphicsEndImageContext();

使用CoreGraphics实现

// 获取图片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 绘图
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
// 从图片上下文中获取绘制的图片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图片上下文
UIGraphicsEndImageContext();
4.2 drawInRect

在系统调用drawInRect方法之前会创建一个新的Context,因此在drawInRect一般而言不需要创建新的Context,只需要通过UIGraphicsGetCurrentContext来获取即可。

使用UIKit实现

- (void) drawRect: (CGRect) rect {
UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
}

使用CoreGraphics实现

- (void) drawRect: (CGRect) rect {
CGContextRef con = UIGraphicsGetCurrentContext();//当前视图的上下文
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}
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 {
CGContextAddEllipseInRect(ctx, CGRectMake(100,100,100,100));
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillPath(ctx);
}
@interface ViewController () 

@property (nonatomic, weak) id myLayerDelegate;

@end

@implementation ViewController
- (void)viewDidLoad {
MyView *myView = [[MyView alloc] initWithFrame:self.view.bounds];
self.view addSubview:myView];
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor magentaColor].CGColor;
layer.bounds = CGRectMake(0, 0, 300, 500);
layer.anchorPoint = CGPointZero;
layer.delegate = [[MyLayerDelegate alloc] init];
[layer setNeedsDisplay];
[myView.layer addSublayer:layer];
}
4. 绘制坐标系统

UIView 有几个比较重要的位置坐标属性:frame, bounds, position,center,anchorPoint

* frame 表示视图,图层的外部坐标,也就是当前UIView相对于父视图的坐标
* bounds 表示视图,图层的内部坐标。原点位于左上角,它的作用主要是用于存放宽高尺寸。
* center 和 position 代表相对于父图层anchorPoint所在的位置,center 是视图的称呼,postion是图层里面的称呼,二者是同一个值。
* anchorPoint 可以看成是一个UIView的移动支点。我们设置center或者position的时候就是移动anchorPoint坐标,anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},因此默认坐标是{0.5, 0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。

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轴相反,所以图片绘制将会上下颠倒。可以用下面几种方式来解决:

  1. 在绘制到context前通过矩阵垂直翻转坐标系
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), uiImage.CGImage);
  1. 使用UIImage的drawInRect函数,该函数内部能自动处理图片的正确方向

UIGraphicsPushContext(context);
[uiImage drawInRect:CGRectMake(0, 0, width, height)];
UIGraphicsPopContext();

4.3 坐标转换
// 将像素point由point所在视图(方法调用者)转换到目标视图view中,返回在目标视图view中的像素值
- [(CGPoint)convertPoint:(CGPoint)point] toView:(UIView *)view;
// 将像素point从view中转换到当前视图(方法调用者)中,返回在当前视图中的像素值
- (CGPoint)[convertPoint:(CGPoint)point fromView:(UIView *)view;]

// 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect
- [(CGRect)convertRect:(CGRect)rect] toView:(UIView *)view;
// 将rect从view中转换到当前视图中,返回在当前视图中的rect
- [(CGRect)convertRect:(CGRect)rect fromView:(UIView *)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坐标系点
CGPoint redCenterInBlueView = [self.grayView convertPoint:self.redView.center toView:self.blueView];
BOOL isInside = [self.blueView pointInside:redCenterInBlueView withEvent:nil];
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。

Contents
  1. 1. 1. 开篇叨叨
  2. 2. 2. 什么时候会触发视图绘制
  3. 3. 2. Core Graphic 简介
  4. 4. 3. 绘图上下文
    1. 4.1. * CGContextSaveGState/CGContextRestoreGState 与 UIGraphicsPushContext/UIGraphicsPopContext区别
  5. 5. 4. 具体绘图方法
    1. 5.1. 4.1 位图图片上下文
    2. 5.2. 4.2 drawInRect
    3. 5.3. 4.2 drawLayer:inContext
  6. 6. 4. 绘制坐标系统
    1. 6.1. 4.1 UIView坐标系
    2. 6.2. 4.2 Quartz坐标系
    3. 6.3. 4.3 坐标转换
    4. 6.4. 4.4 坐标关系
  7. 7. 5. Core Graphic API
  8. 8. 6. CGPath 和 UIBezierPath() 区别