【iOS】Quartz 2D之画板

画板听起来很难,其实做起来很简单。主要很多人不明白思路,所以觉得难以入手。

场景:实现清除、撤销、重做、照片、保存、颜色、线宽等功能的画板。

一、功能开发流程

1.1. UI使用storyboard进行快速搭建

1.2. 自定义绘图区域

画板功能相对来说比较聚合,所有的功能最终都是通过drawRect绘制到画板上的,同时为了方便其他项目使用,我们尽量把功能都集成在一个画板上。

画板使用手势计算路径会更加合理高效,所以我们使用滑动手势来绘制。

DBDrawboardView.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
@implementation DBDrawboardView

- (void)awakeFromNib {
[super awakeFromNib];
[self initConfig];
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initConfig];
}
return self;
}

// 初始化配置
- (void)initConfig {
// 添加滑动手势
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureEventHandle:)];
[self addGestureRecognizer:panGesture];
}

// 滑动手势事件处理
- (void)panGestureEventHandle:(UIPanGestureRecognizer *)gesture {

}

@end

1.3. 基本线条绘制

线条的绘制使用贝塞尔曲线,只需要两个点就可以连成一条直线。当两个点之间的距离很小的时候(颗粒度)就会觉得线条很自然。

第一步,先在手势开始的时候创建一个贝塞尔曲线(每次手势只会创建一次),并记录起点;

第二步,手势状态发生改变时,把手势移动的位置点拼接到曲线路径上,最后渲染就行了。

DBDrawboardView.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
@interface DBDrawboardView ()

// 记录当前操作路径
@property (nonatomic, weak) UIBezierPath *path;

@end

@implementation DBDrawboardView

// 滑动手势事件处理
- (void)panGestureEventHandle:(UIPanGestureRecognizer *)gesture {
CGPoint current_point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) {
// 1.添加路径起点
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:current_point];
// 2.记录当前操作路径
self.path = path;
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 3.拼接曲线路径
[self.path addLineToPoint:current_point];
}
// 4.绘制到画板上
[self setNeedsDisplay];
}

// 绘制
- (void)drawRect:(CGRect)rect {
[self.path stroke];
}

@end

通过上面的简单几行代码,就可以绘制出基本线条了。但是每次只能绘制一条连续的线条,正常画板是可以绘制不同状态多个线条的,其实只需要添加一个路径栈就行。

1.4. 路径栈

使用栈来保存每一条路径,等需要绘制的时候,从栈底依次取出。OC中最适合的容器栈就是数组了。

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
// 保存历史栈
@property (nonatomic, strong) NSMutableArray *pathArray;

// 滑动手势事件处理
- (void)panGestureEventHandle:(UIPanGestureRecognizer *)gesture {
CGPoint current_point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) {
// 1.添加路径起点
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:current_point];
// 2.记录当前操作路径
self.path = path;
// 3.保存到历史栈
[self.pathArray addObject:path];
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 4.拼接曲线路径
[self.path addLineToPoint:current_point];
}
// 5.绘制到画板上
[self setNeedsDisplay];
}

// 绘制
- (void)drawRect:(CGRect)rect {
for (UIBezierPath *path in self.pathArray) {
[path stroke];
}
}

1.4和1.3的diff:

1
2
3
4
5
6
7
@property (nonatomic, strong) NSMutableArray *pathArray;

[self.pathArray addObject:path];

for (UIBezierPath *path in self.pathArray) {
[path stroke];
}

这样,整个画板的基础功能就算完成了,只需要把其他高级功能完善。

1.5. 设置颜色和线宽

颜色和线宽在自定义的画板view内部实现后,只需要对外提供接口。由于UIBezierPath不支持设置线条颜色,所以我们需要新建一个子类扩展一个颜色属性。

DBDrawboardView

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// DBDrawboardView.h
@interface DBDrawboardView : UIView

// 线宽,默认1.0
@property (nonatomic) CGFloat lineWidth;
// 颜色,默认黑色
@property (nonatomic, strong) UIColor *lineColor;

@end

@interface DBDrawBezierPath : UIBezierPath

@property (nonatomic, strong) UIColor *lineColor;

@end

