iOS开发之Quartz2D入门

宣室求贤访逐臣,贾生才调更无伦。
可怜夜半虚前席,不问苍生问鬼神。

李商隐 《贾生》

基本介绍

  • Quartz 2D是一个二维绘图引擎,同时支持iOS和Mac系统
  • Quartz 2D能完成的工作

    • 绘制图形 : 线条\三角形\矩形\圆\弧等
    • 绘制文字
    • 绘制\生成图片(图像)
    • 读取\生成PDF
    • 截图\裁剪图片
    • 自定义UI控件
    • … …
  • drawRect:方法的使用

    • 常见图形的绘制:线条、多边形、圆
    • 绘图状态的设置:文字颜色、线宽等
    • 图形上下文状态的保存与恢复(图形上下文栈)
    • 图片裁剪
    • 截图

图形上下文

  • 图形上下文(GraphicsContext):是一个CGContextRef类型的数据
  • 图形上下文的作用:

    • 保存绘图信息、绘图状态
    • 决定绘制的输出目标(绘制到什么地方去?)(输出目标可以是PDF文件、Bitmap或者显示器的窗口上)
      hack_appList_result_failed
  • 相同的一套绘图序列,指定不同的GraphicsContext,就可将相同的图像绘制到不同的目标上

  • Quartz2D提供了以下几种类型的Graphics Context:
1
2
3
4
5
Bitmap Graphics Context
PDF Graphics Context
Window Graphics Context
Layer Graphics Context
Printer Graphics Context
hack_appList_result_failed

如何利用Quartz2D自定义view?(自定义UI控件)

  • 如何利用Quartz2D绘制东西到view上?

    • 首先,得有图形上下文,因为它能保存绘图信息,并且决定着绘制到什么地方去
    • 其次,那个图形上下文必须跟view相关联,才能将内容绘制到view上面
  • 为什么要实现drawRect:方法才能绘图到view上?

    • 因为在drawRect:方法中才能取得跟view相关联的图形上下文
  • drawRect:方法在什么时候被调用?

    • 当view第一次显示到屏幕上时(被加到UIWindow上显示出来)
      调用view的setNeedsDisplay或者setNeedsDisplayInRect:时

注意点

  • Quartz2D的API是纯C语言的
  • Quartz2D的API来自于Core Graphics框架
  • 数据类型和函数基本都以CG作为前缀
1
2
3
4
CGContextRef
CGPathRef
CGContextStrokePath(ctx);
……

常用属性方法

  • drawRect:中取得的上下文
    • drawRect:方法中取得上下文后,就可以绘制东西到view上
    • View内部有个layer(图层)属性,drawRect:方法中取得的是一个Layer GraphicsContext,因此,绘制的东西其实是绘制到view的layer上去了
    • View之所以能显示东西,完全是因为它内部的layer

自定义view的步骤

  • 新建一个类,继承自UIView
  • 实现- (void)drawRect:(CGRect)rect方法,然后在这个方法中
  • 取得跟当前view相关联的图形上下文
  • 绘制相应的图形内容
  • 利用图形上下文将绘制的所有内容渲染显示到view上面

Quartz2D绘图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这个方法只会在第一次加载的时候调用 不能的手动调用
- (void)drawRect:(CGRect)rect{

// 1.获取上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();

// 2.画圆
CGContextAddArc(ctx, 125, 125, self.radius, 0, M_PI * 2, 0);

[[UIColor redColor] set]; // 设置颜色

// 3.渲染
CGContextFillPath(ctx);
}

重绘实现简单动画

  • CADisplayLink定时器
  • 如果每隔一段时间重绘,一般不会使用NSTimer
  • 每次屏幕刷新的时候才会触发,每秒屏幕刷新60次,即1秒触发60次
  • 每次刷新的时候就重绘一次
    • setNeedsDisplay:重绘,给当前控件绑定一个刷新的标识,每次屏幕刷新其实就是重绘
    • 每次屏幕自动刷新60次,每次刷新的时候只会把当前需要刷新的view重绘
1
2
// 重绘(会删除上一次的绘制信息)
[self setNeedsDisplay];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)awakeFromNib
{
// 添加定时器(适合时间间隔比较小得情况) 重绘实现动画
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];

// 添加到主运行循环,才会触发这个定时器
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)drawRect:(CGRect)rect
{
self.dogY += 7;
self.dogY1 += 5;

if (self.dogY > rect.size.height) {
self.dogY = -128;
self.dogY1 = -128;
}
// 画dog
UIImage *dog = [UIImage imageNamed:@"dog"];
[dog drawAtPoint:CGPointMake(50, self.dogY)];
// 画dog1
UIImage *dog1 = [UIImage imageNamed:@"dog1"];
[dog1 drawAtPoint:CGPointMake(150, self.dogY1)];
}

