#Quartz 2D学习记录
Quartz 2D简单介绍
一、什么是Quartz 2D
Quarz 2D是一个二维绘画引擎,同时支持ios和mac,其API是Core Graphics框架的,是纯C语言的。IOS系统提供的大部分控件的内容都是通过Quartz 2D画出来的,因此Quartz 2D的一个很重要的价值是:自定义view(自定义UI控件)。
二、一个重要的概念:图形上下文
图形上下文(Graphics context)是一个CGContextRef数据,其作用是:
- 保存绘图信息、绘图属性
- 绘制目标图案
- 输出绘制好的图案到输出目标去,即渲染到什么地方去(可以是PDF文件、bitmap或者显示器的窗口上)
三、自定义view,即绘制view
在view中实现- (void)drawRect:(CGRect)rect方法,然后在方法中:
1. 取得跟当前view相关联的图形上下文;
2. 绘制相应的图形内容
3. 利用图形上下文将绘制好的图形内容渲染显示到view上面
Quartz 2D简单使用
代码示例
//绘制步骤:1、获取当前视图的图形上下文 2、开始绘图 3、渲染绘制内容
/**绘制直线**/
//1、获取当前视图的图形上下文(图形上下文决定绘制的输出目标)
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2、开始绘图
//设置起点
CGContextMoveToPoint(ctx, 20, 50);
//设置终点
CGContextAddLineToPoint(ctx, 300, 50);
//设置线条属性
//设置颜色
CGContextSetStrokeColorWithColor(ctx, [UIColor purpleColor].CGColor);
//另外一种设置颜色的方式
// [[UIColor purpleColor] set];
//设置线条宽度
CGContextSetLineWidth(ctx, 10);
//设置线条起点和终点的样式为圆角
CGContextSetLineCap(ctx, kCGLineCapRound);
//3、将画布上绘制的内容渲染到view的layer上
CGContextStrokePath(ctx);
/**绘制三角形**/
//设置起点
CGContextMoveToPoint(ctx, 150, 80);
//设置第一个拐点
CGContextAddLineToPoint(ctx, 220, 150);
//设置第二个拐点
CGContextAddLineToPoint(ctx, 80, 150);
//设置第三个点(终点)
// CGContextAddLineToPoint(ctx, 150, 80);
//可以用下面方法代替 缝合起点和终点
CGContextClosePath(ctx);
//设置线条的拐点转角样式为圆角
CGContextSetLineJoin(ctx, kCGLineJoinRound);
//渲染
CGContextStrokePath(ctx);
/**绘制空心四边形**/
CGContextAddRect(ctx, CGRectMake(40, 200, 200, 100));
//设置空心(线条)颜色
// CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor);
//也可以这样设置颜色
[[UIColor lightGrayColor] setStroke];
//设置线条宽度
CGContextSetLineWidth(ctx, 10);
//渲染(空心的)
CGContextStrokePath(ctx);
/**绘制实心四边形**/
CGContextAddRect(ctx, CGRectMake(40, 320, 200, 100));
//设置实心颜色
// CGContextSetFillColorWithColor(ctx, [UIColor orangeColor].CGColor);
[[UIColor orangeColor] setFill];
//渲染(实心的)
CGContextFillPath(ctx);
/**绘制圆(可以用绘制椭圆的方式画圆)**/
//参数依次为圆心x,圆心y,半径,开始位置的弧度,结束位置的弧度,绘制路径(1为顺时针,0为逆时针)
//由于Quartz2D的坐标系是x轴向右,y轴向上,不同于UIKit坐标系。因此在不将Quartz2D坐标系翻转的情况下,画出来的图形是与原图形关于x轴对称的。
CGContextAddArc(ctx, 100, 520, 50, 0, M_PI_2, 1);
CGContextStrokePath(ctx); //空心
// CGContextFillPath(ctx); //实心
/**绘制椭圆**/
CGContextAddEllipseInRect(ctx, CGRectMake(230, 400, 120, 200));
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextStrokePath(ctx);
/**绘制弧线**/
// CGContextAddArcToPoint(ctx, 100, 200, 100, 200, 50);
CGContextAddArc(ctx, 200, 200, 100, 0, M_PI, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor cyanColor].CGColor);
CGContextStrokePath(ct
绘制结果
图形上下文栈
一、绘制原理
举个例子,假如要绘制两条线,一条红色一条默认的黑色。先绘制红色线,绘制完毕渲染上去后,再去绘制第二条。绘制第二条的时候如果不重新设置绘画颜色,那么绘制出来的线条也是红色的。也就是说,绘制属性如果不对其清空(即重新设置)是默认保留在图形上下文上的。因此可以这样理解:
一个图形上下文有3块区域,分别是绘制属性,图像信息,绘制区域:
- 绘制属性:包括画笔的颜色、线条宽度、是否圆角等等;
- 绘图信息:比如要绘制一条直线,那么保存了线条的起点和终点,绘制圆,保存了半径、中点坐标、起点终点、方向等等。也就是说,绘图信息中保存的是绘图路径,这个在下面会详细介绍。
- 绘图区域:这个区域不是指视图上的区域,而是图形上下文上的绘制区域,因为绘画是在图形上下文上绘制好之后才被渲染到视图的制定区域上的。
二、保存图形上下文绘制属性
前面说过如果要绘制多个不同属性的图形,那么每次渲染好一个图形后就要重新设置绘制属性,通常绘制多个图形都是这样做的。有时候可能用到一个简单的方法:即在绘制一个图形前先保存当前图形上下文中的绘制属性,这个绘制属性会被保存到图形上下文栈上,如果下次需要绘制同样属性的图形,直接把这个绘制属性从栈顶取出来(恢复)就好了。需要注意的是保存一次只能取一次,可以保存多次,但是每次只从栈顶取。
代码示例
/**保存绘制属性(以绘制3条线为例,第一条第三条属性一致)**/
//第一条
CGContextMoveToPoint(ctx, 20, 300);
CGContextAddLineToPoint(ctx, 200,300);
//设置绘制属性
CGContextSetLineWidth(ctx, 10);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetStrokeColorWithColor(ctx, [UIColor orangeColor].CGColor);
CGContextStrokePath(ctx);
//第二条线
//先保存当前的绘制属性
CGContextSaveGState(ctx);
CGContextMoveToPoint(ctx, 20, 400);
CGContextAddLineToPoint(ctx, 200, 400);
//设置新的绘制属性
CGContextSetLineWidth(ctx, 5);
CGContextSetLineCap(ctx, kCGLineCapButt);
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextStrokePath(ctx);
//第三条线
//取出(恢复)之前保存的绘制属性
CGContextRestoreGState(ctx);
CGContextMoveToPoint(ctx, 20, 500);
CGContextAddLineToPoint(ctx, 200, 500);
CGContextStrokePath(ctx);
绘制结果
矩阵操作
一、矩阵操作介绍
矩阵操作主要有旋转操作、缩放操作、平移操作,是以视图左上角为原点进行的。对矩阵的操作一定要在绘制之前完成,不然绘制完了再操作无效。
二、代码说明
CGContextRef ctx = UIGraphicsGetCurrentContext();
//矩阵旋转45度(参数为图形上下文、旋转角度)是以左上角为旋转点的
//设置矩阵操作要在绘制前完成
// CGContextRotateCTM(ctx, M_PI_4);
//缩放(参数为图形上下文,x方向缩放比例,y方向缩放比例)
// CGContextScaleCTM(ctx, 0.5, 0.5);
//平移(参数为图形上下文,x方向平移距离,y方向平移距离)
CGContextTranslateCTM(ctx, 100, 100);
CGContextAddRect(ctx, CGRectMake(100, 100, 100, 100));
//标记
NSString *loc1 = @"1";
NSString *loc2 = @"2";
[loc1 drawAtPoint:CGPointMake(99, 99) withAttributes:nil];
[loc2 drawAtPoint:CGPointMake(201, 99) withAttributes:nil];
CGContextStrokePath(ctx);
剪切操作
一、剪切介绍
指剪切掉指定区域意外的部分,只保留该区域内的内容。
原则:先设置好剪切区域,或者说剪切方法,再去绘制相关内容。
二、代码相关
CGContextRef ctx = UIGraphicsGetCurrentContext();
//剪切自定义区域意外的部分(以剪切方法为三角形,剪切内容为图片为例)
// // CGContextAddEllipseInRect(ctx, CGRectMake(100, 100, 50, 50));
// CGContextMoveToPoint(ctx, 100, 100);
// CGContextAddLineToPoint(ctx, 60, 150);
// CGContextAddLineToPoint(ctx, 140, 150);
// CGContextClosePath(ctx);
// CGContextClip(ctx);
//剪切指定矩形区域意外的部分
CGContextClipToRect(ctx, CGRectMake(80, 100, 10, 10));
UIImage *image = [UIImage imageNamed:@"google"];
[image drawAtPoint:CGPointMake(80, 100)];
绘图路径
一、绘图路径介绍
我们之前画一条直线,都是直接设置好它的起点和终点,然后就开始画了。画一个圆,设置好圆心半径起点终点和方向即可。事实上,我们设置好这些绘图信息后,系统会默认创建一条绘图路径,画图就是根据这条路径来画的。一条线对应一条路径,一个圆对应另一条路径。那么我们自然可以通过手动创建路径的方式绘制,需要绘制几个图案,就要创建几条路径。
二、注意点
A、Quartz2D中所有通过creat/copy/retain方法创建出来的值都要释放,以path为例:
- CGPathRelease(path);或
- CFRelease(path);
B、可以将要绘制的所有路径加入到图形上下文后,最后一次性渲染。
三、代码示例
//绘制一条直线的两种方法(两种方式是等效的)
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(ctx, 20, 200);
CGContextAddLineToPoint(ctx, 300, 200);
CGContextStrokePath(ctx);
//手动创建路径绘制
//创建一条路径
CGMutablePathRef path = CGPathCreateMutable();
//添加绘图信息到路径
CGPathMoveToPoint(path, NULL, 20, 300);
CGPathAddLineToPoint(path, NULL, 300, 300);
//将路径添加到图形上下文
CGContextAddPath(ctx, path);
//创建另一条路径
CGMutablePathRef path2 = CGPathCreateMutable();
CGPathAddEllipseInRect(path2, NULL, CGRectMake(100, 400, 100, 100));
CGContextAddPath(ctx, path2);
CGContextStrokePath(ctx);
//渲染所有路径对应的图案
CGContextStrokePath(ctx);
//Quartz2D中所有通过creat/copy/retain方法创建出来的值都要释放
CGPathRelease(path);
CGPathRelease(path2);
//或者
// CFRelease(path);
运行结果
Bitmap
一、bitmap图形上下文
Quartz2D提供了以下几种类型的图形上下文
- Bitmap Graphics Context
- PDF Graphics Context
- Window Graphics Context
- Layer Graphics Context
- Printer Graphics Context
常用的是Bitmap Graphics Context。所谓Bitmap,其实就是UIImage,这也是最常用到的图形上下文,通常用它来生成一张图片。步骤如下:
创建一个bitmap图形上下文(有两种方式)
A.UIGraphicsBeginImageContext(<#CGSize size#>);
B.UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);这两种方法都可以创建一个bitmap图形上下文,但是第一种创建的图片清晰度和质量没有第二种好。方法二接收的参数依次为:
- size:指定创建出来的bitmap尺寸大小
- opaque:是否透明,YES表示透明,NO不透明
- scale:缩放比例,0为不缩放
获取创建好的bitmap图形上下文然后在上面做文章(画图)
- 取出绘制好的图片
- 关闭bitmap图形上下文
- 释放需要释放的变量
代码示例(截屏)
//点击按钮截屏
- (void)actionShot:(UIButton *)sender{
//可以隐藏按钮,渲染完后显示回来
self.buttonShot.hidden = YES;
//创建图形上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(self.view.frame.size.width, self.view.frame.size.height), NO, 0);
//获取图形上下文并将当前屏幕渲染到图形上下文上
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[delegate.window .layer renderInContext:UIGraphicsGetCurrentContext()];
//从图形上下文中取出绘制好的图片
UIImage *screenImage = UIGraphicsGetImageFromCurrentImageContext();
//关闭图形上下文
UIGraphicsEndImageContext();
self.buttonShot.hidden = NO;
// //截屏完毕 有时候可能想获取屏幕中指定区域的图片,如下操作
// //得到截屏的cgimage
CGImageRef image = screenImage.CGImage;
//设置目标区域,注意这里需要考虑retina分辨率的放大倍数,以iphone6plus为例,在原尺寸的基础上*3,这里就不判断了。
CGRect rect = CGRectMake(0, 0, screenImage.size.width*3, screenImage.size.height*3);
//取出目标区域的图片
CGImageRef targetImage = CGImageCreateWithImageInRect(image, rect);
//最终图片
UIImage *finalImage = [UIImage imageWithCGImage:targetImage];
//保存到相册
UIImageWriteToSavedPhotosAlbum(finalImage, self, @selector(image: didFinishSavingWithError:contextInfo:), nil);
//保存到沙盒
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString *currentTime = [dateFormatter stringFromDate:[NSDate date]];
NSString *imagePath = [path stringByAppendingPathComponent:[NSString stringWithFormat:@"ScreenShot_%@.png",currentTime]];
NSData *imageDate = UIImagePNGRepresentation(finalImage);
[imageDate writeToFile:imagePath atomically:YES];
CGImageRelease(targetImage);
}
//保存至相册后的回调
- (void)image: (UIImage *) image didFinishSavingWithError: (NSError *) error contextInfo: (void *) contextInfo
{
NSString *msg = nil ;
if(error != NULL){
msg = @"保存图片失败" ;
}else{
msg = @"保存图片成功" ;
}
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"保存图片结果提示"
message:msg
delegate:self
cancelButtonTitle:@"确定"
otherButtonTitles:nil];
[alert show];
}
Quartz2D内存管理
前面讲绘图路径的时候提到过内存管理,下面再总结一下:
- 凡是使用含有“create”或“copy”的函数创建的对象,使用完毕后必需释放,否则将导致内存泄露。不含“create”或“copy”的则不需要释放。
- 如果retain了一个对象,不再使用时需要release。以CGColorSpace为例,如果创建了一个CGColorSpace对象,则使用函数CGColorSpaceRetain和CGColorSpaceRelease来retain和release对象。也可以使用Core Foundation的CFRetain和CFRelease。注意不能传递NULL值给这些函数。
补充
坐标系统
1、Quartz坐标系统默认原点位于左下角,x正轴向右,y正轴向上。
2、在UIKit中的坐标系统中,原点位于左上角;而Quartz的坐标系统原点位于左下角。在UIView中获得的图形上下文(Graphics Context),系统已经将坐标系统转变成了UIKit的坐标系,是通过将图形上下文的CTM原点平移到左上角,同时将y轴反转(y值乘以-1)得到的。因此在绘制UIView时不需要再对获取到得图形上下文再作坐标系的调整(已经匹配UIView的坐标系了)。
3、对于位图(bitmap),通过UIGraphicsBeginImageContextWithOptions创建的位图图形上下文(bitmap graphics context),其坐标系统原点是在左上角的,和UIView的图形上下文坐标系统一样,系统已经自动对其进行了转换。
注意
:
1、只是通过UIGraphicsBeginImageContextWithOptions创建的位图图形上下文坐标系统是自动做了转换的,如果通过CGBitmapContextCreate手动创建位图图形上下文,其坐标系统依旧是Quartz的坐标系统,即以左下角为原点,y轴正方向从下到上。
2、在drawRect的图形上下文或者UIGraphicsBeginImageContextWithOptions获取的图形上下文中通过CGContextDrawImage方法绘制图片,绘制结果发现图片是倒置的。既然上面说图形上下文已经转换成了UIKit的坐标系,原点在左上角,又怎么会倒置呢?原因在于CGContextDrawImage方法传入的参数CGImage的坐标系是倒置的,避免这种情况可以通过对UIImage调用drawInRect绘制。
3、只是在iOS中UIView和UIGraphicsBeginImageContextWithOptions返回的图形上下文坐标系原点默认被调整为左上角,在Mac OSX中并不适用。
4、必要的时候需要翻转坐标系,方法是 :
CGContextTranslateCTM(cox, 0, height);
CGContextScaleCTM(cox, 1.0, -1.0);
渐变
可以通过CGShading对象或者CGGradient对象绘制渐变,CGGradient易于重用且用法简单,这里只介绍使用CGGradient绘制渐变。
主要步骤:
- 创建一个CGGradient对象,提供一个颜色空间,一个饱含两个或更多颜色组件的数组,一个包含两个或多个位置的数组,和两个数组中元素的个数。
- 调用CGContextDrawLinearGradient或CGContextDrawRadialGradient函数并提供一个上下文、一个CGGradient对象、绘制选项和开始结束几何图形来绘制渐变。
- 当不再需要时释放CGGradient对象。
代码示例:
//创建渐变梯度对象方法一
// size_t numOfLocations = 3; //位置数
// CGFloat locations[3] = {0, 0.5, 1}; //元素取值0~1 表示位置点到轴线起点的距离与轴线长度的比例
// CGFloat components[12] = {1.0, 0.5, 0.4, 1.0, 0.8, 0.8, 0.3, 1.0, 0.5,0.5,0.5,1.0}; //颜色组件 元素个数为4的倍数(RGBA*位置数)
// CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, numOfLocations);
//创建渐变梯度对象方法二
CGFloat locations[3] = {0, 0.5, 1};
NSArray *colors = [NSArray arrayWithObjects:
(id)[UIColor colorWithRed:0.540 green:1.000 blue:0.814 alpha:1.000].CGColor,
(id)[UIColor colorWithRed:1.000 green:0.687 blue:0.695 alpha:1.000].CGColor,
(id)[UIColor colorWithRed:0.970 green:1.000 blue:0.488 alpha:1.000].CGColor,nil];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace,(__bridge CFArrayRef)colors, locations);
//绘制轴向渐变
CGContextDrawLinearGradient(ctx, gradient, CGPointZero, CGPointMake(rect.size.width, rect.size.height), 0);
//绘制径向渐变
// CGContextDrawRadialGradient(ctx, gradient, CGPointMake(50, 50), 25, CGPointMake(250, 500), 50, 0);
CGColorSpaceRelease(colorSpace);
CGGradientRelease(gradient);
透明层
透明层是一个独立于图形上下文的目标缓存,当需要在一组对象上使用特效时,透明层非常有用,它会把一组对象看成是一个整体来进行特效的绘制。以绘制3个矩形的阴影为例:
未使用透明层:
CGSize myShadowOffset = CGSizeMake (10, -10);
CGContextSetShadow (ctx, myShadowOffset, 5);
CGContextSetRGBFillColor (ctx, 0, 1, 0, 1);
CGContextFillRect (ctx, CGRectMake (20, 20,80,80));
CGContextSetRGBFillColor (ctx, 0, 0, 1, 1);
CGContextFillRect (ctx, CGRectMake (80,80,80,80));
CGContextSetRGBFillColor (ctx, 1, 0, 0, 1);
CGContextFillRect (ctx, CGRectMake (140,140,80,80));
使用透明层:
CGSize myShadowOffset = CGSizeMake (10, -10);
CGContextSetShadow (ctx, myShadowOffset, 5);
CGContextBeginTransparencyLayer (ctx, NULL); //开启透明层 然后开始绘制
CGContextSetRGBFillColor (ctx, 0, 1, 0, 1);
CGContextFillRect (ctx, CGRectMake (20, 20,80,80));
CGContextSetRGBFillColor (ctx, 0, 0, 1, 1);
CGContextFillRect (ctx, CGRectMake (80,80,80,80));
CGContextSetRGBFillColor (ctx, 1, 0, 0, 1);
CGContextFillRect (ctx, CGRectMake (140,140,80,80));
CGContextEndTransparencyLayer (ctx); //关闭透明层
参考文档:
《Quartz 2D编程指南》