iOS_Quartz 2D绘图
目 录:
一、基础知识掌握
二、Quartz 2D绘图基础:CGContextRef实现简单地绘制图形
三、CGContextRef实现文字、图片、基于路径的图形绘制
四、在内存中绘制位图
五、添加渐变效果
六、PDF文档
引言:Quartz 2D绘图的核心API是CGContextRef,该API专门用于绘制各种图形。Quartz 2D是一个二维图形绘制引擎,它支持iOS环境和Mac OS X环境,为开发者会提供了很多的方便,它在绘图上功能是非常强大的,如基于路径的绘图、透明度、阴影、颜色管理、反锯齿、PDF文档生成和PDF元数据访问等。Quartz 2DAPI作为Core Graphics框架的一部分,因此其中的很多数据类型和方法都是以CG开头的。会经常见到Quartz 2D(Quartz)和Core Graphics两个术语交互使用。
一、基础知识掌握
学习Quartz 2D之前首先先来掌握几个基础知识。
1.图形上下文(Graphics Context)——绘制目标
1)Graphics Context是一个数据类型(CGContextRef),封装了Quartz绘制图像到输出设备的信息。输出设备可以是PDF文件、Bitmap、Layer、打印机或者显示器的窗口上
2)Quartz中所有的对象都是绘制到一个Graphics Context中
3)当用Quartz绘图时,所有设备相关的特性都包含在Graphics Context中。换句话说,我们可以简单地给Quartz绘图序列指定不同的Graphics Context,就可将相同的图像绘制到不同的设备上。而不需要任何设备相关的计算,这些都由Quartz替我们完成
2.Quartz 2D坐标系
1)Quartz中默认的坐标系统是:原点(0, 0)在左下角。沿着X轴从左到右坐标值逐渐增大;沿着Y轴从下到上坐标值逐渐增大
2)有一些技术在设置它们的graphics context时使用了不同于Quartz的默认坐标系统。最常见的一种修改的坐标系统是原点位于左上角,而沿着Y轴从上到下坐标值逐渐增大。例如:UIView中的UIGraphicsGetCurrentContext方法返回的图形上下文就是用的是这种坐标系(坐标系统的原点位于左上角)
1)原点(0,0)在屏幕的左上角,X轴向右正向延伸,Y轴向下正向延伸
2)iOS的像素分辨率会随设备的硬件而变化,iPhone4第一次引入了视网膜屏幕,像素分辨率为960* 640,刚好是前一代iPod和iPhone像素分辨率( 480* 320)的两倍
3)在绘图时,需要使用“点”的概念来思考问题,而不是像素。也就是说在点坐标系中绘图,不是硬件的像素坐标系
4)虽然这些设备的像素分辨率不同,但用到的坐标系保持不变(以点为单位)。在iPhone4上,一个点会用2像素宽度来绘制
提示:如果绘图的上下文,是使用UIGraphicsGetCurrentContext或者其他以UI开头的方法获取到的,在绘图时无需进行坐标转换
4.Quartz 2D的绘图顺序
后面绘制的图形,会覆盖先前绘制的图形。如下图所示:
二、Quartz 2D绘图基础:CGContextRef实现简单地绘制图形
使用Quartz 2D绘图的关键步骤有两步:
1.获取CGContextRef;
2.调用CGContextRef的方法进行绘图。
不同场景下获取CGContextRef的方式各不相同,下面介绍iOS开发中最常见的场景下如何获取CGContextRef.
(1)自定义UIView获取CGContextRef
开发自定义UIView的方法是,开发一个集成UIView的子类,并重写UIView的drawRect:方法,当该UIVie每次显示出来时,或该UIView的内容需要更新时,系统都会自动调用UIView的drawRect:方法。在调用UIView的drawRect:方法之前系统会自动配置绘图环境,因此程序只要通过如下函数即可获取CGContextRef绘图API:
CGContextRef context = UIGraphicsGetCurrentContext();
(2)创建位图时获取CGContextRef
如果需要在创建位图时获取CGContextRef,那么程序需要先调用UIGraphicsBeginImageContext()函数来创建内存中的图片。然后调用UIGraphicsGetCurrentContext()获取绘图的CGContextRef。
Quartz 2D时面向过程的API,Quartz 2D提供了大量函数来完成绘图。
现在通过一个案例来熟悉一下Quartz 2D的使用过程,以及常用的函数使用。首先为UIView创建一个DemoView类,本案例中所有的绘图操作都是在DemoView类中进行实现的。
#import "DemoView.h" @implementation DemoView - (void)drawRect:(CGRect)rect { // 获取绘图的上下文 CGContextRef context = UIGraphicsGetCurrentContext(); //1.绘制三角形 [self drawTriangle:context]; //2.绘制矩形 [self drawRectangle:context]; } //绘制三角形 -(void)drawTriangle:(CGContextRef)context { //2.添加绘图路径 CGContextMoveToPoint(context, 100, 100); CGContextAddLineToPoint(context, 200, 100); CGContextAddLineToPoint(context, 150, 200); CGContextAddLineToPoint(context, 100, 100); //3.设置绘图的属性 CGFloat myColor[4] = {1.0,0.0,0.0,1.0}; //设置描边的颜色 CGContextSetStrokeColor(context, myColor); CGFloat myColor1[4]= {0.0,1.0,0.0,1.0}; //设置填充的颜色 CGContextSetFillColor(context, myColor1); //设置线宽 CGContextSetLineWidth(context, 5.0); //设置线的类型:虚线————注意:如果对上下文进行了修改之后的所有连线的类型都默认为虚线啦!其他属性也一并如此。 CGFloat dash[2] = {1.0,2.0}; CGContextSetLineDash(context, 0, dash, 2); //设置连接点的类型 /* enum CGLineJoin { kCGLineJoinMiter, 连接处为尖角形状 kCGLineJoinRound, 连接处为圆角形状 kCGLineJoinBevel 连接处为平角形状 }; */ CGContextSetLineJoin(context, kCGLineJoinRound); //4.绘图 CGContextDrawPath(context, kCGPathFillStroke); } //绘制矩形 -(void)drawRectangle:(CGContextRef)context { //添加一个矩形 CGContextAddRect(context, CGRectMake(50, 50, 50, 50)); //添加一个圆形 CGContextAddEllipseInRect(context, CGRectMake(10, 100, 50, 50)); //添加一个椭圆 CGContextAddEllipseInRect(context, CGRectMake(200, 100, 100, 50)); //设置绘图属性 CGFloat myColor[4] = {1.0,0.0,0.0,1.0}; //设置描边颜色 CGContextSetStrokeColor(context, myColor); CGFloat myColor1[4] = {0.0,1.0,0.0,1.0}; //设置填充颜色 CGContextSetFillColor(context, myColor1); //绘图 CGContextDrawPath(context, kCGPathFillStroke); } @end
运行结果如下图:
Quartz 2D绘制的线条默认时实线的。上图中使用的却是点线,关于点线模式的设置我做多下详细的介绍:
如果需要创建点线可调用CGContextRef的CGContextSetLineDash(CGContextRef c, CGFloat phase, const CGFloat *lengths, size_t count);该函数的第3个参数是点线模式的关键,该参数是一个CGFloat型数组(第4个参数通用用于指定该数组的长度),每个CGFloat值依次控制点线的实现长度、间距。比如该参数如下:
前面说到CGContextRef不但能提供绘制基本图形的功能,还可以提供绘制文字、图片、基于路径的图形的绘制。下面来看下对这三种的绘制时Quartz 2D是如何实现的。
1.使用路径
使用路径的步骤如下:
#import "DemoView.h" @implementation DemoView - (void)drawRect:(CGRect)rect { // 获取绘图的上下文 CGContextRef context = UIGraphicsGetCurrentContext(); //3.通过路径的方式创建三角形 [self drawTriangleByPath:context]; //4.绘制文字 [self drawString:context]; //5.绘制图片 [self drawImage]; } -(void)drawImage { UIImage *image = [UIImage imageNamed:@"6.jpg"]; //将该图片本身绘制到当前绘图CGContextRef的指定区域中。 [image drawInRect:CGRectMake(100, 300, 200, 150)]; //将该图片本身绘制到当前回去CGContextRef的指定点 [image drawAtPoint:CGPointMake(10, 450)]; } -(void)drawString:(CGContextRef)context { NSString *string = @"hello world"; //设置使用描边模式绘制文字 CGContextSetTextDrawingMode(context, kCGTextStroke); //将文字本身绘制到当前CGContextRef的指定点 [string drawAtPoint:CGPointMake(100, 250) withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:18],NSForegroundColorAttributeName:[UIColor blueColor]}]; //设置使用填充、描边绘制文字 CGContextSetTextDrawingMode(context, kCGTextFillStroke); //将文本本身绘制到当前CGContextRef的指定区域中 [string drawWithRect:CGRectMake(200, 250, 70, 80) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:18],NSForegroundColorAttributeName:[UIColor blueColor]} context:nil]; } -(void)drawTriangleByPath:(CGContextRef)context { //创建路径 CGMutablePathRef path = CGPathCreateMutable(); //向路径中添加图形 CGPathMoveToPoint(path, NULL, 20, 200);//创建起点 CGPathAddLineToPoint(path, NULL, 100, 300); CGPathAddLineToPoint(path, NULL, 60, 400); //将path添加到上下文 CGContextAddPath(context, path); //闭合路径 CGContextClosePath(context); //设置绘图的属性 //设置描边颜色 [[UIColor redColor]setStroke]; //设置填充颜色 [[UIColor greenColor]setFill]; //同时设置填充颜色,又能设置描边颜色 // [[UIColor whiteColor]set]; //使用默认的阴影颜色,阴影向左上角投影,模糊度为5 // CGContextSetShadow(context, CGSizeMake(-8, -6), 5); CGContextSetShadowWithColor(context, CGSizeMake(-8, -6), 5, [[UIColor redColor]CGColor]); //绘图 CGContextDrawPath(context, kCGPathFillStroke); } @end
运行效果图,如下所示:
经过两个案例,细心地朋友可能发现了这样的问题:在案例1中对三角形进行设置了线的类型,设置为点线模式,但是矩形的线的类型没有设置但同样变成了点线模式。在案例2中对三角形设置了阴影模式,但是图片、文字也均带有了阴影。如何解决?
解决办法,分为两步走:
1.对上下文进行操作之前,使用CGContextSaveGState(CGContextRef c)保存当前上下文状态
2.对上下文进行操作之后,使用CGContextRestoreGState(CGContextRef c)可以恢复之前保存的上下文状态
四、在内存中绘制位图
前面介绍的都是通过扩展UIView、重写drawRect:方法进行绘图,这种绘图方式是直接在UIView控件上绘制所有的图形——由于每次该控件显示出来时,drawRect:方法都会被调用,这意味着每次该控件显示出来时,程序都需要重绘所有的图形,很明显,这种方式的性能并不好。除此之外,总有些时候需要在内存中绘制图片,这样既可导出到手机本地,也可上传到网络上。
#import "ViewController.h" @interface ViewController ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate> @property (weak, nonatomic) IBOutlet UIImageView *imageView; @property(strong,nonatomic)UIImage *image; @property (weak, nonatomic) IBOutlet UITextField *textField; @end @implementation ViewController - (IBAction)drawClicked:(UIButton *)sender { //开始图形绘制的上下文 UIGraphicsBeginImageContext(self.imageView.frame.size); //获取图形绘制的上下文 CGContextRef context = UIGraphicsGetCurrentContext(); //绘制矩形 CGContextAddRect(context, CGRectMake(100, 100, 100, 100)); //设置描边、填充颜色 [[UIColor redColor]set]; //绘制图形 CGContextDrawPath(context, kCGPathFillStroke); //从图形绘制上下文获取图片 self.image = UIGraphicsGetImageFromCurrentImageContext(); //结束图形绘制的上下文 UIGraphicsEndImageContext(); //设置显示图片 [self.imageView setImage:self.image]; } - (IBAction)waterMarkClicked:(UIButton *)sender { //开始图形绘制的上下文 UIGraphicsBeginImageContext(self.imageView.frame.size); //先画图片 // self.image = [UIImage imageNamed:@"6.jpg"]; [self.image drawInRect:self.imageView.bounds]; //再画水印 NSString *string = self.textField.text; [string drawAtPoint:CGPointMake(100, 100) withAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:[UIFont systemFontOfSize:25]}]; //从当前上下文获取图片 self.image = UIGraphicsGetImageFromCurrentImageContext(); //结束图形绘制的上下文 UIGraphicsEndImageContext(); //显示带水印的图片 self.imageView.image = self.image; } - (IBAction)saveClicked:(UIButton *)sender { //保存图片到外部设备 /* //设置图片保存的路径 NSString *doucuments = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject]; NSString *fileName = [doucuments stringByAppendingPathComponent:@"watermark.png"]; //获取图片中的data NSData *imageData = UIImagePNGRepresentation(self.image); //保存图片 [imageData writeToFile:fileName atomically:YES]; NSLog(@"%@",NSHomeDirectory()); */ //保存图片到相册 UIImageWriteToSavedPhotosAlbum(self.image, nil, nil, nil); } - (IBAction)photoSelect:(UIButton *)sender { //打开相册 UIImagePickerController *imagePicker = [[UIImagePickerController alloc]init]; //设置图片的来源 imagePicker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum; //设置代理 imagePicker.delegate = self; //使用模态窗口的方式显示相册 [self presentViewController:imagePicker animated:YES completion:nil]; } #pragma mark - 实现UIImagePickerControllerDelegate代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { NSLog(@"%@",info); //通过字典方式获取图片 self.image = [info objectForKey:UIImagePickerControllerOriginalImage]; [self.imageView setImage:self.image]; //关闭模态窗口 [picker dismissViewControllerAnimated:YES completion:nil]; } - (void)viewDidLoad { [super viewDidLoad]; } @end
运行效果图,如下图:
五、添加渐变效果
前面介绍的都是使用一种颜色来填充区域,除此之外,Quartz 2D还允许使用颜色渐变、位图来填充指定区域。
Quartz 2D为我们提供了两种渐变填充的函数,分别是:线性渐变填充、圆形径向渐变填充。
- void CGContextDrawRadialGradient(CGContextRef context,GradientRef gradient, CGPoint startCenter, CGFloat startRadius,CGPoint endCenter, CGFloat endRadius, CGGradientDrawingOptions options),参数详解如下:
/* 参数2:gradient参数代表渐变对象,
参数3:startCenter参数设置起始圆的圆心,
参数4:startRadius设置起始圆的半径,
参数5:endCenter参数代表结束圆的圆心
参数6:endRadius代表结束圆的半径
参数7:options可支持kCGGradientDrawsBeforeStartLocation(可扩展填充起点之前的区域)或kCGGradientDrawsAfterEndLocation(扩展填充结束点之后的区域)*/
- void CGContextDrawLinearGradient(CGContextRef context,CGGradientRef gradient, CGPoint startPoint, CGPoint endPoint,CGGradientDrawingOptions options):
/* 参数2:gradient参数代表渐变对象,
参数3:startPoint开始点坐标
参数4:endPoint结束点坐标
参数5:options可支持kCGGradientDrawsBeforeStartLocation(可扩展填充起点之前的区域)或kCGGradientDrawsAfterEndLocation(扩展填充结束点之后的区域)*/
废话不多说,直接上代码分析:
#import "DemoView.h" @implementation DemoView - (void)drawRect:(CGRect)rect { // 画渐变:线性渐变、径向渐变 //获取绘图的上下文 CGContextRef context = UIGraphicsGetCurrentContext(); //画线性渐变 // [self drawLinarGradient:context]; [self drawRadialGradient:context]; } -(void)drawRadialGradient:(CGContextRef)context { //2.创建渐变 //2.1创建颜色空间 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); //2.2设置开始颜色、结束时颜色 UIColor *startColor =[UIColor yellowColor]; const CGFloat *startColorCompents = CGColorGetComponents([startColor CGColor]); UIColor *endColor = [UIColor blackColor]; const CGFloat *endColorCompents = CGColorGetComponents([endColor CGColor]); CGFloat components[8] = {startColorCompents[0],startColorCompents[1], startColorCompents[2], startColorCompents[3], endColorCompents[0], endColorCompents[1], endColorCompents[2], endColorCompents[3]}; CGFloat locations[2] = {0.0,1.0}; CGGradientRef gradien = CGGradientCreateWithColorComponents(colorSpace, components, locations, 2); /* 参数详解; 参数1:用于指定该渐变所使用的颜色空间(如:RGB,CMYK,Gray等颜色空间); 参数2:用于指定根据不同的颜色空间设置多种颜色 参数3:locations参数指定各颜色点得分布位置(如果将该参数指定为NULL,各颜色点将会均匀分布) 参数4:count参数指定该渐变包含多少种颜色 */ //3.绘画渐变效果(径向渐变) CGContextDrawRadialGradient(context, gradien, CGPointMake(200, 200), 50, CGPointMake(200, 200), 100, kCGGradientDrawsBeforeStartLocation|kCGGradientDrawsAfterEndLocation); /* 参数2:gradient参数代表渐变对象, 参数3:startCenter参数设置起始圆的圆心, 参数4:startRadius设置起始圆的半径, 参数5:endCenter参数代表结束圆的圆心 参数6:endRadius代表结束圆的半径 参数7:options可支持kCGGradientDrawsBeforeStartLocation(可扩展填充起点之前的区域)或kCGGradientDrawsAfterEndLocation(扩展填充结束点之后的区域) */ //4.清理工作 CGColorSpaceRelease(colorSpace); CGGradientRelease(gradien); } -(void)drawLinarGradient:(CGContextRef)context { //2.创建渐变 //2.1创建颜色空间 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); //2.2设置开始颜色、结束时颜色 UIColor *startColor =[UIColor redColor]; const CGFloat *startColorCompents = CGColorGetComponents([startColor CGColor]); UIColor *endColor = [UIColor blueColor]; const CGFloat *endColorCompents = CGColorGetComponents([endColor CGColor]); CGFloat components[8] = {startColorCompents[0],startColorCompents[1], startColorCompents[2], startColorCompents[3], endColorCompents[0], endColorCompents[1], endColorCompents[2], endColorCompents[3]}; CGFloat locations[2] = {0.0,1.0}; CGGradientRef gradien = CGGradientCreateWithColorComponents(colorSpace, components,NULL, 2); //3.绘画渐变效果(线性) CGContextDrawLinearGradient(context, gradien, CGPointMake(100, 100), CGPointMake(200, 100),kCGGradientDrawsAfterEndLocation | kCGGradientDrawsBeforeStartLocation); /* options可支持kCGGradientDrawsBeforeStartLocation(可扩展填充起点之前的区域)或kCGGradientDrawsAfterEndLocation(扩展填充结束点之后的区域) */ //4.清理工作 CGColorSpaceRelease(colorSpace); CGGradientRelease(gradien); } @end
运行效果图,如下所示:
六、PDF文档
1.绘制、读取PDF文档
PDF文档存储依赖于分辨率的向量图形、文本和位图,并用于程序的一系列指令中。一个PDF文档可以包含多页的图形和文本。PDF可用于创建跨平台、只读的文档,也可用于绘制依赖于分辨率的图形。Quartz为所有应用程序创建高保真的PDF文档,这些文档保留应用的绘制操作,如图下图所示。PDF文档的结果将通过系统的其它部分或第三方法的产品来有针对性地进行优化。Quartz创建的PDF文档在Preview和Acrobat中都能正确的显示。
案例1:创建pdf文档(创建并读取)
#import "ViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIWebView *webView; @end @implementation ViewController - (IBAction)showPDF:(id)sender { //设置pdf文件的路径 NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSLog(@"%@",documentPath); NSString *pdfFileName = [documentPath stringByAppendingPathComponent:@"face.pdf"]; //创建URL NSURL *url = [NSURL URLWithString:pdfFileName]; //创建request NSURLRequest *request = [NSURLRequest requestWithURL:url]; //在webView中显示 [self.webView loadRequest:request]; } - (void)viewDidLoad { [super viewDidLoad]; // [self createImagePDF]; } -(void)createImagePDF { //设置pdf文件的路径 NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSLog(@"%@",documentPath); NSString *pdfFileName = [documentPath stringByAppendingPathComponent:@"face.pdf"]; //开始pdf的绘图上下文 UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectMake(0, 0, 320, 480), nil); for(int i = 0; i < 9; i++) { //开始pdf新的一页 UIGraphicsBeginPDFPage(); //画图像 UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%d.png",i]]; [image drawAtPoint:CGPointMake(320/2, 480/2)]; } //结束pdf的绘图上下文 UIGraphicsEndPDFContext(); } //创建pdf -(void)createPDF { //设置pdf文件的路径 NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSLog(@"%@",documentPath); NSString *pdfFileName = [documentPath stringByAppendingPathComponent:@"test.pdf"]; //开始pdf的绘图上下文 UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectMake(0, 0, 320, 480), nil); //获取当前的绘图上下文 CGContextRef context = UIGraphicsGetCurrentContext(); //开始pdf新的一页 UIGraphicsBeginPDFPage(); //画圆形 CGContextAddEllipseInRect(context, CGRectMake(100, 100, 100, 100)); //设置描边、填充颜色 [[UIColor redColor]set]; //将图形绘制到上下文中 CGContextDrawPath(context, kCGPathFillStroke); //画字符串 NSString *str = @"this is a test page."; [str drawAtPoint:CGPointMake(200, 200) withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:18],NSForegroundColorAttributeName:[UIColor blueColor]}]; //结束pdf的绘图上下文 UIGraphicsEndPDFContext(); } @end
运行效果图,如下图所示:
以上案例***实现三个功能:
将图片绘制到pdf文档中去,
(2)将自定义的图形绘制到pdf文档中
(3)通过UIWebView控件进行显示pdf文档内容(这种方式只只用于图形类的内容,如果是电子书文本类的内容,则此方式不可实现)。
2.使用Quartz 2D的方式读取PDF文档内容
#import "PDFView.h" @interface PDFView()<UIActionSheetDelegate,UIAlertViewDelegate> { CGPDFDocumentRef _pdfDoc; size_t _pageNo;//当前页码 size_t _totalPage;//总的页数 } @end @implementation PDFView - (void)drawRect:(CGRect)rect { NSLog(@"重绘"); CGContextRef context = UIGraphicsGetCurrentContext(); //旋转坐标系 CGContextTranslateCTM(context, 80, self.frame.size.height-60); CGContextScaleCTM(context, 1, -1); //画页面 [self drawPDFPage:_pageNo context:context]; } //打开pdf文档 -(void)openPDF:(NSURL *)url { CFURLRef urlRef = (__bridge CFURLRef)url; _pdfDoc = CGPDFDocumentCreateWithURL(urlRef); _totalPage = CGPDFDocumentGetNumberOfPages(_pdfDoc); _pageNo = 1; } //显示pdf页面 -(void)drawPDFPage:(size_t)pageNo context:(CGContextRef)context { CGPDFPageRef pdfPage = CGPDFDocumentGetPage(_pdfDoc, pageNo); CGContextDrawPDFPage(context, pdfPage); } - (IBAction)prePage:(id)sender { if(_pageNo > 1) { _pageNo--; [self setNeedsDisplay]; } } - (IBAction)nextPage:(id)sender { if(_pageNo < _totalPage) { _pageNo++; [self setNeedsDisplay]; } } @end
运行效果图,如下所示: