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