常用拼接路径的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 新建一个起点
void CGContextMoveToPoint(CGContextRef c, CGFloat x, CGFloat y)

// 添加新的线段到某个点
void CGContextAddLineToPoint(CGContextRef c, CGFloat x, CGFloat y)

// 添加一个矩形
void CGContextAddRect(CGContextRef c, CGRect rect)

// 添加一个椭圆
void CGContextAddEllipseInRect(CGContextRef context, CGRect rect)

// 添加一个圆弧
void CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y,
CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise)

常用绘制路径的函数

1
2
3
4
5
6
7
8
9
10
// Mode参数决定绘制的模式
void CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode)

// 绘制空心路径
void CGContextStrokePath(CGContextRef c)

// 绘制实心路径
void CGContextFillPath(CGContextRef c)

提示:一般以CGContextDrawCGContextStrokeCGContextFill开头的函数,都是用来绘制路径的

图形上下文栈的操作

1
2
3
4
5
// 将当前的上下文copy一份,保存到栈顶(那个栈叫做”图形上下文栈”)
void CGContextSaveGState(CGContextRef c)

// 将栈顶的上下文出栈,替换掉当前的上下文
void CGContextRestoreGState(CGContextRef c)

矩阵操作

1
2
3
4
5
6
7
8
9
10
// 利用矩阵操作,能让绘制到上下文中的所有路径一起发生变化

// 缩放
void CGContextScaleCTM(CGContextRef c, CGFloat sx, CGFloat sy)

// 旋转
void CGContextRotateCTM(CGContextRef c, CGFloat angle)

// 平移
void CGContextTranslateCTM(CGContextRef c, CGFloat tx, CGFloat ty)

图片水印

  • 有时候,在手机客户端app中也需要用到水印技术
    • 比如,用户拍完照片后,可以在照片上打个水印,标识这个图片是属于哪个用户的
  • 实现方式:利用Quartz2D,将水印(文字、LOGO)画到图片的右下角
  • 核心代码
1
2
3
4
5
6
7
8
开启一个基于位图的图形上下文
void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)

从上下文中取得图片(UIImage
UIImage* UIGraphicsGetImageFromCurrentImageContext();

结束基于位图的图形上下文
void UIGraphicsEndImageContext();

图片裁剪

  • 有时候需要把一张普通的图片刻意裁剪成圆形
hack_appList_result_failed
1
2
3
// 核心代码
void CGContextClip(CGContextRef c)
// 将当前上下所绘制的路径裁剪出来(超出这个裁剪区域的都不能显示)

屏幕截图

  • 有时候需要截取屏幕上的某一块内容:
1
2
3
 // 核心代码
- (void)renderInContext:(CGContextRef)ctx;
调用某个view的layer的renderInContext:方法即可

贝塞尔曲线应用

  • 矩形
1
2
3
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(10, 10, 200, 200) cornerRadius:100];

[path stroke];
  • 椭圆
1
2
3
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(10, 10, 200, 200)];

[path stroke];
  • 圆弧
1
2
3
4
5
6
7
8
9
10
// center:圆心
// radius:半径
// startAngle:起始角度
// endAngle:结束角度
// clockwise: YES:顺时针 NO:逆时针
CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
CGFloat radius = rect.size.width * 0.5 * 0.5;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:M_PI clockwise:YES];

[path stroke];

注意点

绘制图片的注意点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 绘制图片
UIImage *image = [UIImage imageNamed:@"阿狸头像"];

// drawAtPoint:默认绘制的图片跟图片本身一样
[image drawAtPoint:CGPointZero];

// drawInRect:默认绘制的图片跟控件一样,拉伸跟控件一样大
[image drawInRect:rect];

// 设置裁剪区域的代码必须要在绘制之前
// 设置裁剪区域,超出裁剪区域的部分会被裁剪掉
UIRectClip(CGRectMake(0, 0, 50, 50));

// Pattern:平铺
[image drawAsPatternInRect:rect];

// 快速的填充一个矩形
UIRectFill(CGRectMake(0, 0, 50, 50));

// 超出控件的部分会被裁剪掉
// 绘图的时候,绘制的内容不能超过当前控件

绘制文字的注意点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 文字
NSString *str = @"hello world hello world hello world";

// Attributes:可以给文字添加属性,富文本,字体,颜色,空心,阴影...
// 用一个字典去描述文本属性
NSMutableDictionary *strAttr = [NSMutableDictionary dictionary];