// DBDrawboardView.m

@interface DBDrawboardView ()

// 记录当前操作路径
@property (nonatomic, weak) DBDrawBezierPath *path;
// 保存历史栈
@property (nonatomic, strong) NSMutableArray *pathArray;

@end

@implementation DBDrawBezierPath

// 初始化配置
- (void)initConfig {
// 添加滑动手势
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureEventHandle:)];
[self addGestureRecognizer:panGesture];

// 设置默认状态
self.lineColor = [UIColor blackColor];
self.lineWidth = 1.0;
}

// 滑动手势事件处理
- (void)panGestureEventHandle:(UIPanGestureRecognizer *)gesture {
CGPoint current_point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) {
// 1.添加路径起点
DBDrawBezierPath *path = [DBDrawBezierPath bezierPath];
[path moveToPoint:current_point];
// 2.设置上下文状态
path.lineJoinStyle = kCGLineJoinRound;
path.lineCapStyle = kCGLineCapRound;
path.lineColor = self.lineColor;
path.lineWidth = self.lineWidth;
// 3.记录当前操作路径
self.path = path;
// 4.保存到历史栈
[self.pathArray addObject:path];
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 5.拼接曲线路径
[self.path addLineToPoint:current_point];
}
[self setNeedsDisplay];
}

// 绘制
- (void)drawRect:(CGRect)rect {
for (DBDrawBezierPath *path in self.pathArray) {
[path.lineColor set];
[path stroke];
}
}

@end

1.6. 橡皮擦

橡皮擦是真的没有什么要说的,就是一个和画板背景色一样的路径。会Photoshop的同学应该不陌生,毕竟橡皮擦掉的东西也是可以撤回的。

1
2
3
4
// 橡皮擦
- (void)eraser {
self.lineColor = self.backgroundColor;
}

1.7. 清空

把当前路径栈元素清空,重新绘制。

1
2
[self.pathArray removeAllObjects];
[self setNeedsDisplay];

1.8. 撤回和重做

同样可以对比Photoshop的历史记录面板,撤回的时候可以重做,重做后还可以撤回,重做的思路和撤回基本一致,一个操作的是当前路径栈,一个操作的移除栈。但是撤回或重做的时候,不管层级关系是怎样的,只要有任何影响画板重绘的操作,重做都会失效。

撤回

把路径栈栈顶的路径去除,同时把该路径添加到移除栈中,重新绘制。

1
2
3
4
5
6
7
8
if (self.pathArray.count != 0) {
// 1.把最后一个路径移入到移除栈中
[self.removeArray addObject:self.pathArray.lastObject];
// 2.把路径从当前栈中移除
[self.pathArray removeLastObject];
// 3.重绘
[self setNeedsDisplay];
}

重做

把移除栈中的栈顶路径移除,并压入到当前栈,重新绘制。

1
2
3
4
5
6
7
8
if (self.removeArray.count != 0) {
// 1.把移除栈中的最后一个路径移入到当前栈中
[self.pathArray addObject:self.removeArray.lastObject];
// 2.把路径从移除栈中移除
[self.removeArray removeLastObject];
// 3.重绘
[self setNeedsDisplay];
}

但是仅仅这样做还是有问题的,上面提到其他任何操作都会影响重做是否有效。所以我们需要记录下每个功能的状态,更新重做功能的时效性。并且橡皮擦之后有清空操作,画板颜色如果没有调整,会看不到绘制的内容。

1.9. 优化

新建一个记录画板不同绘制状态的变量。每次操作都记录对应状态,然后及时更新画板就行了。

DBDrawboardView

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// DBDrawboardView.h
typedef NS_ENUM(NSUInteger, DBDrawboardDrawState) {
DBDrawboardDrawStateNormal, // 普通绘制
DBDrawboardDrawStateClear, // 清空
DBDrawboardDrawStateUndo, // 撤销
DBDrawboardDrawStateRedo, // 重做
DBDrawboardDrawStateEraser // 橡皮擦
};

@interface DBDrawboardView : UIView

// 线宽,默认1.0
@property (nonatomic) CGFloat lineWidth;
// 颜色,默认黑色
@property (nonatomic, strong) UIColor *lineColor;

