Quartz 2D是一个二维图形绘制引擎,支持iOS环境和Mac OS X环境
Quartz 2D API可以实现许多功能,如基于路径的绘图、透明度、阴影、颜色管理、反锯齿、PDF文档生成和PDF元数据访问等
Quartz 2D API是Core Graphics框架的一部分,因此其中的很多数据类型和方法都是以CG开头的。会经常见到Quartz 2D(Quartz)和Core Graphics两个术语交互使用
Quartz 2D与分辨率和设备无关,因此在使用Quartz 2D绘图时,无需考虑最终绘图的目标设备
Core Graphic框架是一组基于C的API,可以用于一切绘图操作,这个框架的重要性,仅次于UIKit和Foundation
当使用UIKit创建按钮、标签或者其他UIView的子类时,UIKit会用Core Graphics将这些元素绘制在屏幕上。此外,UIEvent(UIKit中的事件处理类)也会使用Core Graphics,用来帮助确定触摸事件在屏幕上所处的位置
因为UIKit依赖于Core Graphics,所以当引入<UIKit/Uikit.h>时,Core Graphics框架会被自动引入,即UIKit内部已经引入了Core Graphics框架的主头文件:<CoreGraphics/CoreGraphics.h>
为了让开发者不必触及底层的Core Graphics的C接口,UIKit内部封装了Core Graphics的一些API,可以快速生成通用的界面元素。但是,有时候直接利用Core Graphics的C接口是很有必要和很有好处的,比如创建一个自定义的界面元素。
Quartz 2D绘图的基本步骤:
1. 获取与视图相关联的上下文对象
UIGraphicsGetCurrentContext
2. 创建及设置路径 (path)
2.1 创建路径
2.2 设置路径起点
2.3 增加路径内容……
3. 将路径添加到上下文
4. 设置上下文状态
边线颜色、填充颜色、线宽、线段连接样式、线段首尾样式、虚线样式…
5. 绘制路径
6. 释放路径
接下来实现一个画板:
#import <Foundation/Foundation.h>
@interface DrawPath : NSObject
+ (id)drawPathWithCGPath:(CGPathRef)drawPath
color:(UIColor *)color
lineWidth:(CGFloat)lineWidth;
@property (strong, nonatomic) UIBezierPath *drawPath;
@property (strong, nonatomic) UIColor *drawColor;
@property (assign, nonatomic) CGFloat lineWith;
// 用户选择的图像
@property (strong, nonatomic) UIImage *image;
@end
#import "DrawPath.h" @implementation DrawPath + (id)drawPathWithCGPath:(CGPathRef)drawPath color:(UIColor *)color lineWidth:(CGFloat)lineWidth { DrawPath *path = [[DrawPath alloc]init]; path.drawPath = [UIBezierPath bezierPathWithCGPath:drawPath]; path.drawColor = color; path.lineWith = lineWidth; return path; } @end
#import "DrawView.h" #import "DrawPath.h" @interface DrawView() // 当前的绘图路径 @property (assign, nonatomic) CGMutablePathRef drawPath; // 绘图路径数组 @property (strong, nonatomic) NSMutableArray *drawPathArray; // 标示路径是否被释放 @property (assign, nonatomic) BOOL pathReleased; @end @implementation DrawView - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setBackgroundColor:[UIColor whiteColor]]; // 设置属性的初始值 self.lineWidth = 10.0; self.drawColor = [UIColor redColor]; } return self; } #pragma mark - 绘制视图 // 注意:drawRect方法每次都是完整的绘制视图中需要绘制部分的内容 - (void)drawRect:(CGRect)rect { // 1. 获取上下文 CGContextRef context = UIGraphicsGetCurrentContext(); [self drawView:context]; } #pragma mark 绘图视图内容的方法 - (void)drawView:(CGContextRef)context { // 首先将绘图数组中的路径全部绘制出来 for (DrawPath *path in self.drawPathArray) { if (path.image == nil) { CGContextAddPath(context, path.drawPath.CGPath); [path.drawColor set]; CGContextSetLineWidth(context, path.lineWith); CGContextSetLineCap(context, kCGLineCapRound); CGContextDrawPath(context, kCGPathStroke); } else { // 有图像,没路径 // CGContextDrawImage(context, self.bounds, path.image.CGImage); [path.image drawInRect:self.bounds]; } } //-------------------------------------------------------- // 以下代码绘制当前路径的内容,就是手指还没有离开屏幕 // 内存管理部分提到,所有create创建的都要release,而不能设置成NULL if (!self.pathReleased) { // 1. 添加路径 CGContextAddPath(context, self.drawPath); // 2. 设置上下文属性 [self.drawColor set]; CGContextSetLineWidth(context, self.lineWidth); CGContextSetLineCap(context, kCGLineCapRound); // 3. 绘制路径 CGContextDrawPath(context, kCGPathStroke); } } #pragma mark - 触摸事件 #pragma mark 触摸开始,创建绘图路径 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView:self]; self.drawPath = CGPathCreateMutable(); // 记录路径没有被释放 self.pathReleased = NO; // 在路径中记录触摸的初始点 CGPathMoveToPoint(self.drawPath, NULL, location.x, location.y); } #pragma mark 移动过程中,将触摸点不断添加到绘图路径 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { // 可以获取到用户当前触摸的点 UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView:self]; // 将触摸点添加至路径 CGPathAddLineToPoint(self.drawPath, NULL, location.x, location.y); [self setNeedsDisplay]; } #pragma mark 触摸结束,释放路径 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // 一笔画完之后,将完整的路径添加到路径数组之中 // 使用数组的懒加载 if (self.drawPathArray == nil) { self.drawPathArray = [NSMutableArray array]; } // 要将CGPathRef添加到NSArray之中,需要借助贝塞尔曲线对象 // 贝塞尔曲线是UIKit对CGPathRef的一个封装,贝塞尔路径的对象可以直接添加到数组 // UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:self.drawPath]; DrawPath *path = [DrawPath drawPathWithCGPath:self.drawPath color:self.drawColor lineWidth:self.lineWidth]; // 需要记录当前绘制路径的颜色和线宽 [self.drawPathArray addObject:path]; CGPathRelease(self.drawPath); // 标示路径已经被释放 self.pathReleased = YES; // 测试线宽的代码 // self.lineWidth = arc4random() % 20 + 1.0; } #pragma mark - 工具视图执行方法 - (void)undo { // 在执行撤销操作时,当前没有绘图路径 // 要做撤销操作,需要把路径数组中的最后一条路径删除 [self.drawPathArray removeLastObject]; [self setNeedsDisplay]; } #pragma mark - 清屏操作 - (void)clearScreen { // 在执行清屏操作时,当前没有绘图路径 // 要做清屏操作,只要把路径数组清空即可 [self.drawPathArray removeAllObjects]; [self setNeedsDisplay]; } #pragma mark - image 的 setter方法 - (void)setImage:(UIImage *)image { /* 目前绘图的方法: 1> 用self.drawPathArray记录已经完成(抬起手指)的路径 2> 用self.drawPath记录当前正在拖动中的路径 绘制时,首先绘制self.drawPathArray,然后再绘制self.drawPath image 传入时,drawPath没有被创建(被release但不是NULL) 如果 1> 我们将image也添加到self.drawPathArray(DrawPath) 2> 在绘图时,根据是否存在image判断是绘制路径还是图像 就可以实现用一个路径数组即绘制路径,又绘制图像的目的 之所以要用一个数组,是因为绘图是有顺序的 接下来,首先需要扩充DrawPath,使其支持image */ // 1. 实例化一个新的DrawPath DrawPath *path = [[DrawPath alloc]init]; [path setImage:image]; // 2. 将其添加到self.drawPathArray,数组是懒加载的 if (self.drawPathArray == nil) { self.drawPathArray = [NSMutableArray array]; } [self.drawPathArray addObject:path]; // 3. 重绘 [self setNeedsDisplay]; } @end
#import <UIKit/UIKit.h> @interface MyButton : UIButton @property (assign, nonatomic) BOOL selectedMyButton; @end #import "MyButton.h" @implementation MyButton - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self.titleLabel setFont:[UIFont systemFontOfSize:12.0]]; [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; } return self; } - (void)drawRect:(CGRect)rect { // 如果selectedMyButton == YES在按钮的下方绘制一条红线 if (self.selectedMyButton) { CGRect frame = CGRectMake(0, self.bounds.size.height - 2, self.bounds.size.width, 2); [[UIColor redColor]set]; UIRectFill(frame); } } #pragma mark - setter方法 - (void)setSelectedMyButton:(BOOL)selectedMyButton { _selectedMyButton = selectedMyButton; [self setNeedsDisplay]; } @end #import <UIKit/UIKit.h> #pragma mark - 定义块代码 typedef void(^SelectColorBlock)(UIColor *color); @interface SelectColorView : UIView // 扩展initWithFrame方法,增加块代码参数 // 该块代码,将在选择颜色按钮之后执行 - (id)initWithFrame:(CGRect)frame afterSelectColor:(SelectColorBlock)afterSelectColor; @end #import "SelectColorView.h" #define kButtonSpace 10.0 @interface SelectColorView() { // 选择颜色的块代码变量 SelectColorBlock _selectColorBlock; } @property (strong, nonatomic) NSArray *colorArray; @end @implementation SelectColorView - (id)initWithFrame:(CGRect)frame afterSelectColor:(SelectColorBlock)afterSelectColor { self = [super initWithFrame:frame]; if (self) { _selectColorBlock = afterSelectColor; [self setBackgroundColor:[UIColor lightGrayColor]]; // 绘制颜色的按钮 NSArray *array = @[[UIColor darkGrayColor], [UIColor redColor], [UIColor greenColor], [UIColor blueColor], [UIColor yellowColor], [UIColor orangeColor], [UIColor purpleColor], [UIColor brownColor], [UIColor blackColor], ]; self.colorArray = array; [self createColorButtonsWithArray:array]; } return self; } #pragma mark - 绘制颜色按钮 - (void)createColorButtonsWithArray:(NSArray *)array { // 1. 计算按钮的位置 // 2. 设置按钮的颜色,需要使用数组 // 按钮的宽度,起始点位置 NSInteger count = array.count; CGFloat width = (self.bounds.size.width - (count + 1) * kButtonSpace)/ count; CGFloat height = self.bounds.size.height; NSInteger index = 0; for (NSString *text in array) { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; CGFloat startX = kButtonSpace + index * (width + kButtonSpace); [button setFrame:CGRectMake(startX, 5, width, height - 10)]; // 设置按钮的背景颜色 [button setBackgroundColor:array[index]]; [button setTag:index]; [button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:button]; index++; } } #pragma mark - 按钮监听方法 - (void)tapButton:(UIButton *)button { // 调用块代码 _selectColorBlock(self.colorArray[button.tag]); } @end #import <UIKit/UIKit.h> #pragma mark - 定义块代码 typedef void(^SelectLineWidthBlock)(CGFloat lineWidth); @interface SelectLineWidthView : UIView #pragma mark 扩展初始化方法,增加块代码 - (id)initWithFrame:(CGRect)frame afterSelectLineWidth:(SelectLineWidthBlock)afterSeletLineWidth; @end #import "SelectLineWidthView.h" // 针对不同的界面,因为按钮的数量是不同的,有时候需要调整按钮间距,保证好的视觉效果 #define kButtonSpace 10.0 @interface SelectLineWidthView() { SelectLineWidthBlock _selectLineWidthBlock; } @property (strong, nonatomic) NSArray *lineWidthArray; @end @implementation SelectLineWidthView - (id)initWithFrame:(CGRect)frame afterSelectLineWidth:(SelectLineWidthBlock)afterSeletLineWidth { self = [super initWithFrame:frame]; if (self) { _selectLineWidthBlock = afterSeletLineWidth; [self setBackgroundColor:[UIColor redColor]]; [self setBackgroundColor:[UIColor lightGrayColor]]; // 绘制颜色的按钮 NSArray *array = @[@(1.0), @(3.0), @(5.0), @(8.0), @(10.0), @(15.0), @(20.0) ]; self.lineWidthArray = array; [self createLineButtonsWithArray:array]; } return self; } #pragma mark 创建选择线宽的按钮 - (void)createLineButtonsWithArray:(NSArray *)array { // 1. 计算按钮的位置 // 2. 设置按钮的颜色,需要使用数组 // 按钮的宽度,起始点位置 NSInteger count = array.count; CGFloat width = (self.bounds.size.width - (count + 1) * kButtonSpace)/ count; CGFloat height = self.bounds.size.height; NSInteger index = 0; for (NSString *text in array) { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; CGFloat startX = kButtonSpace + index * (width + kButtonSpace); [button setFrame:CGRectMake(startX, 5, width, height - 10)]; // 设置选择线宽的提示文字 NSString *text = [NSString stringWithFormat:@"%@点", self.lineWidthArray[index]]; [button setTitle:text forState:UIControlStateNormal]; [button.titleLabel setFont:[UIFont systemFontOfSize:15]]; [button setTag:index]; [button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:button]; index++; } } #pragma mark - 按钮监听方法 - (void)tapButton:(UIButton *)button { // 把按钮对应的线宽数值作为块代码的参数,执行块代码 _selectLineWidthBlock([self.lineWidthArray[button.tag]floatValue]); } @end #import <UIKit/UIKit.h> #import "SelectColorView.h" #import "SelectLineWidthView.h" #pragma mark 工具视图的操作块代码 typedef void(^ToolViewActionBlock)(); //#pragma mark - 选择橡皮擦的块代码定义 //typedef void(^ToolViewSelectEarserBlock)(); //#pragma mark - 选择撤销操作的块代码 //typedef void(^ToolViewSelectUndoBlock)(); //#pragma mark - 选择清屏操作的块代码 //typedef void(^ToolViewSelectClearBlock) (); //#pragma mark - 选择相机的块代码 //typedef void(^ToolViewSelectPhotoBlock) (); @interface ToolView : UIView // 对initWithFrame进行扩展,增加块代码参数 - (id)initWithFrame:(CGRect)frame afterSelectColor:(SelectColorBlock)afterSelectColor afterSelectLineWidth:(SelectLineWidthBlock)afterSelectLineWidth selectEarser:(ToolViewActionBlock)selectEarser seletUndo:(ToolViewActionBlock)selectUndo selectClear:(ToolViewActionBlock)selectClear selectPhoto:(ToolViewActionBlock)selectPhoto; @end #import "ToolView.h" #import "MyButton.h" // 按钮的间距 #define kButtonSpace 10.0 // 注意,枚举的顺序需要和按钮文字数组中的顺序保持一致 typedef enum { kButtonColor = 0, kButtonLineWidth, kButtonEarser, kButtonUndo, kButtonClearScreen, kButtonCamera, kButtonSave } kButtonActionType; @interface ToolView() { SelectColorBlock _selectColorBlock; SelectLineWidthBlock _selectLineWidthBlock; ToolViewActionBlock _selectEarserBlock; ToolViewActionBlock _selectUndoBlock; ToolViewActionBlock _selectClearBlock; ToolViewActionBlock _selectPhotoBlock; } // 当前用户选中的按钮 @property (weak, nonatomic) MyButton *selectButton; // 选择颜色视图 @property (weak, nonatomic) SelectColorView *colorView; // 选择线宽视图 @property (weak, nonatomic) SelectLineWidthView *lineWidthView; @end @implementation ToolView - (id)initWithFrame:(CGRect)frame afterSelectColor:(SelectColorBlock)afterSelectColor afterSelectLineWidth:(SelectLineWidthBlock)afterSelectLineWidth selectEarser:(ToolViewActionBlock)selectEarser seletUndo:(ToolViewActionBlock)selectUndo selectClear:(ToolViewActionBlock)selectClear selectPhoto:(ToolViewActionBlock)selectPhoto { self = [super initWithFrame:frame]; if (self) { _selectColorBlock = afterSelectColor; _selectLineWidthBlock = afterSelectLineWidth; _selectEarserBlock = selectEarser; _selectUndoBlock = selectUndo; _selectClearBlock = selectClear; _selectPhotoBlock = selectPhoto; [self setBackgroundColor:[UIColor lightGrayColor]]; // 通过循环的方式创建按钮 NSArray *array = @[@"颜色", @"线宽", @"橡皮", @"撤销", @"清屏", @"相机", @"保存"]; [self createButtonsWithArray:array]; } return self; } #pragma mark 创建工具视图中的按钮 - (void)createButtonsWithArray:(NSArray *)array { // 按钮的宽度,起始点位置 NSInteger count = array.count; CGFloat width = (self.bounds.size.width - (count + 1) * kButtonSpace)/ count; CGFloat height = self.bounds.size.height; NSInteger index = 0; for (NSString *text in array) { MyButton *button = [MyButton buttonWithType:UIButtonTypeCustom]; CGFloat startX = kButtonSpace + index * (width + kButtonSpace); [button setFrame:CGRectMake(startX, 8, width, height - 16)]; [button setTitle:text forState:UIControlStateNormal]; [button setTag:index]; [button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:button]; // 测试代码 // [button setSelectedMyButton:YES]; index++; } } #pragma mark 按钮监听方法 - (void)tapButton:(MyButton *)button { // 方法1:遍历所有的按钮,将selectedMyButton设置为NO,取消所有的下方红线 // 方法2:在属性中记录前一次选中的按钮,将该按钮的属性设置为NO if (self.selectButton != nil && self.selectButton != button) { [self.selectButton setSelectedMyButton:NO]; } // 通过设置当前按钮selectedMyButton属性,在下方绘制红线 [button setSelectedMyButton:YES]; self.selectButton = button; switch (button.tag) { case kButtonColor: // 点击按钮的时候强行关闭当前显示的子视图 [self forceHideView:self.colorView]; // 显示/隐藏颜色选择视图 [self showHideColorView]; break; case kButtonLineWidth: // 点击按钮的时候强行关闭当前显示的子视图 [self forceHideView:self.lineWidthView]; // 显示/隐藏选择线宽视图 [self showHideLineWidthView]; break; case kButtonEarser: // 以变量的方式调用视图控制器的块代码 _selectEarserBlock(); [self forceHideView:nil]; break; case kButtonUndo: _selectUndoBlock(); [self forceHideView:nil]; break; case kButtonClearScreen: _selectClearBlock(); [self forceHideView:nil]; break; case kButtonCamera: _selectPhotoBlock(); [self forceHideView:nil]; break; default: break; } } #pragma mark - 子视图操作方法 #pragma mark 强行隐藏当前显示的子视图 // 如果显示的视图与传入的比较视图相同,就不再关闭 - (void)forceHideView:(UIView *)compareView { // 1. 用属性记录当前显示的子视图,强行关闭该视图即可 // 2. 遍历所有子视图,如果处于显示状态,则将其关闭 // 3. 直接判断子视图,此方法仅适用于子数图数量极少 UIView *view = nil; if (self.colorView.frame.origin.y > 0) { view = self.colorView; } else if (self.lineWidthView.frame.origin.y > 0) { view = self.lineWidthView; } else { return; } if (view == compareView) { return; } CGRect toFrame = view.frame; CGRect toolFrame = self.frame; toFrame.origin.y = -44; toolFrame.size.height = 44; [UIView animateWithDuration:0.5f animations:^{ [self setFrame:toolFrame]; [view setFrame:toFrame]; }]; } #pragma mark 显示隐藏指定视图 - (void)showHideView:(UIView *)view { // 2. 动画显示颜色视图 CGRect toFrame = view.frame; // 工具条视图边框 CGRect toolFrame = self.frame; if (toFrame.origin.y < 0) { // 隐藏的我们显示 toFrame.origin.y = 44; toolFrame.size.height = 88; } else { toFrame.origin.y = -44; toolFrame.size.height = 44; } [UIView animateWithDuration:0.5f animations:^{ [self setFrame:toolFrame]; [view setFrame:toFrame]; }]; } #pragma mark 显示隐藏选择线宽视图 - (void)showHideLineWidthView { // 1. 懒加载选择线宽视图 if (self.lineWidthView == nil) { SelectLineWidthView *view = [[SelectLineWidthView alloc]initWithFrame:CGRectMake(0, -44, 320, 44) afterSelectLineWidth:^(CGFloat lineWidth) { NSLog(@"%f", lineWidth); _selectLineWidthBlock(lineWidth); // 强行关闭线宽选择子视图 [self forceHideView:nil]; }]; [self addSubview:view]; self.lineWidthView = view; } [self showHideView:self.lineWidthView]; } #pragma mark 显示隐藏颜色视图 - (void)showHideColorView { // 1. 懒加载颜色视图 if (self.colorView == nil) { SelectColorView *view = [[SelectColorView alloc]initWithFrame:CGRectMake(0, -44, 320, 44) afterSelectColor:^(UIColor *color) { // 以函数的方式调用块代码变量 _selectColorBlock(color); // 选中颜色后,强行关闭颜色选择子视图 [self forceHideView:nil]; NSLog(@"aaah"); }]; [self addSubview:view]; self.colorView = view; } [self showHideView:self.colorView]; } @end #import "MainViewController.h" #import "DrawView.h" #import "ToolView.h" @interface MainViewController () @property (weak, nonatomic) DrawView *drawView; @end @implementation MainViewController #pragma mark - 实例化视图 - (void)loadView { self.view = [[UIView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];; DrawView *drawView = [[DrawView alloc]initWithFrame:self.view.bounds]; [self.view addSubview:drawView]; self.drawView = drawView; ToolView *toolView = [[ToolView alloc]initWithFrame:CGRectMake(0, 0, 320, 44) afterSelectColor:^(UIColor *color) { // 给绘图视图设置颜色 [drawView setDrawColor:color]; } afterSelectLineWidth:^(CGFloat lineWidth) { // 工具视图选择线宽之后,需要执行的代码 [drawView setLineWidth:lineWidth]; } selectEarser:^{ [drawView setDrawColor:[UIColor whiteColor]]; [drawView setLineWidth:30.0]; } seletUndo:^{ [drawView undo]; } selectClear:^{ [drawView clearScreen]; } selectPhoto:^{ // 弹出图像选择窗口,来选择照片 UIImagePickerController *picker = [[UIImagePickerController alloc]init]; // 1. 设置照片源 [picker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]; // 2. 设置代理 [picker setDelegate:self]; // 3. 显示 [self presentViewController:picker animated:YES completion:nil]; }]; [self.view addSubview:toolView]; } #pragma mark - 照片选择代理方法 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { UIImage *image = info[@"UIImagePickerControllerOriginalImage"]; // 设置绘图视图 [self.drawView setImage:image]; // 关闭照片选择窗口 [self dismissViewControllerAnimated:YES completion:nil]; } @end