// 设置字典,字典内容 是 一个key对应一个value

// 字体
strAttr[NSFontAttributeName] = [UIFont systemFontOfSize:50];
// 颜色
strAttr[NSForegroundColorAttributeName] = [UIColor redColor];

// 描边
strAttr[NSStrokeWidthAttributeName] = @1;
strAttr[NSStrokeColorAttributeName] = [UIColor greenColor];

// 阴影
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowOffset = CGSizeMake(10, 10);
shadow.shadowColor = [UIColor yellowColor];
shadow.shadowBlurRadius = 5;
strAttr[NSShadowAttributeName] = shadow;

// drawAtPoint不会自动换行
// [str drawAtPoint:CGPointZero withAttributes:strAttr];

// drawInRect可以自动换行
[str drawInRect:self.bounds withAttributes:strAttr];

内存管理

  • 使用含有“Create”或“Copy”的函数创建的对象,使用完后必须释放,否则将导致内存泄露
  • 使用不含有“Create”或“Copy”的函数获取的对象,则不需要释放
  • 如果retain了一个对象,不再使用时,需要将其release
  • 可以使用Quartz 2D的函数来指定retainrelease一个对象。例如,如果创建了一个CGColorSpace对象,则使用函数CGColorSpaceRetainCGColorSpaceRelease来retain和`release对象。
  • 也可以使用Core Foundation的CFRetainCFRelease`。注意不能传递NULL值给这些函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Create\copy\retain ---> release (出现这些关键字就需要手动管理内存)
- (void)drawRect:(CGRect)rect
{
// (调用oc的方法的时候不需要获取上下文)
CGContextRef ctx = UIGraphicsGetCurrentContext();

// 1.先创建一个路径
CGMutablePathRef linePath = CGPathCreateMutable();

// 2.拼接路径
CGPathMoveToPoint(linePath, NULL, 0, 0);
CGPathAddLineToPoint(linePath, NULL, 100, 100);

// 3.添加路径到上下文
CGContextAddPath(ctx, linePath);

CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddArc(circlePath, NULL, 150, 150, 50, 0, M_PI * 2, 0);
CGContextAddPath(ctx, circlePath);

// 4.渲染
CGContextStrokePath(ctx);

// 路径的内存管理
CGPathRelease(linePath);
CGPathRelease(circlePath);

// 颜色空间的内存管路
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
CGColorSpaceRelease(cs);

// 也可以统一使用这个管理内存
// CFRelease(linePath);
// CFRelease(circlePath);
// CFRelease(cs);
}

给UIImage添加分类

添加水印

分类的.h文件

1
2
3
4
5
#import <UIKit/UIKit.h>

@interface UIImage (XSWaterMark)
+ (instancetype)waterMarkImgaeWithBg:(NSString *)bg water:(NSString *)logo;
@end

分类的.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "UIImage+XSWaterMark.h"

@implementation UIImage (XSWaterMark)

+ (instancetype)waterMarkImgaeWithBg:(NSString *)bg water:(NSString *)logo{

// 1.创建背景图片
UIImage *bgImage = [UIImage imageNamed:bg];

// 2.开启(创建)基于位图的上下文
// 上下文 : 基于位图(bitmap) , 所有的东西需要绘制到一张新的图片上去
// size : 新图片的尺寸
// BOOL opaque : Yes 是不透明 NO表示透明(一般都用不透明) scale:一般不用缩放 0.0
// 这行代码过后.就相当于常见一张新的bitmap,也就是新的UIImage对象
UIGraphicsBeginImageContextWithOptions(bgImage.size, NO, 0.0);

// 3.画出背景图片到上下文中
[bgImage drawInRect:CGRectMake(0, 0, bgImage.size.width, bgImage.size.height)];

// 4.创建logo图片 画logo图片到上下文中去
UIImage *logoImage = [UIImage imageNamed:logo];
CGFloat margin = 5; // logo和边框的间隙
CGFloat scale = 0.5;
CGFloat logoW = logoImage.size.width * scale;
CGFloat logoH = logoImage.size.height * scale;
CGFloat logoX = bgImage.size.width - logoW + margin;
CGFloat logoY = bgImage.size.height - logoH - margin;
[logoImage drawInRect:CGRectMake(logoX, logoY, logoW, logoH)];

// 5.获取上下文中生成的新的图片
UIImage *waterImage = UIGraphicsGetImageFromCurrentImageContext();

// 6.结束上下文
UIGraphicsEndImageContext();

return waterImage;
}
@end

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#import "ViewController.h"
#import "UIImage+XSWaterMark.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *iconView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 1.返回水印图片
UIImage *waterImage = [UIImage waterMarkImgaeWithBg:@"bg" water:@"logo"];