/*
* 清除
*/
- (void)clear;

/*
* 撤销
*/
- (void)undo;

/*
* 重做
*/
- (void)redo;

/*
* 橡皮擦
*/
- (void)eraser;

@end


@interface DBDrawBezierPath : UIBezierPath

@property (nonatomic, strong) UIColor *lineColor;

@end

// DBDrawboardView.m
@interface DBDrawboardView ()

// 记录当前操作路径
@property (nonatomic, weak) DBDrawBezierPath *path;
// 保存历史栈
@property (nonatomic, strong) NSMutableArray *pathArray;
// 移除保存栈
@property (nonatomic, strong) NSMutableArray *removeArray;
// 当前绘制状态
@property (nonatomic) DBDrawboardDrawState currentState;
// 正常绘制颜色
@property (nonatomic, strong) UIColor *normalColor;

@end

@implementation DBDrawboardView

- (void)awakeFromNib {
[super awakeFromNib];
[self initConfig];
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initConfig];
}
return self;
}

// 初始化配置
- (void)initConfig {
// 添加滑动手势
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureEventHandle:)];
[self addGestureRecognizer:panGesture];

// 设置默认状态
self.lineColor = [UIColor blackColor];
self.lineWidth = 1.0;
}

#pragma mark - Public Method
// 清空
- (void)clear {
self.currentState = DBDrawboardDrawStateClear;
// 防止橡皮擦状态直接清空后画笔还是白色
self.lineColor = self.normalColor;
if (self.pathArray.count != 0) {
[self.pathArray removeAllObjects];
[self.removeArray removeLastObject];
[self setNeedsDisplay];
}
}

// 撤销
- (void)undo {
self.currentState = DBDrawboardDrawStateUndo;
if (self.pathArray.count != 0) {
// 1.把最后一个路径移入到移除栈中
[self.removeArray addObject:self.pathArray.lastObject];
// 2.把路径从当前栈中移除
[self.pathArray removeLastObject];
// 3.重绘
[self setNeedsDisplay];
}
}

// 重做
- (void)redo {
// 防止已经绘制,重做还是有数据
if (self.currentState == DBDrawboardDrawStateNormal) {
[self.removeArray removeAllObjects];
} else {
self.currentState = DBDrawboardDrawStateRedo;
if (self.removeArray.count != 0) {
// 1.把移除栈中的最后一个路径移入到当前栈中
[self.pathArray addObject:self.removeArray.lastObject];
// 2.把路径从移除栈中移除
[self.removeArray removeLastObject];
// 3.重绘
[self setNeedsDisplay];
}
}
}

// 橡皮擦
- (void)eraser {
self.currentState = DBDrawboardDrawStateEraser;
_lineColor = self.backgroundColor;
}

#pragma mark - Private Method
// 滑动手势事件处理
- (void)panGestureEventHandle:(UIPanGestureRecognizer *)gesture {
CGPoint current_point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) {
self.currentState = DBDrawboardDrawStateNormal;
// 1.添加路径起点
DBDrawBezierPath *path = [DBDrawBezierPath bezierPath];
[path moveToPoint:current_point];
// 2.设置上下文状态
path.lineJoinStyle = kCGLineJoinRound;
path.lineCapStyle = kCGLineCapRound;
path.lineColor = self.lineColor;
path.lineWidth = self.lineWidth;
// 3.记录当前操作路径
self.path = path;
// 4.保存到历史栈
[self.pathArray addObject:path];
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 5.拼接曲线路径
[self.path addLineToPoint:current_point];
}
[self setNeedsDisplay];
}

// 绘制
- (void)drawRect:(CGRect)rect {
for (id path in self.pathArray) {
if ([path isKindOfClass:[DBDrawBezierPath class]]) {
DBDrawBezierPath *linePath = (DBDrawBezierPath *)path;
[linePath.lineColor set];
[linePath stroke];
} else if ([path isKindOfClass:[UIImage class]]) {
UIImage *image = (UIImage *)path;
CGFloat margin = 30.0;
[image drawInRect:CGRectMake(margin, margin, rect.size.width-margin*2, rect.size.height-margin*2)];
}
}
}

