原文出处: KenshinCui
概述
在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益于它强大的开发框架。今天我们将围绕iOS中两大图形、图像绘图框架进行介绍:Quartz 2D绘制2D图形和Core Image中强大的滤镜功能。
在iOS中常用的绘图框架就是Quartz 2D,Quartz 2D是Core Graphics框架的一部分,是一个强大的二维图像绘制引擎。Quartz 2D在UIKit中也有很好的封装和集成,我们日常开发时所用到的UIKit中的组件都是由Core Graphics进行绘制的。不仅如此,当我们引入UIKit框架时系统会自动引入Core Graphics框架,并且为了方便开发者使用在UIKit内部还对一些常用的绘图API进行了封装。
在iOS中绘图一般分为以下几个步骤:
1.获取绘图上下文
2.创建并设置路径
3.将路径添加到上下文
4.设置上下文状态
5.绘制路径
6.释放路径
图形上下文CGContextRef代表图形输出设备(也就是绘制的位置),包含了绘制图形的一些设备信息,Quartz 2D中的所有对象最终都必须绘制到图形上下文。这样一来,我们在绘制图形时就不必关心具体的设备信息,统一了代码编写方式(在Quartz 2D中的绘图上下文可以是位图Bitmap、PDF、窗口Window、层Layer、打印对象Printer)。
在UIKit中默认已经为我们准备好了一个图形上下文对象,在UI控件的drawRect:方法(这个方法在loadView、viewDidLoad方法后执行)中我们可以通过UIKit封装函数UIGraphicsGetCurrentContext()方法获得这个图形上下文(注意在其他UI控件方法中无法取得这个对象),然后我们只要按照绘图步骤一步步执行即可。下面自定义一个KCView继承自UIView,重写drawRect:方法绘制两条直线说明上面绘图的步骤:
KCView.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 |
// // KCView.m // Quartz2D // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // /** 基本绘图 */
#import "KCView.h"
@implementation KCView
#pragma mark 绘图 //绘图只能在此方法中调用,否则无法得到当前图形上下文 -(void)drawRect:(CGRect)rect{ //1.取得图形上下文对象 CGContextRef context = UIGraphicsGetCurrentContext();
//2.创建路径对象 CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, nil, 20, 50);//移动到指定位置(设置路径起点) CGPathAddLineToPoint(path, nil, 20, 100);//绘制直线(从起始位置开始) CGPathAddLineToPoint(path, nil, 300, 100);//绘制另外一条直线(从上一直线终点开始绘制)
//3.添加路径到图形上下文 CGContextAddPath(context, path);
//4.设置图形上下文状态属性 CGContextSetRGBStrokeColor(context, 1.0, 0, 0, 1);//设置笔触颜色 CGContextSetRGBFillColor(context, 0, 1.0, 0, 1);//设置填充色 CGContextSetLineWidth(context, 2.0);//设置线条宽度 CGContextSetLineCap(context, kCGLineCapRound);//设置顶点样式,(20,50)和(300,100)是顶点 CGContextSetLineJoin(context, kCGLineJoinRound);//设置连接点样式,(20,100)是连接点 /*设置线段样式 phase:虚线开始的位置 lengths:虚线长度间隔(例如下面的定义说明第一条线段长度8,然后间隔3重新绘制8点的长度线段,当然这个数组可以定义更多元素) count:虚线数组元素个数 */ CGFloat lengths[2] = { 18, 9 }; CGContextSetLineDash(context, 0, lengths, 2); /*设置阴影 context:图形上下文 offset:偏移量 blur:模糊度 color:阴影颜色 */ CGColorRef color = [UIColor grayColor].CGColor;//颜色转化,由于Quartz 2D跨平台,所以其中不能使用UIKit中的对象,但是UIkit提供了转化方法 CGContextSetShadowWithColor(context, CGSizeMake(2, 2), 0.8, color);
//5.绘制图像到指定图形上下文 /*CGPathDrawingMode是填充方式,枚举类型 kCGPathFill:只有填充(非零缠绕数填充),不绘制边框 kCGPathEOFill:奇偶规则填充(多条路径交叉时,奇数交叉填充,偶交叉不填充) kCGPathStroke:只有边框 kCGPathFillStroke:既有边框又有填充 kCGPathEOFillStroke:奇偶填充并绘制边框 */ CGContextDrawPath(context, kCGPathFillStroke);//最后一个参数是填充类型
//6.释放对象 CGPathRelease(path); } @end |
在视图控制器创建KCView并添加到根视图中:
1 2 3 4 5 6 7 |
- (void)viewDidLoad { [super viewDidLoad];
KCView *view=[[KCView alloc]initWithFrame:[UIScreen mainScreen].bounds]; view.backgroundColor=[UIColor whiteColor]; [self.view addSubview:view]; } |
运行效果如下:
上面的绘图方式未免显得有些麻烦,其实Core Graphics 内部对创建对象添加到上下文这两步操作进行了封装,可以一步完成。另外前面也说过UIKit内部其实封装了一些以“UI”开头的方法帮助大家进行图形绘制。就拿前面的例子来说我们改进一些绘制方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
-(void)drawLine2{ //1.获得图形上下文 CGContextRef context=UIGraphicsGetCurrentContext();
//2.绘制路径(相当于前面创建路径并添加路径到图形上下文两步操作) CGContextMoveToPoint(context, 20, 50); CGContextAddLineToPoint(context, 20, 100); CGContextAddLineToPoint(context, 300, 100); //封闭路径:a.创建一条起点和终点的线,不推荐 //CGPathAddLineToPoint(path, nil, 20, 50); //封闭路径:b.直接调用路径封闭方法 CGContextClosePath(context);
//3.设置图形上下文属性 [[UIColor redColor]setStroke];//设置红色边框 [[UIColor greenColor]setFill];//设置绿色填充 //[[UIColor blueColor]set];//同时设置填充和边框色
//4.绘制路径 CGContextDrawPath(context, kCGPathFillStroke); } |
上面的操作相比前面的方法应该说已经简化了不少,除了路径之外其他矩形、椭圆等都有对应的创建方法。另外上面我们也演示了封闭路径的方法,大家可以运行看一下效果。
相信大家了解了上面的绘制步骤其他图形绘制并不麻烦,下面以一个例子简单演示一下其他图形的绘制,包括文字和图像的绘制。
在下面的方法中还可以看到UIKit对绘图方法的封装,使用起来更加简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#pragma mark 绘制矩形 -(void)drawRectWithContext:(CGContextRef)context{ //添加矩形对象 CGRect rect=CGRectMake(20, 50, 280.0, 50.0); CGContextAddRect(context,rect); //设置属性 [[UIColor blueColor]set]; //绘制 CGContextDrawPath(context, kCGPathFillStroke); }
#pragma mark 绘制矩形(利用UIKit的封装方法) -(void)drawRectByUIKitWithContext:(CGContextRef)context{ CGRect rect= CGRectMake(20, 150, 280.0, 50.0); CGRect rect2=CGRectMake(20, 250, 280.0, 50.0); //设置属性 [[UIColor yellowColor]set]; //绘制矩形,相当于创建对象、添加对象到上下文、绘制三个步骤 UIRectFill(rect);//绘制矩形(只有填充)
[[UIColor redColor]setStroke]; UIRectFrame(rect2);//绘制矩形(只有边框) } @end |
运行效果:
1 2 3 4 5 6 7 8 9 10 11 |
#pragma mark 绘制椭圆 -(void)drawEllipse:(CGContextRef)context{ //添加对象,绘制椭圆(圆形)的过程也是先创建一个矩形 CGRect rect=CGRectMake(50, 50, 220.0, 200.0); CGContextAddEllipseInRect(context, rect); //设置属性 [[UIColor purpleColor]set]; //绘制 CGContextDrawPath(context, kCGPathFillStroke); } @end |
运行效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
-(void)drawArc:(CGContextRef)context{ /*添加弧形对象 x:中心点x坐标 y:中心点y坐标 radius:半径 startAngle:起始弧度 endAngle:终止弧度 closewise:是否逆时针绘制,0则顺时针绘制 */ CGContextAddArc(context, 160, 160, 100.0, 0.0, M_PI_2, 1);
//设置属性 [[UIColor yellowColor]set];
//绘制 CGContextDrawPath(context, kCGPathFillStroke); } |
运行效果:
要绘制规则图形在iOS中相当简单,但是不规则图形怎么绘制呢?此时就要利用路径。前面我们绘制了直线,它和曲线绘制都属于路径绘制。和直线绘制相比曲线绘制就要复杂一些,但是路径作为高级动画的基础又是我们必须掌握的,因此这里我们就一起来熟悉一下曲线绘制。在Quartz 2D中曲线绘制分为两种:二次贝塞尔曲线和三次贝塞尔曲线。二次曲线只有一个控制点,而三次曲线有两个控制点,如下图所示:
当然,在iOS中两种曲线分别对应两种方法:
CGContextAddQuadCurveToPoint(CGContextRef c, CGFloat cpx, CGFloat cpy, CGFloat x, CGFloat y);
CGContextAddCurveToPoint(context, CGFloat cp1x, CGFloat cp1y, CGFloat cp2x, CGFloat cp2y, CGFloat x, CGFloat y);
下面就演示一下这两种曲线的绘制方法
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 |
#pragma mark 绘制贝塞尔曲线 -(void)drawCurve:(CGContextRef)context{
//绘制曲线 CGContextMoveToPoint(context, 20, 100);//移动到起始位置 /*绘制二次贝塞尔曲线 c:图形上下文 cpx:控制点x坐标 cpy:控制点y坐标 x:结束点x坐标 y:结束点y坐标 */ CGContextAddQuadCurveToPoint(context, 160, 0, 300, 100);
CGContextMoveToPoint(context, 20, 500); /*绘制三次贝塞尔曲线 c:图形上下文 cp1x:第一个控制点x坐标 cp1y:第一个控制点y坐标 cp2x:第二个控制点x坐标 cp2y:第二个控制点y坐标 x:结束点x坐标 y:结束点y坐标 */ CGContextAddCurveToPoint(context, 80, 300, 240, 500, 300, 300);
//设置图形上下文属性 [[UIColor yellowColor]setFill]; [[UIColor redColor]setStroke];
//绘制路径 CGContextDrawPath(context, kCGPathFillStroke);
} |
运行效果:
备注:贝塞尔曲线是由法国数学家“贝塞尔”发现的,他发现:任何一条曲线都能够由和它相切的直线的两个端点来描述,这种曲线表示方式后来被广泛应用到计算机中,称为“贝塞尔曲线”。
除了绘制图形还可以绘制文本内容。
1 2 3 4 5 6 7 8 9 10 11 |
-(void)drawText:(CGContextRef)context{ //绘制到指定的区域内容 NSString *str=@"Star Walk is the most beautiful stargazing app you’ve ever seen on a mobile device. It will become your go-to interactive astro guide to the night sky, following your every movement in real-time and allowing you to explore over 200, 000 celestial bodies with extensive information about stars and constellations that you find."; CGRect rect= CGRectMake(20, 50, 280, 300); UIFont *font=[UIFont systemFontOfSize:18];//设置字体 UIColor *color=[UIColor redColor];//字体颜色 NSMutableParagraphStyle *style=[[NSMutableParagraphStyle alloc]init];//段落样式 NSTextAlignment align=NSTextAlignmentLeft;//对齐方式 style.alignment=align; [str drawInRect:rect withAttributes:@{NSFontAttributeName:font,NSForegroundColorAttributeName:color,NSParagraphStyleAttributeName:style}]; } |
运行效果:
Quartz 2D还可以将图像绘制到图形上下文。
1 2 3 4 5 6 7 8 9 |
-(void)drawImage:(CGContextRef)context{ UIImage *image=[UIImage imageNamed:@"image2.jpg"]; //从某一点开始绘制 [image drawAtPoint:CGPointMake(10, 50)]; //绘制到指定的矩形中,注意如果大小不合适会会进行拉伸 // [image drawInRect:CGRectMake(10, 50, 300, 450)]; //平铺绘制 // [image drawAsPatternInRect:CGRectMake(0, 0, 320, 568)]; } |
运行效果:
从前面的示例中我们可以看到如何设置填充颜色,事实上很多时候纯色的填充并不能满足我们的需求,例如有时候我们要绘制一些图形可能需要设置一个漂亮的背景,这个时候我们可能就会选择渐变填充方式。Quartz 2D的渐变方式分为两种:
a.线性渐变线:渐变色以直线方式从开始位置逐渐向结束位置渐变
b.径向渐变:以中心点为圆心从起始渐变色向四周辐射,直到终止渐变色
要做渐变则必须先设置从开始位置到结束位置的渐变颜色,做过photoshop的朋友相信对于渐变色设置并不陌生,只要在指定位置指定不同的颜色,剩下的事情交给系统处理即可,如下图在起始位置、3/10位置、结束位置指定了三种颜色就形成由三种颜色组成的渐变色:
另外,在iOS中绘制渐变还需要注意一点就是指定颜色空间,所谓颜色空间就是不同颜色在不同的维度上取值最终组成一种颜色的过程。就拿RGB来说,如果将红色、绿色、蓝色看成是x、y、z轴坐标系,那么在三个坐标上分别取0~255范围内的不同值则可以组成各类颜色。当然,不同颜色空间的“坐标系”也是不同的(也就是说颜色表示的方式是不同的),常用的颜色空间除了RGB还有CMYK(印刷业常用这种颜色模式)、Gray。
在使用Quartz 2D绘图时我们的颜色除了使用常规的方法(如何前面CGContextSetRGBFillColor(CGContextRef context, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha)方法)设置RGB和透明度外,有时还会遇到颜色参数是一个数组情况。如使用颜色空间填充时用到的CGContextSetFillColor(CGContextRef context, const CGFloat *components)方法,这个时候components数组中具体是如何存储颜色就要根据颜色空间而定,如果颜色空间使用RGB则数组中的元素四个为一组,分别是red(红)、green(绿)、blue(蓝)、alpha(透明度);如果使用CMYK颜色空间,那么数组中的元素五个为一组,分别是cyan(青)、magenta(洋红)、yellow(黄)、black(黑)、alpha(透明度)。
下面的代码分别演示了两种渐变方式,具体渐变绘制函数参数代码中已经注释的很清楚了:
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 |
// // KCView3.m // Quartz2D // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. //
#import "KCView3.h"
@implementation KCView3
-(void)drawRect:(CGRect)rect{ CGContextRef context=UIGraphicsGetCurrentContext(); // [self drawLinearGradient:context]; [self drawRadialGradient:context]; }
#pragma mark 线性渐变 -(void)drawLinearGradient:(CGContextRef)context{ //使用rgb颜色空间 CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
/*指定渐变色 space:颜色空间 components:颜色数组,注意由于指定了RGB颜色空间,那么四个数组元素表示一个颜色(red、green、blue、alpha), 如果有三个颜色则这个数组有4*3个元素 locations:颜色所在位置(范围0~1),这个数组的个数不小于components中存放颜色的个数 count:渐变个数,等于locations的个数 */ CGFloat compoents[12]={ 248.0/255.0,86.0/255.0,86.0/255.0,1, 249.0/255.0,127.0/255.0,127.0/255.0,1, 1.0,1.0,1.0,1.0 }; CGFloat locations[3]={0,0.3,1.0}; CGGradientRef gradient= CGGradientCreateWithColorComponents(colorSpace, compoents, locations, 3);
/*绘制线性渐变 context:图形上下文 gradient:渐变色 startPoint:起始位置 endPoint:终止位置 options:绘制方式,kCGGradientDrawsBeforeStartLocation 开始位置之前就进行绘制,到结束位置之后不再绘制, kCGGradientDrawsAfterEndLocation开始位置之前不进行绘制,到结束点之后继续填充 */ CGContextDrawLinearGradient(context, gradient, CGPointZero, CGPointMake(320, 300), kCGGradientDrawsAfterEndLocation);
//释放颜色空间 CGColorSpaceRelease(colorSpace); }
#pragma mark 径向渐变 -(void)drawRadialGradient:(CGContextRef)context{ //使用rgb颜色空间 CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
/*指定渐变色 space:颜色空间 components:颜色数组,注意由于指定了RGB颜色空间,那么四个数组元素表示一个颜色(red、green、blue、alpha), 如果有三个颜色则这个数组有4*3个元素 locations:颜色所在位置(范围0~1),这个数组的个数不小于components中存放颜色的个数 count:渐变个数,等于locations的个数 */ CGFloat compoents[12]={ 248.0/255.0,86.0/255.0,86.0/255.0,1, 249.0/255.0,127.0/255.0,127.0/255.0,1, 1.0,1.0,1.0,1.0 }; CGFloat locations[3]={0,0.3,1.0}; CGGradientRef gradient= CGGradientCreateWithColorComponents(colorSpace, compoents, locations, 3);
/*绘制径向渐变 context:图形上下文 gradient:渐变色 startCenter:起始点位置 startRadius:起始半径(通常为0,否则在此半径范围内容无任何填充) endCenter:终点位置(通常和起始点相同,否则会有偏移) endRadius:终点半径(也就是渐变的扩散长度) options:绘制方式,kCGGradientDrawsBeforeStartLocation 开始位置之前就进行绘制,但是到结束位置之后不再绘制, kCGGradientDrawsAfterEndLocation开始位置之前不进行绘制,但到结束点之后继续填充 */ CGContextDrawRadialGradient(context, gradient, CGPointMake(160, 284),0, CGPointMake(165, 289), 150, kCGGradientDrawsAfterEndLocation); //释放颜色空间 CGColorSpaceRelease(colorSpace); } @end |
运行效果:
上面我们只是绘制渐变到图形上下文,实际开发中有时候我们还需要填充对应的渐变色,例如现在绘制了一个矩形,如何填充成渐变色呢?在此可以利用渐变裁切来完成(当然利用层CALayer更加方便但这不在今天的话题讨论范围内),特别说明一下区域裁切并不仅仅适用于渐变填充,对于其他图形绘制仍然适用,并且注意裁切只能限于矩形裁切。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
-(void)drawRectWithLinearGradientFill:(CGContextRef)context{ CGColorSpaceRef colorSpace=CGColorSpaceCreateDeviceRGB();
//裁切处一块矩形用于显示,注意必须先裁切再调用渐变 //CGContextClipToRect(context, CGRectMake(20, 50, 280, 300)); //裁切还可以使用UIKit中对应的方法 UIRectClip(CGRectMake(20, 50, 280, 300));
CGFloat compoents[12]={ 248.0/255.0,86.0/255.0,86.0/255.0,1, 249.0/255.0,127.0/255.0,127.0/255.0,1, 1.0,1.0,1.0,1.0 }; CGFloat locations[3]={0,0.3,1.0}; CGGradientRef gradient= CGGradientCreateWithColorComponents(colorSpace, compoents, locations, 3);
CGContextDrawLinearGradient(context, gradient, CGPointMake(20, 50), CGPointMake(300, 300), kCGGradientDrawsAfterEndLocation);
//释放颜色空间 CGColorSpaceRelease(colorSpace); } |
运行效果:
常用的图形上下文状态设置上面基本都用到了,我们不再一一解释,这里着重说一下叠加模式和填充模式,初学者对于这两个状态设置往往容易产生疑惑。
使用Quartz 2D绘图时后面绘制的图像会覆盖前面的,默认情况下如果前面的被覆盖后将看不到后面的内容,但是有时候这个结果并不是我们想要的,因此在Quartz 2D中提供了填充模式供开发者配置调整。由于填充模式类别特别多,因此下面以一个例子来说明:
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 |
-(void)drawRectByUIKitWithContext2:(CGContextRef)context{ CGRect rect= CGRectMake(0, 130.0, 320.0, 50.0); CGRect rect1= CGRectMake(0, 390.0, 320.0, 50.0);
CGRect rect2=CGRectMake(20, 50.0, 10.0, 250.0); CGRect rect3=CGRectMake(40.0, 50.0, 10.0, 250.0); CGRect rect4=CGRectMake(60.0, 50.0, 10.0, 250.0); CGRect rect5=CGRectMake(80.0, 50.0, 10.0, 250.0); CGRect rect6=CGRectMake(100.0, 50.0, 10.0, 250.0); CGRect rect7=CGRectMake(120.0, 50.0, 10.0, 250.0); CGRect rect8=CGRectMake(140.0, 50.0, 10.0, 250.0); CGRect rect9=CGRectMake(160.0, 50.0, 10.0, 250.0); CGRect rect10=CGRectMake(180.0, 50.0, 10.0, 250.0); CGRect rect11=CGRectMake(200.0, 50.0, 10.0, 250.0); CGRect rect12=CGRectMake(220.0, 50.0, 10.0, 250.0); CGRect rect13=CGRectMake(240.0, 50.0, 10.0, 250.0); CGRect rect14=CGRectMake(260.0, 50.0, 10.0, 250.0); CGRect rect15=CGRectMake(280.0, 50.0, 10.0, 250.0);
CGRect rect16=CGRectMake(30.0, 310.0, 10.0, 250.0); CGRect rect17=CGRectMake(50.0, 310.0, 10.0, 250.0); CGRect rect18=CGRectMake(70.0, 310.0, 10.0, 250.0); CGRect rect19=CGRectMake(90.0, 310.0, 10.0, 250.0); CGRect rect20=CGRectMake(110.0, 310.0, 10.0, 250.0); CGRect rect21=CGRectMake(130.0, 310.0, 10.0, 250.0); CGRect rect22=CGRectMake(150.0, 310.0, 10.0, 250.0); CGRect rect23=CGRectMake(170.0, 310.0, 10.0, 250.0); CGRect rect24=CGRectMake(190.0, 310.0, 10.0, 250.0); CGRect rect25=CGRectMake(210.0, 310.0, 10.0, 250.0); CGRect rect26=CGRectMake(230.0, 310.0, 10.0, 250.0); CGRect rect27=CGRectMake(250.0, 310.0, 10.0, 250.0); CGRect rect28=CGRectMake(270.0, 310.0, 10.0, 250.0); CGRect rect29=CGRectMake(290.0, 310.0, 10.0, 250.0);
[[UIColor yellowColor]set]; UIRectFill(rect);
[[UIColor greenColor]setFill]; UIRectFill(rect1);
[[UIColor redColor]setFill]; UIRectFillUsingBlendMode(rect2, kCGBlendModeClear); UIRectFillUsingBlendMode(rect3, kCGBlendModeColor); UIRectFillUsingBlendMode(rect4, kCGBlendModeColorBurn); UIRectFillUsingBlendMode(rect5, kCGBlendModeColorDodge); UIRectFillUsingBlendMode(rect6, kCGBlendModeCopy); UIRectFillUsingBlendMode(rect7, kCGBlendModeDarken); UIRectFillUsingBlendMode(rect8, kCGBlendModeDestinationAtop); UIRectFillUsingBlendMode(rect9, kCGBlendModeDestinationIn); UIRectFillUsingBlendMode(rect10, kCGBlendModeDestinationOut); UIRectFillUsingBlendMode(rect11, kCGBlendModeDestinationOver); UIRectFillUsingBlendMode(rect12, kCGBlendModeDifference); UIRectFillUsingBlendMode(rect13, kCGBlendModeExclusion); UIRectFillUsingBlendMode(rect14, kCGBlendModeHardLight); UIRectFillUsingBlendMode(rect15, kCGBlendModeHue); UIRectFillUsingBlendMode(rect16, kCGBlendModeLighten);
UIRectFillUsingBlendMode(rect17, kCGBlendModeLuminosity); UIRectFillUsingBlendMode(rect18, kCGBlendModeMultiply); UIRectFillUsingBlendMode(rect19, kCGBlendModeNormal); UIRectFillUsingBlendMode(rect20, kCGBlendModeOverlay); UIRectFillUsingBlendMode(rect21, kCGBlendModePlusDarker); UIRectFillUsingBlendMode(rect22, kCGBlendModePlusLighter); UIRectFillUsingBlendMode(rect23, kCGBlendModeSaturation); UIRectFillUsingBlendMode(rect24, kCGBlendModeScreen); UIRectFillUsingBlendMode(rect25, kCGBlendModeSoftLight); UIRectFillUsingBlendMode(rect26, kCGBlendModeSourceAtop); UIRectFillUsingBlendMode(rect27, kCGBlendModeSourceIn); UIRectFillUsingBlendMode(rect28, kCGBlendModeSourceOut); UIRectFillUsingBlendMode(rect29, kCGBlendModeXOR); } |
运行效果:
相信大家对比代码和显示效果并不难发现每种叠加的效果。例子中只是使用UIKit的封装方法进行叠加模式设置,更一般的方法当然是使用CGContextSetBlendMode(CGContextRef context, CGBlendMode mode)方法进行设置。
前面的示例中已经演示过纯色填充、渐变填充,而有时我们需要按一定的自定义样式进行填充,这种方式有点类似于贴瓷砖的方式。我们知道如果家里贴地板或瓷砖时,通常我们会先选择一种瓷砖样式,根据房间面积我们购买不同量的瓷砖。但是不管买多少,这些瓷砖的样式都是一模一样的。填充模式就是为了达到这种效果而产生的:我们只需要绘制一个瓷砖的样式,然后让程序自动调用这种样式填充指定大小的区域。
Quartz 2D支持两种填充模式:有颜色填充和无颜色填充。两种模式使用起来区别很小,有颜色填充就是在绘制瓷砖时就指定颜色,在调用填充时就不用再指定瓷砖颜色;无颜色填充模式就是绘制瓷砖时不用指定任何颜色,在调用填充时再指定具体填充颜色。相比较无颜色填充模式而言,有颜色填充模式更加的灵活,推荐使用。
下面我们具体看一下如何按指定模式进行图形填充:
1.在使用填充模式时首先要构建一个符合CGPatternDrawPatternCallback签名的方法,这个方法专门用来创建“瓷砖”。注意:如果使用有颜色填充模式,需要设置填充色。例如我们定义一个方法drawTile绘制以下瓷砖(有颜色填充):
2.接着需要指定一个填充的颜色空间,这个颜色空间跟前面绘制渐变的颜色空间不太一样,前面创建渐变使用的颜色空间是设备无关的,我们需要基于这个颜色空间创建一个颜色空间专门用于填充(注意对于有颜色填充创建填充颜色空间参数为NULL,不用基于设备无关的颜色空间创建)。
3.然后我们就可以使用CGPatternCreate方法创建一个填充模式,创建填充模式时需要注意其中的参数,在代码中已经做了一一解释(这里注意对于有颜色填充模式isColored设置为true,否则为false)。
4.最后调用CGContextSetFillPattern方法给图形上下文指定填充模式(这个时候注意最后一个参数,如果是有颜色填充模式最后一个参数为透明度alpa的地址,对于无颜色填充模式最后一个参数是当前填充颜色空间的颜色数组)。
5.绘制图形,这里我们绘制一个矩形。
6.释放资源。
下面是具体代码(包含两种填充模式代码,可以一一运行)
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 |
// // UIView4.m // Quartz2D // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. //
#import "KCView.h" #define TILE_SIZE 20
@implementation KCView
-(void)drawRect:(CGRect)rect{ CGContextRef context=UIGraphicsGetCurrentContext(); [self drawBackgroundWithColoredPattern:context]; // [self drawBackgroundWithPattern:context];
}
#pragma mark - 有颜色填充模式 void drawColoredTile(void *info,CGContextRef context){ //有颜色填充,这里设置填充色 CGContextSetRGBFillColor(context, 254.0/255.0, 52.0/255.0, 90.0/255.0, 1); CGContextFillRect(context, CGRectMake(0, 0, TILE_SIZE, TILE_SIZE)); CGContextFillRect(context, CGRectMake(TILE_SIZE, TILE_SIZE, TILE_SIZE, TILE_SIZE)); } -(void)drawBackgroundWithColoredPattern:(CGContextRef)context{ //设备无关的颜色空间 // CGColorSpaceRef rgbSpace= CGColorSpaceCreateDeviceRGB(); //模式填充颜色空间,注意对于有颜色填充模式,这里传NULL CGColorSpaceRef colorSpace=CGColorSpaceCreatePattern(NULL); //将填充色颜色空间设置为模式填充的颜色空间 CGContextSetFillColorSpace(context, colorSpace);
//填充模式回调函数结构体 CGPatternCallbacks callback={0,&drawColoredTile,NULL}; /*填充模式 info://传递给callback的参数 bounds:瓷砖大小 matrix:形变 xStep:瓷砖横向间距 yStep:瓷砖纵向间距 tiling:贴砖的方法 isClored:绘制的瓷砖是否已经指定了颜色(对于有颜色瓷砖此处指定位true) callbacks:回调函数 */ CGPatternRef pattern=CGPatternCreate(NULL, CGRectMake(0, 0, 2*TILE_SIZE, 2*TILE_SIZE), CGAffineTransformIdentity,2*TILE_SIZE+ 5,2*TILE_SIZE+ 5, kCGPatternTilingNoDistortion, true, &callback);
CGFloat alpha=1; //注意最后一个参数对于有颜色瓷砖指定为透明度的参数地址,对于无颜色瓷砖则指定当前颜色空间对应的颜色数组 CGContextSetFillPattern(context, pattern, &alpha);
UIRectFill(CGRectMake(0, 0, 320, 568));
// CGColorSpaceRelease(rgbSpace); CGColorSpaceRelease(colorSpace); CGPatternRelease(pattern); }
#pragma mark - 无颜色填充模式 //填充瓷砖的回调函数(必须满足CGPatternCallbacks签名) void drawTile(void *info,CGContextRef context){ CGContextFillRect(context, CGRectMake(0, 0, TILE_SIZE, TILE_SIZE)); CGContextFillRect(context, CGRectMake(TILE_SIZE, TILE_SIZE, TILE_SIZE, TILE_SIZE)); } -(void)drawBackgroundWithPattern:(CGContextRef)context{ //设备无关的颜色空间 CGColorSpaceRef rgbSpace= CGColorSpaceCreateDeviceRGB(); //模式填充颜色空间 CGColorSpaceRef colorSpace=CGColorSpaceCreatePattern(rgbSpace); //将填充色颜色空间设置为模式填充的颜色空间 CGContextSetFillColorSpace(context, colorSpace);
//填充模式回调函数结构体 CGPatternCallbacks callback={0,&drawTile,NULL}; /*填充模式 info://传递给callback的参数 bounds:瓷砖大小 matrix:形变 xStep:瓷砖横向间距 yStep:瓷砖纵向间距 tiling:贴砖的方法(瓷砖摆放的方式) isClored:绘制的瓷砖是否已经指定了颜色(对于无颜色瓷砖此处指定位false) callbacks:回调函数 */ CGPatternRef pattern=CGPatternCreate(NULL, CGRectMake(0, 0, 2*TILE_SIZE, 2*TILE_SIZE), CGAffineTransformIdentity,2*TILE_SIZE+ 5,2*TILE_SIZE+ 5, kCGPatternTilingNoDistortion, false, &callback);
CGFloat components[]={254.0/255.0,52.0/255.0,90.0/255.0,1.0}; //注意最后一个参数对于无颜色填充模式指定为当前颜色空间颜色数据 CGContextSetFillPattern(context, pattern, components); // CGContextSetStrokePattern(context, pattern, components); UIRectFill(CGRectMake(0, 0, 320, 568));
CGColorSpaceRelease(rgbSpace); CGColorSpaceRelease(colorSpace); CGPatternRelease(pattern); } @end |
运行效果:
这里强调一点,在drawTile回调方法中不要使用UIKit封装方法进行图形绘制(例如UIRectFill等),由于这个方法由Core Graphics内部调用,而Core Graphics考虑到跨平台问题,内部是不允许调用UIKit方法的。
我们知道在UIKit开发中UIView有一个transform属性用于控件的形变,其实在绘图中我们也经常用到图形形变,这个时候可以借助图形上下文的形变方法来完成。在弄清形变之前我们要清楚图形上下文的坐标原点,因为无论是位移还是旋转都是相对于坐标原点进行的。其实Quartz 2D的坐标系同UIKit并不一样,它的坐标原点在屏幕左下方,但是为了统一编程方式,UIKit对其进行了转换,坐标原点统一在屏幕左上角。注意在设置图形上下文形变之前一定要注意保存上下文的初始状态,在使用完之后进行恢复。否则在处理多个图形形变的时候很容易弄不清楚到底是基于怎样的坐标系进行绘图,容易找不到原点(做过html5 canvas绘图的朋友对这一点应该很熟悉,在html5中绘图也经常进行状态保存和恢复)。下面通过一个图片的变换演示一下图形上下文的形变(其他图形也是一样的,就不再演示):
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 |
// // KCView2.m // Quartz2D // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. //
#import "KCView2.h"
@implementation KCView2
-(void)drawRect:(CGRect)rect{ CGContextRef context=UIGraphicsGetCurrentContext(); [self drawImage:context]; }
#pragma mark 图形上下文形变 -(void)drawImage:(CGContextRef)context{ //保存初始状态 CGContextSaveGState(context);
//形变第一步:图形上下文向右平移40 CGContextTranslateCTM(context, 100, 0);
//形变第二步:缩放0.8 CGContextScaleCTM(context, 0.8, 0.8);
//形变第三步:旋转 CGContextRotateCTM(context, M_PI_4/4);
UIImage *image=[UIImage imageNamed:@"photo1.jpg"]; [image drawInRect:CGRectMake(0, 50, 240, 300)];
//恢复到初始状态 CGContextRestoreGState(context); }
@end |
最终运行效果见第四幅截图,下图描绘出了整个程序的运行过程(移动->缩放->旋转):
在前面基本绘图部分,绘制图像时使用了UIKit中封装的方法进行了图像绘制,我们不妨看一下使用Quartz 2D内置方法绘制是什么效果。
1 2 3 4 5 6 |
-(void)drawImage2:(CGContextRef)context{ UIImage *image=[UIImage imageNamed:@"image2.jpg"]; //图像绘制 CGRect rect= CGRectMake(10, 50, 300, 450); CGContextDrawImage(context, rect, image.CGImage); } |
运行效果:
看起来整个图像是倒过来的,原因正是前面说的:在Core Graphics中坐标系的y轴正方向是向上的,坐标原点在屏幕左下角,y轴方向刚好和UIKit中y轴方向相反。而使用UIKit进行绘图之所以没有问题是因为UIKit中进行了处理,事实上对于其他图形即使使用Core Graphics绘制也没有问题,因为UIKit统一了编程方式。但是使用Core Graphics中内置方法绘制图像是存在这种问题的,如何解决呢?
其实图形上下文只要沿着x轴旋转180度,然后向上平移适当的高度即可(但是注意不要沿着z轴旋转,这样得不到想要的结果)。可是通过前面介绍的CGContextRotateCTM方法只能通过沿着z轴旋转,此时不妨使用另外一种方法,那就是在y轴方向缩放-1,同样可以达到想要的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-(void)drawImage2:(CGContextRef)context{ UIImage *image=[UIImage imageNamed:@"image2.jpg"]; CGSize size=[UIScreen mainScreen].bounds.size; CGContextSaveGState(context); CGFloat height=450,y=50; //上下文形变 CGContextScaleCTM(context, 1.0, -1.0);//在y轴缩放-1相当于沿着x张旋转180 CGContextTranslateCTM(context, 0, -(size.height-(size.height-2*y-height)));//向上平移 //图像绘制 CGRect rect= CGRectMake(10, y, 300, height); CGContextDrawImage(context, rect, image.CGImage);
CGContextRestoreGState(context); } |
在UIView的drawRect:中绘制的图形会在控件显示的时候调用(而且显示时会重绘所有图形),有时候我们希望绘制内容的显示是实时的,此时我们就需要调用绘图方法重新绘制,但是在iOS开发中不允许开发者直接调用drawRect:方法,刷新绘制内容需要调用setNeedsDisplay方法。下面以一个调整字体大小的界面演示一下视图的刷新。
KCView.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// // KCView.h // RefreshView // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. //
#import <UIKit/UIKit.h>
@interface KCView : UIView
@property (nonatomic,copy) NSString *title;
@property (nonatomic,assign) CGFloat fontSize;
@end |
KCView.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// // KCView.m // RefreshView // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. //
#import "KCView.h"
@implementation KCView
-(void)drawRect:(CGRect)rect{
NSString *str=_title; UIFont *font=[UIFont fontWithName:@"Marker Felt" size:_fontSize]; UIColor *foreignColor=[UIColor redColor]; [str drawInRect:CGRectMake(100, 120, 300, 200) withAttributes:@{NSFontAttributeName:font,NSForegroundColorAttributeName:foreignColor}]; }
@end |
KCMainViewController.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 |
// // KCMainViewController.m // RefreshView // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. //
#import "KCMainViewController.h" #import "KCView.h"
@interface KCMainViewController (){ KCView *_contentView; NSArray *_fontSize; }
@end
@implementation KCMainViewController
- (void)viewDidLoad { [super viewDidLoad];
[self initLayout];
[self addPickerView]; }
-(void)initLayout{ _fontSize=@[@15,@18,@20,@22,@25,@28,@30,@32,@35,@40]; _contentView=[[KCView alloc]initWithFrame:CGRectMake(0, 0, 320, 300)]; _contentView.backgroundColor=[UIColor whiteColor]; _contentView.title=@"Hello world!"; _contentView.fontSize=[_fontSize[0] intValue]; [self.view addSubview:_contentView]; }
-(void)addPickerView{ UIPickerView *picker=[[UIPickerView alloc]initWithFrame:CGRectMake(0, 300, 320, 268)]; picker.dataSource=self; picker.delegate=self;
[self.view addSubview:picker]; }
-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{ return 1; } -(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{ return _fontSize.count; }
-(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component{ return [NSString stringWithFormat:@"%@号字体",_fontSize[row] ]; } -(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component{ _contentView.fontSize=[[_fontSize objectAtIndex:row] intValue];
//刷新视图 [_contentView setNeedsDisplay]; } @end |
运行效果:
前面我们也说过,Quartz 2D的图形上下方除了可以绘制到层上还可以绘制到位图、PDF等,这里我们就介绍一下如何利用Quartz 2D绘制图像到位图及PDF中。
上面的示例中一直都是在drawRect:方法中利用UIGraphicsGetCurrentContext()方法取得上下文,要得到位图或者PDF的上下文可以利用UIGraphicsBeginImageContext(CGSize size)和UIGraphicsBeginPDFPageWithInfo(CGRect bounds, NSDictionary *pageInfo)方法。位图图形上下文和PDF图形上下文UIKit是不会负责创建的,所以需要用户手动创建,并且在使用完后关闭它。在使用UIKit中系统创建的图形上下文的时候,我们只能在drawRect:方法中使用,由于这两类图形上下文是由我们手动创建的因此可以放到任何方法中调用。此外,这两个方法开启的图形上下文并没有返回值,如果我们要得到我们创建的图形上下文只要在创建上下文之后、关闭之前调用UIGraphicsGetCurrentContext()方法,此时取得的上下文即是我们自己创建的图形上下文。
下面利用位图图形上下文给一个图片添加水印,在下面的程序中我们首先创建上下文,然后在上下文中绘制图片、直线和文本,最后从当前位图上下文中取得最终形成的新图片显示到界面。
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 |
// // KCMainViewController.m // Quartz2D // // Created by Kenshin Cui on 14-3-17. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import <CoreText/CoreText.h> #import "KCMainViewController.h" #import "KCView.h" #import "KCView2.h" #import "KCView3.h"
@interface KCMainViewController ()
@end
@implementation KCMainViewController
- (void)viewDidLoad { [super viewDidLoad];
UIImage *image=[self drawImageAtImageContext]; |