// 2.显示图片到view中
self.iconView.image = waterImage;

// 3.将图片压缩为二进制的数据信息
NSData *data = UIImagePNGRepresentation(waterImage);

// 4.写入数据到沙盒document文件夹中
// 4.1.获取写入的沙盒文档路径
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"new.png"];
// 4.2.写入数据
[data writeToFile:path atomically:YES];


// 打印沙盒的位置
// NSLog(@"%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES));
}

圆形切图

分类的.h文件

1
2
3
4
5
6
#import <UIKit/UIKit.h>

@interface UIImage (XSImageClipToCircle)

+ (instancetype)clipImageWithName:(NSString *)name BorderWidth:(CGFloat)borderWidth borderColor:(UIColor *)color;
@end

分类的.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#import "UIImage+XSImageClipToCircle.h"

@implementation UIImage (XSImageClipToCircle)

+ (instancetype)clipImageWithName:(NSString *)name BorderWidth:(CGFloat)borderWidth borderColor:(UIColor *)color{

// 加载原图
UIImage *oldImage = [UIImage imageNamed:name];

// 1.开启上下文
// CGFloat border = 3; // 图片距离边框的边距
CGFloat imageW = oldImage.size.width + 2 * borderWidth;
CGFloat imageH = oldImage.size.height + 2 * borderWidth;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(imageW, imageH), NO, 0.0);

// 2.取得上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();

// 3.画最外面的框的圆
[color set]; // 圆环颜色为白色
CGFloat bigCircleRadius = imageW * 0.5;
CGFloat bigCircleX = bigCircleRadius;
CGFloat bigCircleY = bigCircleRadius;
CGContextAddArc(ctx, bigCircleX, bigCircleY, bigCircleRadius, 0, M_PI * 2, 0);
CGContextFillPath(ctx); // 画出最外面的大圆

// 4.最里面的小圆(和大圆是同心圆)
CGFloat smallCircleRadius = bigCircleRadius - borderWidth;
CGFloat smallCircleX = bigCircleX;
CGFloat smallCircleY = bigCircleY;
CGContextAddArc(ctx, smallCircleX, smallCircleY, smallCircleRadius, 0, M_PI * 2, 0);
// 4.1.裁剪
CGContextClip(ctx);

// 5.画图片到小圆里
CGFloat clipImageX = borderWidth;
CGFloat clipImageY = borderWidth;
CGFloat clipImageW = oldImage.size.width;
CGFloat clipImageH = oldImage.size.height;
[oldImage drawInRect:CGRectMake(clipImageX, clipImageY, clipImageW, clipImageH)];

// 6.取得新的图片
UIImage *clipImage = UIGraphicsGetImageFromCurrentImageContext();

// 7.结束上下文
UIGraphicsEndImageContext();

return clipImage;
}
@end

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import "ViewController.h"
#import "UIImage+XSImageClipToCircle.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *iconView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 1.返回裁剪的图片
UIImage *clipImage = [UIImage clipImageWithName:@"me" BorderWidth:2 borderColor:[UIColor whiteColor]];

// 2.显示图片
self.iconView.image = clipImage;

// 3.新图片压缩为二进制数据
NSData *data = UIImagePNGRepresentation(clipImage);

// 4.写入数据到document文件夹中
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"clip.png"];

[data writeToFile:path atomically:YES];
}

截屏

分类的.h文件

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

@interface UIImage (XSCaptureScreen)

+ (instancetype)captureWithView:(UIView *)view;

@end

分类的.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "UIImage+XSCaptureScreen.h"

@implementation UIImage (XSCaptureScreen)

+ (instancetype)captureWithView:(UIView *)view{

// 1.开启上下文(和控制器view一样尺寸)
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);

// 2.获取上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();

// 3.将控制器view渲染到上下文中
[view.layer renderInContext:ctx];

// 4.获取新的图片
UIImage *captureImage = UIGraphicsGetImageFromCurrentImageContext();

// 5.结束上下文
UIGraphicsEndImageContext();

return captureImage;
}
@end

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import "ViewController.h"
#import "UIImage+XSCaptureScreen.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIView *cyanView;

- (IBAction)CaptureBtnClick;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

}

/**
* 点击截屏调用的方法
*/
- (IBAction)CaptureBtnClick {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

// 1.截图
UIImage *captureImage = [UIImage captureWithView:self.cyanView];

// 2.写入数据
NSData *data = UIImagePNGRepresentation(captureImage);
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"captureImage.png"];
[data writeToFile:path atomically:YES];
});
}
@end
要不要鼓励一下😘