#pragma mark - Setter and Getter
- (void)setLineColor:(UIColor *)lineColor {
_lineColor = lineColor;
self.normalColor = lineColor;
}

- (NSMutableArray *)pathArray {
if (!_pathArray) {
_pathArray = [NSMutableArray array];
}
return _pathArray;
}

- (NSMutableArray *)removeArray {
if (!_removeArray) {
_removeArray = [NSMutableArray array];
}
return _removeArray;
}

@end

@implementation DBDrawBezierPath

@end

1.10. 保存

把画板内容截屏保存到本地相册。

注意:相册权限需要配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 保存
- (IBAction)save:(id)sender {
// 截屏
UIGraphicsBeginImageContext(self.drawboardView.bounds.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.drawboardView.layer drawInContext:ctx];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// 保存到相册
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
}

// 图片保存结果
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
if (error) {
NSLog(@"保存失败:%@", error);
} else {
NSLog(@"保存成功");
}
}

1.11. 添加照片

照片可以缩小、平移、旋转等形变操作,使用自定义view装载图片,并把形变操作直接应用到这个view上会更好。

如果直接把图片添加到画板上,将来绘制图片的时候图片就会和画板一样大,不是我们要的效果。如果我们把图片先放到一个和画板尺寸一样的透明view上,在这个透明view上进行形变操作,最后把透明view渲染到画板上就可以解决问题了。

1.11.1. 自定义装载图片的view

DBImageContainerView

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// DBImageContainerView.h
@protocol DBImageContainerViewDelegate <NSObject>

/**
* 生成图片
*/
- (void)imageContainerViewForGenerateImage:(UIImage *)image;

@end

@interface DBImageContainerView : UIView

@property (nonatomic, strong) UIImage *image;

@end

// DBImageContainerView.m
#import "DBImageContainerView.h"

@interface DBImageContainerView ()<UIGestureRecognizerDelegate>

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation DBImageContainerView

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
CGRect rect = {{0, 0}, frame.size};
self.imageView.frame = rect;
[self addSubview:self.imageView];
[self addGestures];
}
return self;
}

- (void)setImage:(UIImage *)image {
_image = image;
self.imageView.image = image;
}

// 添加手势
- (void)addGestures {
// 拖拽
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
[self.imageView addGestureRecognizer:panGesture];

// 捏合
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGesture:)];
pinchGesture.delegate = self;
[self.imageView addGestureRecognizer:pinchGesture];

// 旋转
UIRotationGestureRecognizer *rotationGesture = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotationGesture:)];
rotationGesture.delegate = self;
[self.imageView addGestureRecognizer:rotationGesture];

// 长按
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
[self.imageView addGestureRecognizer:longPressGesture];
}

// 拖动
- (void)panGesture:(UIPanGestureRecognizer *)gesture {
CGPoint translate_point = [gesture translationInView:gesture.view];
gesture.view.transform = CGAffineTransformTranslate(gesture.view.transform, translate_point.x, translate_point.y);
[gesture setTranslation:CGPointZero inView:gesture.view];
}

// 捏合
- (void)pinchGesture:(UIPinchGestureRecognizer *)gesture {
gesture.view.transform = CGAffineTransformScale(gesture.view.transform, gesture.scale, gesture.scale);
gesture.scale = 1.0;
}

// 旋转
- (void)rotationGesture:(UIRotationGestureRecognizer *)gesture {
gesture.view.transform = CGAffineTransformRotate(gesture.view.transform, gesture.rotation);
gesture.rotation = 0.0;
}

// 长按
- (void)longPressGesture:(UILongPressGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
[UIView animateWithDuration:0.25 animations:^{
self.imageView.alpha = 0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.25 animations:^{
self.imageView.alpha = 1;
} completion:^(BOOL finished) {
// 截屏
UIGraphicsBeginImageContext(self.bounds.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.layer renderInContext:ctx];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (self.delegate && [self.delegate respondsToSelector:@selector(imageContainerViewForGenerateImage:)]) {
[self.delegate imageContainerViewForGenerateImage:image];
}
[self removeFromSuperview];
}];
}];
}
}

#pragma mark - UIGestureRecognizerDelegate
// 允许同时支持多个手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}

#pragma mark - Setter and Getter
- (UIImageView *)imageView {
if (!_imageView) {
_imageView = [[UIImageView alloc] init];
_imageView.userInteractionEnabled = YES;
}
return _imageView;
}

@end

1.11.2. 渲染

在画板视图中绘制添加的图片
DBImageContainerView.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加照片
- (void)addPhoto:(UIImage *)image {
DBImageContainerView *imageContainerView = [[DBImageContainerView alloc] initWithFrame:self.bounds];
imageContainerView.delegate = self;
imageContainerView.image = image;
[self addSubview:imageContainerView];
}

#pragma mark - DBImageContainerViewDelegate
// 生成图片
- (void)imageContainerViewForGenerateImage:(UIImage *)image {
[self.pathArray addObject:image];
[self setNeedsDisplay];
}

二、完整代码及效果

ViewController.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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#import "ViewController.h"
#import "DBDrawboardView.h"

@interface ViewController ()<UIImagePickerControllerDelegate, UINavigationControllerDelegate>

// 画板
@property (weak, nonatomic) IBOutlet DBDrawboardView *drawboardView;
// 当前选中的按钮
@property (strong, nonatomic) UIButton *currentBtn;
@property (weak, nonatomic) IBOutlet UIButton *firstBtn;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.currentBtn = self.firstBtn;
}

// 清空
- (IBAction)clear:(id)sender {
[self.drawboardView clear];
}

// 撤销
- (IBAction)undo:(id)sender {
[self.drawboardView undo];
}

// 重做
- (IBAction)redo:(id)sender {
[self.drawboardView redo];
}

// 橡皮擦
- (IBAction)eraser:(id)sender {
[self.drawboardView eraser];
}

// 照片
- (IBAction)photos:(id)sender {
UIImagePickerController *pickerVC = [[UIImagePickerController alloc] init];
pickerVC.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
pickerVC.delegate = self;
[self presentViewController:pickerVC animated:YES completion:nil];
}

// 保存
- (IBAction)save:(id)sender {
// 截屏
UIGraphicsBeginImageContext(self.drawboardView.bounds.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.drawboardView.layer drawInContext:ctx];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// 保存到相册
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
}

// 图片保存结果
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
if (error) {
NSLog(@"保存失败:%@", error);
} else {
NSLog(@"保存成功");
}
}

// 修改颜色
- (IBAction)colorChanged:(UIButton *)sender {
self.currentBtn = sender;
}

// 修改线宽
- (IBAction)lineWidthChanged:(UISlider *)sender {
self.drawboardView.lineWidth = sender.value;
}

#pragma mark - UIImagePickerControllerDelegate
// 图片选择完成
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {
UIImage *image = info[UIImagePickerControllerOriginalImage];
[self.drawboardView addPhoto:image];
[picker dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - Setter and Getter
- (void)setCurrentBtn:(UIButton *)currentBtn {
if (![_currentBtn isEqual:currentBtn]) {
_currentBtn.transform = CGAffineTransformIdentity;
currentBtn.transform = CGAffineTransformMakeScale(1.2, 1.2);
_currentBtn = currentBtn;
self.drawboardView.lineColor = currentBtn.backgroundColor;
}
}

- (BOOL)prefersStatusBarHidden {
return YES;
}

@end

DBDrawboardView

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// DBDrawboardView.h
typedef NS_ENUM(NSUInteger, DBDrawboardDrawState) {
DBDrawboardDrawStateNormal, // 普通绘制
DBDrawboardDrawStateClear, // 清空
DBDrawboardDrawStateUndo, // 撤销
DBDrawboardDrawStateRedo, // 重做
DBDrawboardDrawStateEraser // 橡皮擦
};

@interface DBDrawboardView : UIView

// 线宽,默认1.0
@property (nonatomic) CGFloat lineWidth;
// 颜色,默认黑色
@property (nonatomic, strong) UIColor *lineColor;

/*
* 清除
*/
- (void)clear;

/*
* 撤销
*/
- (void)undo;

/*
* 重做
*/
- (void)redo;

/*
* 橡皮擦
*/
- (void)eraser;

/*
* 添加图片
* @param image 图片
*/
- (void)addPhoto:(UIImage *)image;

@end


@interface DBDrawBezierPath : UIBezierPath

@property (nonatomic, strong) UIColor *lineColor;

@end

// DBDrawboardView.m
#import "DBDrawboardView.h"
#import "DBImageContainerView.h"

@interface DBDrawboardView ()<DBImageContainerViewDelegate>

// 记录当前操作路径
@property (nonatomic, weak) DBDrawBezierPath *path;
// 保存历史栈
@property (nonatomic, strong) NSMutableArray *pathArray;
// 移除保存栈
@property (nonatomic, strong) NSMutableArray *removeArray;
// 当前绘制状态
@property (nonatomic) DBDrawboardDrawState currentState;
// 正常绘制颜色
@property (nonatomic, strong) UIColor *normalColor;

@end

@implementation DBDrawboardView

- (void)awakeFromNib {
[super awakeFromNib];
[self initConfig];
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initConfig];
}
return self;
}

// 初始化配置
- (void)initConfig {
self.clipsToBounds = YES;
// 添加滑动手势
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureEventHandle:)];
[self addGestureRecognizer:panGesture];

// 设置默认状态
self.lineColor = [UIColor blackColor];
self.lineWidth = 1.0;
}

#pragma mark - Public Method
// 清空
- (void)clear {
self.currentState = DBDrawboardDrawStateClear;
// 防止橡皮擦状态直接清空后画笔还是白色
self.lineColor = self.normalColor;
if (self.pathArray.count != 0) {
[self.pathArray removeAllObjects];
[self.removeArray removeLastObject];
[self setNeedsDisplay];
}
}

// 撤销
- (void)undo {
self.currentState = DBDrawboardDrawStateUndo;
if (self.pathArray.count != 0) {
// 1.把最后一个路径移入到移除栈中
[self.removeArray addObject:self.pathArray.lastObject];
// 2.把路径从当前栈中移除
[self.pathArray removeLastObject];
// 3.重绘
[self setNeedsDisplay];
}
}

// 重做
- (void)redo {
// 防止已经绘制,重做还是有数据
if (self.currentState == DBDrawboardDrawStateNormal) {
[self.removeArray removeAllObjects];
} else {
self.currentState = DBDrawboardDrawStateRedo;
if (self.removeArray.count != 0) {
// 1.把移除栈中的最后一个路径移入到当前栈中
[self.pathArray addObject:self.removeArray.lastObject];
// 2.把路径从移除栈中移除
[self.removeArray removeLastObject];
// 3.重绘
[self setNeedsDisplay];
}
}
}

// 橡皮擦
- (void)eraser {
self.currentState = DBDrawboardDrawStateEraser;
_lineColor = self.backgroundColor;
}

// 添加照片
- (void)addPhoto:(UIImage *)image {
DBImageContainerView *imageContainerView = [[DBImageContainerView alloc] initWithFrame:self.bounds];
imageContainerView.delegate = self;
imageContainerView.image = image;
[self addSubview:imageContainerView];
}

#pragma mark - DBImageContainerViewDelegate
// 生成图片
- (void)imageContainerViewForGenerateImage:(UIImage *)image {
[self.pathArray addObject:image];
[self setNeedsDisplay];
}

#pragma mark - Private Method
// 滑动手势事件处理
- (void)panGestureEventHandle:(UIPanGestureRecognizer *)gesture {
CGPoint current_point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) {
self.currentState = DBDrawboardDrawStateNormal;
// 1.添加路径起点
DBDrawBezierPath *path = [DBDrawBezierPath bezierPath];
[path moveToPoint:current_point];
// 2.设置上下文状态
path.lineJoinStyle = kCGLineJoinRound;
path.lineCapStyle = kCGLineCapRound;
path.lineColor = self.lineColor;
path.lineWidth = self.lineWidth;
// 3.记录当前操作路径
self.path = path;
// 4.保存到历史栈
[self.pathArray addObject:path];
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 5.拼接曲线路径
[self.path addLineToPoint:current_point];
}
[self setNeedsDisplay];
}

// 绘制
- (void)drawRect:(CGRect)rect {
for (id path in self.pathArray) {
if ([path isKindOfClass:[DBDrawBezierPath class]]) {
DBDrawBezierPath *linePath = (DBDrawBezierPath *)path;
[linePath.lineColor set];
[linePath stroke];
} else if ([path isKindOfClass:[UIImage class]]) {
UIImage *image = (UIImage *)path;
CGFloat margin = 30.0;
[image drawInRect:CGRectMake(margin, margin, rect.size.width-margin*2, rect.size.height-margin*2)];
}
}
}

#pragma mark - Setter and Getter
- (void)setLineColor:(UIColor *)lineColor {
_lineColor = lineColor;
self.normalColor = lineColor;
}

- (NSMutableArray *)pathArray {
if (!_pathArray) {
_pathArray = [NSMutableArray array];
}
return _pathArray;
}

- (NSMutableArray *)removeArray {
if (!_removeArray) {
_removeArray = [NSMutableArray array];
}
return _removeArray;
}

@end

@implementation DBDrawBezierPath

@end

DBImageContainerView

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// DBImageContainerView.h
#import <UIKit/UIKit.h>

@protocol DBImageContainerViewDelegate <NSObject>

/**
* 生成图片
*/
- (void)imageContainerViewForGenerateImage:(UIImage *)image;

@end

@interface DBImageContainerView : UIView

@property (nonatomic, weak) id<DBImageContainerViewDelegate> delegate;
@property (nonatomic, strong) UIImage *image;

@end

// DBImageContainerView.m
#import "DBImageContainerView.h"

@interface DBImageContainerView ()<UIGestureRecognizerDelegate>

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation DBImageContainerView

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
CGRect rect = {{0, 0}, frame.size};
self.imageView.frame = rect;
[self addSubview:self.imageView];
[self addGestures];
}
return self;
}

- (void)setImage:(UIImage *)image {
_image = image;
self.imageView.image = image;
}

// 添加手势
- (void)addGestures {
// 拖拽
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
[self.imageView addGestureRecognizer:panGesture];

// 捏合
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGesture:)];
pinchGesture.delegate = self;
[self.imageView addGestureRecognizer:pinchGesture];

// 旋转
UIRotationGestureRecognizer *rotationGesture = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotationGesture:)];
rotationGesture.delegate = self;
[self.imageView addGestureRecognizer:rotationGesture];

// 长按
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
[self.imageView addGestureRecognizer:longPressGesture];
}

// 拖动
- (void)panGesture:(UIPanGestureRecognizer *)gesture {
CGPoint translate_point = [gesture translationInView:gesture.view];
gesture.view.transform = CGAffineTransformTranslate(gesture.view.transform, translate_point.x, translate_point.y);
[gesture setTranslation:CGPointZero inView:gesture.view];
}

// 捏合
- (void)pinchGesture:(UIPinchGestureRecognizer *)gesture {
gesture.view.transform = CGAffineTransformScale(gesture.view.transform, gesture.scale, gesture.scale);
gesture.scale = 1.0;
}

// 旋转
- (void)rotationGesture:(UIRotationGestureRecognizer *)gesture {
gesture.view.transform = CGAffineTransformRotate(gesture.view.transform, gesture.rotation);
gesture.rotation = 0.0;
}

// 长按
- (void)longPressGesture:(UILongPressGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
[UIView animateWithDuration:0.0 animations:^{
// self.imageView.alpha = 0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.0 animations:^{
// self.imageView.alpha = 1;
} completion:^(BOOL finished) {
// 截屏
UIGraphicsBeginImageContext(self.bounds.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.layer renderInContext:ctx];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (self.delegate && [self.delegate respondsToSelector:@selector(imageContainerViewForGenerateImage:)]) {
[self.delegate imageContainerViewForGenerateImage:image];
}
[self removeFromSuperview];
}];
}];
}
}

#pragma mark - UIGestureRecognizerDelegate
// 允许同时支持多个手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}

#pragma mark - Setter and Getter
- (UIImageView *)imageView {
if (!_imageView) {
_imageView = [[UIImageView alloc] init];
_imageView.userInteractionEnabled = YES;
}
return _imageView;
}

@end