ios 绘图

 

转自:http://www.cnblogs.com/xdream86/archive/2012/12/12/2814552.html

iOS绘图教程

 

  本文是《Programming iOS5》中Drawing一章的翻译,考虑到主题完整性,翻译版本中加入了一些书中未涉及到的内容。但愿本文可以对你有所帮助。html

  本文由海水的味道翻译整理,转载请注明译者和出处,请勿用于商业用途!ios

      Core Graphics Framework是一套基于C的API框架,使用了Quartz做为绘图引擎。它提供了低级别、轻量级、高保真度的2D渲染。该框架能够用于基于路径的绘图、变换、颜色管理、脱屏渲染,模板、渐变、遮蔽、图像数据管理、图像的建立、遮罩以及PDF文档的建立、显示和分析。为了从感官上对这些概念作一个入门的认识,你能够运行一下官方的example code编程

     iOS支持两套图形API族:Core Graphics/QuartZ 2D 和OpenGL ES。OpenGL ES是跨平台的图形API,属于OpenGL的一个简化版本。QuartZ 2D是苹果公司开发的一套API,它是Core Graphics Framework的一部分。须要注意的是:OpenGL ES是应用程序编程接口,该接口描述了方法、结构、函数应具备的行为以及应该如何被使用的语义。也就是说它只定义了一套规范,具体的实现由设备制造商根据规范去作。而每每不少人对接口和实现存在误解。举一个不恰当的比喻:上发条的时钟和装电池的时钟都有相同的可视行为,但二者的内部实现大相径庭。由于制造商能够自由的实现Open GL ES,因此不一样系统实现的OpenGL ES也存在着巨大的性能差别。缓存

       Core Graphics API全部的操做都在一个上下文中进行。因此在绘图以前须要获取该上下文并传入执行渲染的函数中。若是你正在渲染一副在内存中的图片,此时就须要传入图片所属的上下文。得到一个图形上下文是咱们完成绘图任务的第一步,你能够将图形上下文理解为一块画布。若是你没有获得这块画布,那么你就没法完成任何绘图操做。固然,有许多方式得到一个图形上下文,这里我介绍两种最为经常使用的获取方法。安全

  第一种方法就是建立一个图片类型的上下文。调用UIGraphicsBeginImageContextWithOptions函数就可得到用来处理图片的图形上下文。利用该上下文,你就能够在其上进行绘图,并生成图片。调用UIGraphicsGetImageFromCurrentImageContext函数可从当前上下文中获取一个UIImage对象。记住在你全部的绘图操做后别忘了调用UIGraphicsEndImageContext函数关闭图形上下文。app

  第二种方法是利用cocoa为你生成的图形上下文。当你子类化了一个UIView并实现了本身的drawRect:方法后,一旦drawRect:方法被调用,Cocoa就会为你建立一个图形上下文,此时你对图形上下文的全部绘图操做都会显示在UIView上。框架

  判断一个上下文是否为当前图形上下文须要注意的几点:函数

  • UIGraphicsBeginImageContextWithOptions函数不只仅是建立了一个适用于图形操做的上下文,而且该上下文也属于当前上下文。
  • drawRect方法被调用时,UIView的绘图上下文属于当前图形上下文。
  • 回调方法所持有的context:参数并不会让任何上下文成为当前图形上下文。此参数仅仅是对一个图形上下文的引用罢了。

  做为初学者,很容易被UIKit和Core Graphics两个支持绘图的框架迷惑。post

  UIKit性能

  像UIImage、NSString(绘制文本)、UIBezierPath(绘制形状)、UIColor都知道如何绘制本身。这些类提供了功能有限但使用方便的方法来让咱们完成绘图任务。通常状况下,UIKit就是咱们所须要的。

  使用UiKit,你只能在当前上下文中绘图,因此若是你当前处于UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,你就能够直接使用UIKit提供的方法进行绘图。若是你持有一个context:参数,那么使用UIKit提供的方法以前,必须将该上下文参数转化为当前上下文。幸运的是,调用UIGraphicsPushContext 函数能够方便的将context:参数转化为当前上下文,记住最后别忘了调用UIGraphicsPopContext函数恢复上下文环境。

  Core Graphics

  这是一个绘图专用的API族,它常常被称为QuartZ或QuartZ 2D。Core Graphics是iOS上全部绘图功能的基石,包括UIKit。

  使用Core Graphics以前须要指定一个用于绘图的图形上下文(CGContextRef),这个图形上下文会在每一个绘图函数中都会被用到。若是你持有一个图形上下文context:参数,那么你等同于有了一个图形上下文,这个上下文也许就是你须要用来绘图的那个。若是你当前处于UIGraphicsBeginImageContextWithOptions函数drawRect:方法中,并无引用一个上下文。为了使用Core Graphics,你能够调用UIGraphicsGetCurrentContext函数得到当前的图形上下文。

  至此,咱们有了两大绘图框架的支持以及三种得到图形上下文的方法(drawRect:、drawRect: inContext:、UIGraphicsBeginImageContextWithOptions。那么咱们就有6种绘图的形式。若是你有些困惑了,不用怕,我接下来将说明这6种状况。无需担忧尚未具体的绘图命令,你只需关注上下文如何被建立以及咱们是在使用UIKit仍是Core Graphics。

  第一种绘图形式:在UIView的子类方法drawRect:中绘制一个蓝色圆,使用UIKit在Cocoa为咱们提供的当前上下文中完成绘图任务。

- (void) drawRect: (CGRect) rect {

UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];

[[UIColor blueColor] setFill];

[p fill];

}

    第二种绘图形式:使用Core Graphics实现绘制蓝色圆。

- (void) drawRect: (CGRect) rect {

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));

CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);

CGContextFillPath(con);

}

       第三种绘图形式:我将在UIView子类的drawLayer:inContext方法中实现绘图任务。drawLayer:inContext方法是一个绘制图层内容的代理方法。为了可以调用drawLayer:inContext方法,咱们须要设定图层的代理对象。但要注意,不该该将UIView对象设置为显示层的委托对象,这是由于UIView对象已是隐式层的代理对象,再将它设置为另外一个层的委托对象就会出问题。轻量级的作法是:编写负责绘图形的代理类。在MyView.h文件中声明以下代码:

@interface MyLayerDelegate : NSObject

@end

        而后MyView.m文件中实现接口代码:

@implementation MyLayerDelegate

- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx {

  UIGraphicsPushContext(ctx);

  UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];

  [[UIColor blueColor] setFill];

  [p fill];

  UIGraphicsPopContext();

}

@end

直接将代理类的实现代码放在MyView.m文件的#import代码的下面,这样感受好像在使用私有类完成绘图任务(虽然这不是私有类)。须要注意的是,咱们所引用的上下文并非当前上下文,因此为了可以使用UIKit,咱们须要将引用的上下文转变成当前上下文。

由于图层的代理是assign内存管理策略,那么这里就不能以局部变量的形式建立MyLayerDelegate实例对象赋值给图层代理。这里选择在MyView.m中增长一个实例变量,由于实例变量默认是strong:

@interface MyView () {

MyLayerDelegate* _layerDeleagete;

}

@end

    使用该图层代理:

MyView *myView = [[MyView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];

CALayer *myLayer = [CALayer layer];

_layerDelegate = [[MyLayerDelegate alloc] init];

myLayer.delegate = _layerDelegate;

[myView.layer addSublayer:myLayer];

[myView setNeedsDisplay]; // 调用此方法,drawLayer: inContext:方法才会被调用。

 第四种绘图形式: 使用Core Graphics在drawLayer:inContext方法中实现一样操做,代码以下:

- (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con {

CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));

CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);

CGContextFillPath(con);

}

  最后,演示UIGraphicsBeginImageContextWithOptions的用法,并从上下文中生成一个UIImage对象。生成UIImage对象的代码并不须要等待某些方法被调用后或在UIView的子类中才能去作。

  第五种绘图形式: 使用UIKit实现:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);

UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];

[[UIColor blueColor] setFill];

[p fill];

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

  解释一下UIGraphicsBeginImageContextWithOptions函数参数的含义:第一个参数表示所要建立的图片的尺寸;第二个参数用来指定所生成图片的背景是否为不透明,如上咱们使用YES而不是NO,则咱们获得的图片背景将会是黑色,显然这不是我想要的;第三个参数指定生成图片的缩放因子,这个缩放因子与UIImage的scale属性所指的含义是一致的。传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,因此咱们获得的图片无论是在单分辨率仍是视网膜屏上看起来都会很好。

   第六种绘图形式: 使用Core Graphics实现:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));

CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);

CGContextFillPath(con);

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

  UIKit和Core Graphics能够在相同的图形上下文中混合使用。在iOS 4.0以前,使用UIKit和UIGraphicsGetCurrentContext被认为是线程不安全的。而在iOS4.0之后苹果让绘图操做在第二个线程中执行解决了此问题。

  UIImage经常使用的绘图操做

  一个UIImage对象提供了向当前上下文绘制自身的方法。咱们如今已经知道如何获取一个图片类型的上下文并将它转变成当前上下文。

  平移操做:下面的代码展现了如何将UIImage绘制在当前的上下文中。

UIImage* mars = [UIImage imageNamed:@"Mars.png"];

CGSize sz = [mars size];

UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*2, sz.height), NO, 0);

[mars drawAtPoint:CGPointMake(0,0)];

[mars drawAtPoint:CGPointMake(sz.width,0)];

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

UIImageView* iv = [[UIImageView alloc] initWithImage:im];

[self.window.rootViewController.view addSubview: iv];

    iv.center = self.window.center;

 

图1 UIImage平移处理

  缩放操做:下面代码展现了如何对UIImage进行缩放操做:

UIImage* mars = [UIImage imageNamed:@"Mars.png"];

CGSize sz = [mars size];

UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*2, sz.height*2), NO, 0);

[mars drawInRect:CGRectMake(0,0,sz.width*2,sz.height*2)];

[mars drawInRect:CGRectMake(sz.width/2.0, sz.height/2.0, sz.width, sz.height) blendMode:kCGBlendModeMultiply alpha:1.0];

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

 

图2 UIImage缩放处理

  UIImage没有提供截取图片指定区域的功能。但经过建立一个较小的图形上下文并移动图片到一个适当的图形上下文坐标系内,指定区域内的图片就会被获取。

  裁剪操做:下面代码展现了如何获取图片的右半边:

UIImage* mars = [UIImage imageNamed:@"Mars.png"];

CGSize sz = [mars size];

UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width/2.0, sz.height), NO, 0);

[mars drawAtPoint:CGPointMake(-sz.width/2.0, 0)];

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

  以上的代码首先建立一个一半图片宽度的图形上下文,而后将图片左上角原点移动到与图形上下文负X坐标对齐,从而让图片只有右半部分与图形上下文相交。

 

图3 UIImage裁剪原理

  CGImage经常使用的绘图操做

  UIImage的Core Graphics版本是CGImage(具体类型是CGImageRef)。二者能够直接相互转化: 使用UIImage的CGImage属性能够访问Quartz图片数据;将CGImage做为UIImage方法imageWithCGImage:initWithCGImage:的参数建立UIImage对象。

  一个CGImage对象可让你获取原始图片中指定区域的图片(也能够获取指定区域外的图片,UIImage却办不到)。

  下面的代码展现了将图片拆分红两半,并分别绘制在上下文的左右两边:

UIImage* mars = [UIImage imageNamed:@"Mars.png"];

// 抽取图片的左右半边

CGSize sz = [mars size];

CGImageRef marsLeft = CGImageCreateWithImageInRect([mars CGImage],CGRectMake(0,0,sz.width/2.0,sz.height));

CGImageRef marsRight = CGImageCreateWithImageInRect([mars CGImage],CGRectMake(sz.width/2.0,0,sz.width/2.0,sz.height));

// 将每个CGImage绘制到图形上下文中

UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5, sz.height), NO, 0);

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height), marsLeft);

CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height), marsRight);

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

// 记得释放内存,ARC在这里无效

CGImageRelease(marsLeft);

CGImageRelease(marsRight);

  你也许发现绘出的图是上下颠倒的!图片的颠倒并非由于被旋转了。当你建立了一个CGImage并使用CGContextDrawImage方法绘图就会引发这种问题。这主要是由于原始的本地坐标系统(坐标原点在左上角)与目标上下文(坐标原点在左下角)不匹配。有不少方法能够修复这个问题,其中一种方法就是使用CGContextDrawImage方法先将CGImage绘制到UIImage上,而后获取UIImage对应的CGImage,此时就获得了一个倒转的CGImage。当再调用CGContextDrawImage方法,咱们就将倒转的图片还原回来了。实现代码以下:

CGImageRef flip (CGImageRef im) {

CGSize sz = CGSizeMake(CGImageGetWidth(im), CGImageGetHeight(im));

UIGraphicsBeginImageContextWithOptions(sz, NO, 0);

CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, sz.width, sz.height), im);

CGImageRef result = [UIGraphicsGetImageFromCurrentImageContext() CGImage];

UIGraphicsEndImageContext();

return result;

}

  如今将以前的代码修改以下:

CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height), flip(marsLeft));

CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height), flip(marsRight));

  然而,这里又出现了另一个问题:在双分辨率的设备上,若是咱们的图片文件是高分辨率(@2x)版本,上面的绘图就是错误的。缘由在于对于UIImage来讲,在加载原始图片时使用imageNamed:方法,它会自动根据所在设备的分辨率类型选择图片,而且UIImage经过设置用来适配的scale属性补偿图片的两倍尺寸。可是一个CGImage对象并无scale属性,它不知道图片文件的尺寸是否为两倍!因此当调用UIImage的CGImage方法,你不能假定所得到的CGImage尺寸与原始UIImage是同样的。在单分辨率和双分辨率下,一个UIImage对象的size属性值都是同样的,可是双分辨率UIImage对应的CGImage是单分辨率UIImage对应的CGImage的两倍大。因此咱们须要修改上面的代码,让其在单双分辨率下均可以工做。代码以下:

UIImage* mars = [UIImage imageNamed:@"Mars.png"];

CGSize sz = [mars size];

// 转换CGImage并使用对应的CGImage尺寸截取图片的左右部分

CGImageRef marsCG = [mars CGImage];

CGSize szCG = CGSizeMake(CGImageGetWidth(marsCG), CGImageGetHeight(marsCG));

CGImageRef marsLeft = CGImageCreateWithImageInRect(marsCG,CGRectMake(0,0,szCG.width/2.0,szCG.height));

CGImageRef marsRight = CGImageCreateWithImageInRect(marsCG, CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height));

UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5, sz.height), NO, 0);

//剩下的和以前的代码同样,修复倒置问题

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height),flip(marsLeft));

CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height),flip(marsRight));

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

CGImageRelease(marsLeft);

CGImageRelease(marsRight);

  上面的代码初看上去很繁杂,不过不用担忧,这里还有另外一种修复倒置问题的方案。相对于使用flip函数,你能够在绘图以前将CGImage包装进UIImage中,这样作有两大优势:

  • 当UIImage绘图时它会自动修复倒置问题
  • 当你从CGImage转化为Uimage时,可调用imageWithCGImage:scale:orientation:方法生成CGImage做为对缩放性的补偿。

  因此这是一个解决倒置和缩放问题的自包含方法。

  代码以下:

UIImage* mars = [UIImage imageNamed:@"Mars.png"];

CGSize sz = [mars size];

CGImageRef marsCG = [mars CGImage];

CGSize szCG = CGSizeMake(CGImageGetWidth(marsCG), CGImageGetHeight(marsCG));

CGImageRef marsLeft = CGImageCreateWithImageInRect(marsCG, CGRectMake(0,0,szCG.width/2.0,szCG.height));

CGImageRef marsRight = CGImageCreateWithImageInRect(marsCG, CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height));

UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5, sz.height), NO, 0);

[[UIImage imageWithCGImage:marsLeft scale:[mars scale] orientation:UIImageOrientationUp] drawAtPoint:CGPointMake(0,0)];

[[UIImage imageWithCGImage:marsRight scale:[mars scale] orientation:UIImageOrientationUp] drawAtPoint:CGPointMake(sz.width,0)];

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

CGImageRelease(marsLeft); CGImageRelease(marsRight); 

 还有另外一种解决倒置问题的方案是在绘制CGImage以前,对上下文应用变换操做,有效地倒置上下文的内部坐标系统。这里先不作讨论。

 为何会发生倒置问题

 究其缘由是由于Core Graphics源于Mac OS X系统,在Mac OS X中,坐标原点在左下方而且正y坐标是朝上的,而在iOS中,原点坐标是在左上方而且正y坐标是朝下的。在大多数状况下,这不会出现任何问题,由于图形上下文的坐标系统是会自动调节补偿的。可是建立和绘制一个CGImage对象时就会暴露出倒置问题。 

  CIFilterCIImage

  CIFilter与CIImage是iOS 5新引入的,虽然它们已在MAX OS X系统中存在多年。前缀“CI”表示Core Image,这是一种使用数学滤镜变换图片的技术。可是你不要去幻想iOS提供了像Photoshop软件那样强大的滤镜功能。使用Core Image以前你须要将CoreImage.framework框架导入到你的target之中。

  所谓滤镜指的是CIFilter类,滤镜可被分为如下几类:

  模板与渐变类

  这两类滤镜建立的CIImage能够和其余的CIImage进行合并,好比一种单色,一个棋盘,条纹,亦或是渐变。

  合成类 

  此类滤镜能够将一张图片与另外的图片合并,合成滤镜模式常见于图形处理软件Photoshop中。

  色彩类

  此滤镜调整、修改图片的色彩。所以你能够改变一张图片的饱和度、色度、亮度、对比度、伽马、白点、曝光度、阴影、高亮等属性。

  几何变换类

  此类滤镜可对图片执行基本的几何变换,好比缩放、旋转、裁剪。

    CIFilter使用起来很是的简单。CIFilter看上去就像一个由键值组成的字典。它生成一个CIImage对象做为其输出。通常地,一个滤镜有一个或多个输入,而对于部分滤镜,生成的图片是基于其余类型的参数值。CIFilter对象是一个集合,可以使用键值对进行检索。经过提供滤镜的字符串名称建立一个滤镜,若是想知道有哪些滤镜,能够查询苹果的Core Image Filter Reference文档,或是调用CIFilter的类方法filterNamesInCategories,参数值为nil。每个滤镜拥有一小部分用来肯定其行为的键值。若是你想修改某一个键(好比亮度键)对应的值,你能够调用setValueforKey方法或当你指定一个滤镜名时提供全部键值对。

     须要处理的图片必须是CIImage类型,调用initWithCGImage方法可得到CIImage。由于CGImage又是做为滤镜的输出,所以滤镜之间可被链接在一块儿(将滤镜的输出做为initWithCGImage方法的输入参数)

   当你构建一个滤镜链时,并无作复杂的运算。只有当整个滤镜链须要输出一个CGImage时,密集型计算才会发生。调用contextWithOptionscreateCGImage: fromRect:方法建立CIContext。与以往不一样的地方是CIImage没有frame与bounds属性;只有extent属性。你将很是频繁的使用这个属性做为createCGImage: fromRect:方法的第二个参数。

   接下来我将演示Core Image的使用。首先建立一个径向渐变的滤镜,该滤镜是从白到黑的渐变方式,白色区域的半径默认是100。接着将其与一张使用CIDarkenBlendMode滤镜的图片合成。CIDarkenBlendMode的做用是背景图片样本将被源图片的黑色部分替换掉。

代码以下:

UIImage* moi = [UIImage imageNamed:@"Mars.jpeg"];

CIImage* moi2 = [[CIImage alloc] initWithCGImage:moi.CGImage];

CIFilter* grad = [CIFilter filterWithName:@"CIRadialGradient"];

CIVector* center = [CIVector vectorWithX:moi.size.width / 2.0 Y:moi.size.height / 2.0];

// 使用setValue:forKey:方法设置滤镜属性

[grad setValue:center forKey:@"inputCenter"];

// 在指定滤镜名时提供全部滤镜键值对

CIFilter* dark = [CIFilter filterWithName:@"CIDarkenBlendMode" keysAndValues:@"inputImage", grad.outputImage, @"inputBackgroundImage", moi2, nil];

CIContext* c = [CIContext contextWithOptions:nil];

CGImageRef moi3 = [c createCGImage:dark.outputImage fromRect:moi2.extent];

UIImage* moi4 = [UIImage imageWithCGImage:moi3 scale:moi.scale orientation:moi.imageOrientation];

CGImageRelease(moi3);

 

  图4 图片合成快照 

  这个例子可能没有什么吸引人的地方,由于全部一切均可以使用Core Graphics完成。除了Core Image是使用GPU处理,可能有点吸引人。Core Graphics也能够作到径向渐变并使用混合模式合成图片。但Core Image要简单得多,特别是当你有多个图片输入想重用一个滤镜链时。而且Core Image的颜色调整功能比Core Graphics更增强大。对了,Core Image还能实现自动人脸识别哦!

  绘制一个UIView

  绘制一个UIVIew最灵活的方式就是由它本身完成绘制。实际上你不是绘制一个UIView,你只是子类化了UIView并赋予子类绘制本身的能力。当一个UIVIew须要执行绘图操做的时, drawRect:方法就会被调用。覆盖此方法让你得到绘图操做的机会。当drawRect方法被调用,当前图形上下文也被设置为属于视图的图形上下文。你可使用Core Graphics或UIKit提供的方法将图形画到该上下文中。

  你不该该手动调用drawRect方法!若是你想调用drawRect:方法更新视图,只需发送setNeedsDisplay方法。这将使得drawRect:方法会在下一个适当的时间调用。固然,不要覆盖drawRect方法除非你知道这样作绝对合法。比方说,在UIImageView子类中覆盖drawRect方法是不合法的,你将得不到你绘制的图形。

       在UIView子类的drawRect方法中无需调用super,由于自己UIView的drawRect方法是空的。为了提升一些绘图性能,你能够调用setNeedsDisplayInRect方法从新绘制视图的子区域,而视图的其余部分依然保持不变。

       通常状况下,你不该该过早的进行优化。绘图代码可能看上去很是的繁琐,但它们是很是快的。而且iOS绘图系统自身也是很是高效,它不会频繁调用drawRect方法,除非无可奈何(或调用了setNeedsDisplay方法)。一旦一个视图已由本身绘制完成,那么绘制的结果会被缓存下来留待重用,而不是每次重头再来。(苹果公司将缓存绘图称为视图的位图存储回填(bitmap backing store))。你可能会发现drawRect方法中的代码在整个应用程序生命周期内只被调用了一次!事实上,将代码移到drawRect方法中是提升性能的广泛作法。这是由于绘图引擎直接对屏幕进行渲染相对于先是脱屏渲染而后再将像素拷贝到屏幕要来的高效。

       当视图的backgroundColor为nil而且opaque属性为YES,视图的背景颜色就会变成黑色。

  Core Graphics上下文属性设置

  当你在图形上下文中绘图时,当前图形上下文的相关属性设置将决定绘图的行为与外观。所以,绘图的通常过程是先设定好图形上下文参数,而后绘图。比方说,要画一根红线,接着画一根蓝线。那么首先须要将上下文的线条颜色属性设定为为红色,而后画红线;接着设置上下文的线条颜色属性为蓝色,再画出蓝线。表面上看,红线和蓝线是分开的,但事实上,在你画每一条线时,线条颜色倒是整个上下文的属性。不管你用的是UIKit方法仍是Core Graphics函数。

  由于图形上下文在每一时刻都有一个肯定的状态,该状态归纳了图形上下文全部属性的设置。为了便于操做这些状态,图形上下文提供了一个用来持有状态的栈。调用CGContextSaveGState函数,上下文会将完整的当前状态压入栈顶;调用CGContextRestoreGState函数,上下文查找处在栈顶的状态,并设置当前上下文状态为栈顶状态。

  所以通常绘图模式是:在绘图以前调用CGContextSaveGState函数保存当前状态,接着根据须要设置某些上下文状态,而后绘图,最后调用CGContextRestoreGState函数将当前状态恢复到绘图以前的状态。要注意的是,CGContextSaveGState函数和CGContextRestoreGState函数必须成对出现,不然绘图极可能出现意想不到的错误,这里有一个简单的作法避免这种状况。代码以下:

- (void)drawRect:(CGRect)rect {

CGContextRef ctx = UIGraphicsGetCurrentContext();

CGContextSaveGState(ctx);

{

// 绘图代码

}

CGContextRestoreGState(ctx);

    }  

  但你不须要在每次修改上下文状态以前都这样作,由于你对某一上下文属性的设置并不必定会和以前的属性设置或其余的属性设置产生冲突。你彻底能够在不调用保存和恢复函数的状况下先设置线条颜色为红色,而后再设置为蓝色。但在必定状况下,你但愿你对状态的设置是可撤销的,我将在接下来讨论这样的状况。

  许多的属性组成了一个图形上下文状态,这些属性设置决定了在你绘图时图形的外观和行为。下面我列出了一些属性和对应修改属性的函数;虽然这些函数是关于Core Graphics的,但记住,实际上UIKit一样是调用这些函数操纵上下文状态。

  线条的宽度和线条的虚线样式

  CGContextSetLineWidthCGContextSetLineDash

  线帽和线条联接点样式

  CGContextSetLineCapCGContextSetLineJoinCGContextSetMiterLimit

  线条颜色和线条模式

  CGContextSetRGBStrokeColorCGContextSetGrayStrokeColorCGContextSetStrokeColorWithColorCGContextSetStrokePattern

  填充颜色和模式

  CGContextSetRGBFillColor,CGContextSetGrayFillColor,CGContextSetFillColorWithColor, CGContextSetFillPattern

  阴影

  CGContextSetShadowCGContextSetShadowWithColor

  混合模式

  CGContextSetBlendMode(决定你当前绘制的图形与已经存在的图形如何被合成)

  总体透明度

  CGContextSetAlpha(个别颜色也具备alpha成分)

  文本属性

  CGContextSelectFontCGContextSetFontCGContextSetFontSizeCGContextSetTextDrawingModeCGContextSetCharacterSpacing

  是否开启反锯齿和字体平滑

  CGContextSetShouldAntialiasCGContextSetShouldSmoothFonts

  另一些属性设置:

  裁剪区域:在裁剪区域外绘图不会被实际的画出来。

  变换(或称为“CTM“,意为当前变换矩阵): 改变你随后指定的绘图命令中的点如何被映射到画布的物理空间。

  许多这些属性设置接下来我都会举例说明。

  路径与绘图

  经过编写移动虚拟画笔的代码描画一段路径,这样的路径并不构成一个图形。绘制路径意味着对路径描边或填充该路径,也或者二者都作。一样,你应该从某些绘图程序中获得过类似的体会。

  一段路径是由点到点的描画构成。想象一下绘图系统是你手里的一只画笔,你首先必需要设置画笔当前所处的位置,而后给出一系列命令告诉画笔如何描画随后的每段路径。每一段新增的路径开始于当前点,当完成一条路径的描画,路径的终点就变成了当前点。

    下面列出了一些路径描画的命令:

    定位当前点

    CGContextMoveToPoint

    描画一条线

    CGContextAddLineToPointCGContextAddLines

    描画一个矩形

    CGContextAddRectCGContextAddRects

    描画一个椭圆或圆形

    CGContextAddEllipseInRect

    描画一段圆弧

    CGContextAddArcToPointCGContextAddArc

    经过一到两个控制点描画一段贝赛尔曲线

    CGContextAddQuadCurveToPointCGContextAddCurveToPoint

    关闭当前路径

    CGContextClosePath 这将从路径的终点到起点追加一条线。若是你打算填充一段路径,那么就不须要使用该命令,由于该命令会被自动调用。

    描边或填充当前路径

    CGContextStrokePathCGContextFillPathCGContextEOFillPathCGContextDrawPath。对当前路径描边或填充会清除掉路径。若是你只想使用一条命令完成描边和填充任务,可使用CGContextDrawPath命令,由于若是你只是使用CGContextStrokePath对路径描边,路径就会被清除掉,你就不能再对它进行填充了。

    建立路径并描边路径或填充路径只需一条命令就可完成的函数:CGContextStrokeLineSegmentsCGContextStrokeRectCGContextStrokeRectWithWidthCGContextFillRectCGContextFillRectsCGContextStrokeEllipseInRectCGContextFillEllipseInRect

    一段路径是被合成的,意思是它是由多条独立的路径组成。举个例子,一条单独的路径可能由两个独立的闭合形状组成:一个矩形和一个圆形。当你在构造一条路径的中间过程(意思是在描画了一条路径后没有调用描边或填充命令,或调用CGContextBeginPath函数来清除路径)调用CGContextMoveToPoint函数,就像是你拾起画笔,并将画笔移动到一个新的位置,如此来准备开始一段独立的相同路径。若是你担忧当你开始描画一条路径的时候,已经存在的路径和新的路径会被认为是已存在路径的一个合成部分,你能够调用CGContextBeginPath函数指定你绘制的路径是一条独立的路径;苹果的许多例子都是这样作的,但在实际开发中我发现这是非必要的。

    CGContextClearRect函数的功能是擦除一个区域。这个函数会擦除一个矩形内的全部已存在的绘图;并对该区域执行裁剪。结果像是打了一个贯穿全部已存在绘图的孔。

    CGContextClearRect函数的行为依赖于上下文是透明仍是不透明。当在图形上下文中绘图时,这会尤其明显和直观。若是图片上下文是透明的(UIGraphicsBeginImageContextWithOptions第二个参数为NO),那么CGContextClearRect函数执行擦除后的颜色为透明,反之则为黑色。

    当在一个视图中直接绘图(使用drawRect:drawLayer:inContext:方法),若是视图的背景颜色为nil或颜色哪怕有一点点透明度,那么CGContextClearRect的矩形区域将会显示为透明的,打出的孔将穿过视图包括它的背景颜色。若是背景颜色彻底不透明,那么CGContextClearRect函数的结果将会是黑色。这是由于视图的背景颜色决定了是否视图的图形上下文是透明的仍是不透明的。

 

图5 CGContextClearRect函数的应用

    如图5,在左边的蓝色正方形被挖去部分留为黑色,然而在右边的蓝色正方形也被挖去部分留为透明。但这两个正方形都是UIView子类的实例,采用相同的绘图代码!不一样之处在于视图的背景颜色,左边的正方形的背景颜色在nib文件中

  可是这却彻底改变了CGContextClearRect函数的效果。UIView子类的drawRect:方法看起来像这样:

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);

CGContextFillRect(con, rect);

CGContextClearRect(con, CGRectMake(0,0,30,30));

  为了说明典型路径的描画命令,我将生成一个向上的箭头图案,我谨慎避免使用便利函数操做,也许这不是建立箭头最好的方式,但依然清楚的展现了各类典型命令的用法。

 

图6 一个简单的路径绘图

CGContextRef con = UIGraphicsGetCurrentContext();

// 绘制一个黑色的垂直黑色线,做为箭头的杆子

CGContextMoveToPoint(con, 100, 100);

CGContextAddLineToPoint(con, 100, 19);

CGContextSetLineWidth(con, 20);

CGContextStrokePath(con);

// 绘制一个红色三角形箭头

CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);

CGContextMoveToPoint(con, 80, 25);

CGContextAddLineToPoint(con, 100, 0);

CGContextAddLineToPoint(con, 120, 25);

CGContextFillPath(con);

// 从箭头杆子上裁掉一个三角形,使用清除混合模式

CGContextMoveToPoint(con, 90, 101);

CGContextAddLineToPoint(con, 100, 90);

CGContextAddLineToPoint(con, 110, 101);

CGContextSetBlendMode(con, kCGBlendModeClear);

CGContextFillPath(con);

  确切的说,为了以防万一,咱们应该在绘图代码周围使用CGContextSaveGStateCGContextRestoreGState函数。可对于这个例子来讲,添加与否不会有任何的区别。由于上下文在调用drawRect:方法中不会被持久,因此不会被破坏。

  若是一段路径须要重用或共享,你能够将路径封装为CGPath(具体类型是CGPathRef)。你能够建立一个新的CGMutablePathRef对象并使用多个相似于图形的路径函数的CGPath函数构造路径,或者使用CGContextCopyPath函数复制图形上下文的当前路径。有许多CGPath函数可用于建立基于简单几何形状的路径(CGPathCreateWithRect、CGPathCreateWithEllipseInRect)或基于已存在路径(CGPathCreateCopyByStrokingPath、CGPathCreateCopyDashingPath、CGPathCreateCopyByTransformingPath)。

  UIKit的UIBezierPath类包装了CGPath。它提供了用于绘制某种形状路径的方法,以及用于描边、填充、存取某些当前上下文状态的设置方法。相似地,UIColor提供了用于设置当前上下文描边与填充的颜色。所以咱们能够重写咱们以前绘制箭头的代码:

UIBezierPath* p = [UIBezierPath bezierPath];

[p moveToPoint:CGPointMake(100,100)];

[p addLineToPoint:CGPointMake(100, 19)];

[p setLineWidth:20];

[p stroke];

[[UIColor redColor] set];

[p removeAllPoints];

[p moveToPoint:CGPointMake(80,25)];

[p addLineToPoint:CGPointMake(100, 0)];

[p addLineToPoint:CGPointMake(120, 25)];

[p fill];

[p removeAllPoints];

[p moveToPoint:CGPointMake(90,101)];

[p addLineToPoint:CGPointMake(100, 90)];

[p addLineToPoint:CGPointMake(110, 101)];

[p fillWithBlendMode:kCGBlendModeClear alpha:1.0];

  在这种特殊状况下,完成一样的工做并无节省多少代码,可是UIBezierPath仍然仍是有用的。若是你须要对象特性,UIBezierPath提供了一个便利方法:bezierPathWithRoundedRectcornerRadius,它可用于绘制带有圆角的矩形,若是是使用Core Graphics就至关冗长乏味了。还能够只让圆角出如今左上角和右上角。

- (void)drawRect:(CGRect)rect {

  CGContextRef ctx = UIGraphicsGetCurrentContext();

  CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);

  CGContextSetLineWidth(ctx, 3);

  UIBezierPath *path;

  path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 100, 100, 100) byRoundingCorners:(UIRectCornerTopLeft |UIRectCornerTopRight) cornerRadii:CGSizeMake(10, 10)];

  [path stroke];

}

 

图7 左右圆角矩形

  裁剪

  路径的另外一用处是遮蔽区域,以防对遮蔽区域进一步绘图。这种用法被称为裁剪。裁剪区域外的图形不会被绘制到。默认状况下,一个图形上下文的裁剪区域是整个图形上下文。你可在上下文中的任何地方绘图。

  总的来讲,裁剪区域是上下文的一个特性。与已存在的裁剪区域相交会出现新的裁剪区域。因此若是你应用了你本身的裁剪区域,稍后将它从图形上下文中移除的作法是使用CGContextSaveGStateCGContextRestoreGState函数将代码包装起来。

    为了便于说明这一点,我使用裁剪而不是使用混合模式在箭头杆子上打孔的方法重写了生成箭头的代码。这样作有点小复杂,由于咱们想要裁剪区域不在三角形内而在三角形外部。为了代表这一点,咱们使用了一个三角形和一个矩形组成了一个组合路径。

    当填充一个组合路径并使用它表示一个裁剪区域时,系统遵循如下两规则之一:

  环绕规则(Winding rule)

  若是边界是顺时针绘制,那么在其内部逆时针绘制的边界所包含的内容为空。若是边界是逆时针绘制,那么在其内部顺时针绘制的边界所包含的内容为空。

  奇偶规则

  最外层的边界表明内部都有效,都要填充;以后向内第二个边界表明它的内部无效,不需填充;如此规则继续向内寻找边界线。咱们的状况很是简单,因此使用奇偶规则就很容易了。这里咱们使用CGContextEOCllip设置裁剪区域而后进行绘图。(若是不是很明白,能够参见这篇文章:五种方法绘制有孔的2d形状

CGContextRef con = UIGraphicsGetCurrentContext();

// 在上下文裁剪区域中挖一个三角形状的孔

CGContextMoveToPoint(con, 90, 100);

CGContextAddLineToPoint(con, 100, 90);

CGContextAddLineToPoint(con, 110, 100);

CGContextClosePath(con);

CGContextAddRect(con, CGContextGetClipBoundingBox(con));

// 使用奇偶规则,裁剪区域为矩形减去三角形区域

CGContextEOClip(con);

// 绘制垂线

CGContextMoveToPoint(con, 100, 100);

CGContextAddLineToPoint(con, 100, 19);

CGContextSetLineWidth(con, 20);

CGContextStrokePath(con);

// 画红色箭头

CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);

CGContextMoveToPoint(con, 80, 25);

CGContextAddLineToPoint(con, 100, 0);

CGContextAddLineToPoint(con, 120, 25);

CGContextFillPath(con); 

  渐变

  渐变能够很简单也能够很复杂。一个简单的渐变(接下来要讨论的)由一端点的颜色与另外一端点的颜色决定,若是在中间点加入颜色(可选),那么渐变会在上下文的两个点之间线性的绘制或在上下文的两个圆之间放射状的绘制。不能使用渐变做为路径的填充色,但可以使用裁剪限制对路径形状的渐变。

  我重写了绘制箭头的代码,箭杆使用了线性渐变。效果如图7所示。

 

图8 箭头杆子渐变

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextSaveGState(con);

// 在上下文裁剪区域挖一个三角形孔

CGContextMoveToPoint(con, 90, 100);

CGContextAddLineToPoint(con, 100, 90);

CGContextAddLineToPoint(con, 110, 100);

CGContextClosePath(con);

CGContextAddRect(con, CGContextGetClipBoundingBox(con));

CGContextEOClip(con);

//绘制一个垂线,让它的轮廓形状成为裁剪区域

CGContextMoveToPoint(con, 100, 100);

CGContextAddLineToPoint(con, 100, 19);

CGContextSetLineWidth(con, 20);

// 使用路径的描边版本替换图形上下文的路径

CGContextReplacePathWithStrokedPath(con);

// 对路径的描边版本实施裁剪

CGContextClip(con);

// 绘制渐变

CGFloat locs[3] = { 0.0, 0.5, 1.0 };

CGFloat colors[12] = {

0.3,0.3,0.3,0.8, // 开始颜色,透明灰

0.0,0.0,0.0,1.0, // 中间颜色,黑色

0.3,0.3,0.3,0.8 // 末尾颜色,透明灰

};

CGColorSpaceRef sp = CGColorSpaceCreateDeviceGray();

CGGradientRef grad = CGGradientCreateWithColorComponents (sp, colors, locs, 3);

CGContextDrawLinearGradient(con, grad, CGPointMake(89,0), CGPointMake(111,0), 0);

CGColorSpaceRelease(sp);

CGGradientRelease(grad);

CGContextRestoreGState(con); // 完成裁剪

// 绘制红色箭头

CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);

CGContextMoveToPoint(con, 80, 25);

CGContextAddLineToPoint(con, 100, 0);

CGContextAddLineToPoint(con, 120, 25);

CGContextFillPath(con);

  调用CGContextReplacePathWithStrokedPath函数伪装对当前路径描边,并使用当前线段宽度和与线段相关的上下文状态设置。但接着建立的是描边路径外部的一个新的路径。所以,相对于使用粗的线条,咱们使用了一个矩形区域做为裁剪区域。

  虽然过程比较冗长可是很是的简单;咱们将渐变描述为一组在一端点(0.0)和另外一端点(1.0)之间连续区上的位置,以及设置与每一个位置相对应的颜色。为了提亮边缘的渐变,加深中间的渐变,我使用了三个位置,黑色点的位置是0.5。为了建立渐变,还须要提供一个颜色空间。最后,我建立出了该渐变,并对裁剪区域绘制线性渐变,最后释放了颜色空间和渐变。

  颜色与模板

  在iOS中,CGColor表示颜色(具体类型为CGColorRef)。使用UIColor的colorWithCGColor:和CGColor方法可bridged cast到UIColor。

  在iOS中,模板表示为CGPattern(具体类型为CGPatternRef)。你能够建立一个模板并使用它进行描边或填充。其过程是至关复杂的。做为一个很是简单的例子,我将使用红蓝相间的三角形替换箭头的三角形部分。如今移除下面行:

  CGContextSetFillColorWithColor(con, [UIColor redColor].CGColor));

  在被移除的地方填入下面代码:

CGColorSpaceRef sp2 = CGColorSpaceCreatePattern(NULL);

CGContextSetFillColorSpace (con, sp2);

CGColorSpaceRelease (sp2);

CGPatternCallbacks callback = {0, &drawStripes, NULL };

CGAffineTransform tr = CGAffineTransformIdentity;

CGPatternRef patt = CGPatternCreate(NULL,CGRectMake(0,0,4,4), tr, 4, 4, kCGPatternTilingConstantSpacingMinimalDistortion, true, &callback);

CGFloat alph = 1.0;

CGContextSetFillPattern(con, patt, &alph);

CGPatternRelease(patt);

  代码很是冗长,但它倒是一个完整的样板。如今咱们从后往前分析代码: 咱们调用CGContextSetFillPattern不是设置填充颜色,咱们设置的是填充的模板。函数的第三个参数是一个指向CGFloat的指针,因此咱们事先设置CGFloat自身。第二个参数是一个CGPatternRef对象,因此咱们须要事先建立CGPatternRef,并在最后释放它。

  如今开始讨论CGPatternCreate。一个模板是在一个矩形元中的绘图。咱们须要矩形元的尺寸(第二个参数)以及矩形元原始点之间的间隙(第四和第五个参数)。这这种状况下,矩形元是4*4的,每个矩形元与它的周围矩形元是紧密贴合的。咱们须要提供一个应用到矩形元的变换参数(第三个参数);在这种状况下,咱们不须要变换作什么工做,因此咱们应用了一个恒等变换。咱们应用了一个瓷砖规则(第六个参数)。咱们须要声明的是颜色模板不是漏印(stencil)模板,因此参数值为true。而且咱们须要提供一个指向回调函数的指针,回调函数的工做是向矩形元绘制模板。第八个参数是一个指向CGPatternCallbacks结构体的指针。这个结构体由数字0和两个指向函数的指针构成。第一个函数指针指向的函数当模板被绘制到矩形元中被调用,第二个函数指针指向的函数当模板被释放后调用。第二个函数指针咱们没有指定,它的存在主要是为了内存管理的须要。但在这个简单的例子中,咱们并不须要。

  在你使用颜色模板调用CGContextSetFillPattern函数以前,你须要设置将应用到模板颜色空间的上下文填充颜色空间。若是你忽略这项工做,那么当你调用CGContextSetFillPattern函数时会发生错误。因此咱们建立了颜色空间,设置它做为上下文的填充颜色空间,并在后面作了释放。

  到这里咱们仍然没有完成绘图。由于我尚未编写向矩形元中绘图的函数!绘图函数地址被表示为&drawStripes。绘图代码以下所示:

void drawStripes (void *info, CGContextRef con) {

// assume 4 x 4 cell

CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);

CGContextFillRect(con, CGRectMake(0,0,4,4));

CGContextSetFillColorWithColor(con, [[UIColor blueColor] CGColor]);

CGContextFillRect(con, CGRectMake(0,0,4,2));

}

 

图9 模板填充 

  如你所见,实际的模板绘图代码是很是简单的。惟一的复杂点在于CGPatternCreate函数必须与模板绘图函数的矩形元尺寸相同。咱们知道矩形元的尺寸为4*4,因此咱们用红色填充它,并接着填充它的下半部分为绿色。当这些矩形元被水平垂直平铺时,咱们获得了如图8所示的条纹图案。

  注意,最后图形上下文遗留下了一个不可取的状态,即填充颜色空间被设置为了一个模板颜色空间。若是稍后尝试设置填充颜色为常规颜色,就会引发错误。一般的解决方案是,使用CGContextSaveGStateCGContextRestoreGState函数将代码包起来。

  你可能观察到图8的平铺效果并不与箭头的三角形内部相符合:最底部的彷佛只平铺了一半蓝色。这是由于一个模板的定位并不关心你填充(描边)的形状,总的来讲它只关心图形上下文。咱们能够调用CGContextSetPatternPhase函数改变模板的定位。

  图形上下文变换

  就像UIView能够实现变换,一样图形上下文也具有这项功能。然而对图形上下文应用一个变换操做不会对已在图形上下文上的绘图产生什么影响,它只会影响到在上下文变换以后被绘制的图形,并改变被映射到图形上下文区域的坐标方式。一个图形上下文变换被称为CTM,意为“当前变换矩阵“(current transformation matrix)。

  彻底利用图形上下文的CTM来免于即便是简单的计算操做是很常见的。你可使用CGContextConcatCTM函数将当前变换乘上任何CGAffineTransform,还有一些便利函数可对当前变换应用平移、缩放,旋转变换。 

  当你得到上下文的时候,对图形上下文的基本变换已经设置好了;这就是系统能映射上下文绘图坐标到屏幕坐标的缘由。不管你对当前变换应用了什么变换,基本变换变换依然有效而且绘图继续工做。经过将你的变换代码封装到CGContextSaveGState和CGContextRestoreGState函数调用中,对基本变换应用的变换操做能够被还原。 

  举个例子,对于咱们迄今为止使用代码绘制的向上箭头来讲,已知的放置箭头的方式仅仅只有一个位置:箭头矩形框的左上角被硬编码在坐标{80,0}。这样代码很难理解、灵活性差、且很难被重用。最明智的作法是经过将全部代码中的x坐标值减去80,让箭头矩形框左上角在坐标{0,0}。事先应用一个简单的平移变换,很容易将箭头画在任何位置。为了映射坐标到箭头的左上角,咱们使用下面代码:

CGContextTranslateCTM(con, 80, 0); //在坐标{0,0}处绘制箭头

       旋转变换特别的有用,它可让你在一个被旋转的方向上进行绘制而无需使用任何复杂的三角函数。然而这略有点复杂,由于旋转变换围绕的点是原点坐标。这几乎不是你所想要的,因此你先是应用了一个平移变换,为的是映射原点到你真正想绕其旋转的点。可是接着,在旋转以后,为了算出你在哪里绘图,你可能须要作一次逆向平移变换。

  为了说明这个作法,我将绕箭头杆子尾部旋转多个角度重复绘制箭头,并把对箭头的绘图封装为UIImage对象。接着咱们简单重复绘制UIImage对象。

  具体代码以下:

- (void)drawRect:(CGRect)rect { 

UIGraphicsBeginImageContextWithOptions(CGSizeMake(40,100), NO, 0.0);

CGContextRef con = UIGraphicsGetCurrentContext();

CGContextSaveGState(con);

CGContextMoveToPoint(con, 90 - 80, 100);

CGContextAddLineToPoint(con, 100 - 80, 90);

CGContextAddLineToPoint(con, 110 - 80, 100);

CGContextMoveToPoint(con, 110 - 80, 100);

CGContextAddLineToPoint(con, 100 - 80, 90);

CGContextAddLineToPoint(con, 90 - 80, 100);

CGContextClosePath(con);

CGContextAddRect(con, CGContextGetClipBoundingBox(con));

CGContextEOClip(con);

CGContextMoveToPoint(con, 100 - 80, 100);

CGContextAddLineToPoint(con, 100 - 80, 19);

CGContextSetLineWidth(con, 20);

CGContextReplacePathWithStrokedPath(con);

CGContextClip(con);

CGFloat locs[3] = { 0.0, 0.5, 1.0 };

CGFloat colors[12] = {

0.3,0.3,0.3,0.8,

0.0,0.0,0.0,1.0,

0.3,0.3,0.3,0.8

};

CGColorSpaceRef sp = CGColorSpaceCreateDeviceGray();

CGGradientRef grad = CGGradientCreateWithColorComponents (sp, colors, locs, 3);

CGContextDrawLinearGradient (con, grad, CGPointMake(89 - 80,0), CGPointMake(111 - 80,0), 0);

CGColorSpaceRelease(sp);

CGGradientRelease(grad);

CGContextRestoreGState(con);

CGColorSpaceRef sp2 = CGColorSpaceCreatePattern(NULL);

CGContextSetFillColorSpace (con, sp2);

CGColorSpaceRelease (sp2);

CGPatternCallbacks callback = {0, &drawStripes, NULL };

CGAffineTransform tr = CGAffineTransformIdentity;

CGPatternRef patt = CGPatternCreate(NULL,CGRectMake(0,0,4,4),tr,4,4,kCGPatternTilingConstantSpacingMinimalDistortion,true, &callback);

CGFloat alph = 1.0;

CGContextSetFillPattern(con, patt, &alph);

CGPatternRelease(patt);

CGContextMoveToPoint(con, 80 - 80, 25);

CGContextAddLineToPoint(con, 100 - 80, 0);

CGContextAddLineToPoint(con, 120 - 80, 25);

CGContextFillPath(con);

UIImage* im = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

con = UIGraphicsGetCurrentContext();

[im drawAtPoint:CGPointMake(0,0)];

for (int i=0; i<3; i++) {

CGContextTranslateCTM(con, 20, 100);

CGContextRotateCTM(con, 30 * M_PI/180.0);

CGContextTranslateCTM(con, -20, -100);

[im drawAtPoint:CGPointMake(0,0)];

}

}

 

图10 使用CTM旋转变换

  变换有多个方法解决咱们早期使用CGContextDrawImage函数遇到的倒置问题。相对于逆向绘图,咱们选择逆向咱们绘图的上下文。实质上,咱们对上下文坐标系统应用了一个“倒置”变换。你自上而下移动上下文,接着你经过应用一个让y坐标乘以-1的缩放变换逆向y坐标的方向。

CGContextTranslateCTM(con, 0, theHeight);

CGContextScaleCTM(con, 1.0, -1.0);

     上下文的顶部应该被你往下移动多远依赖于你绘制的图片。好比说咱们能够绘制没有倒置问题的两个半边的火星图形(前面讨论的一个例子)。

CGContextTranslateCTM(con, 0, sz.height); // sz为[mars size]

CGContextScaleCTM(con, 1.0, -1.0);

CGContextDrawImage(con, CGRectMake(0, 0, sz.width/2.0, sz.height), marsLeft);

CGContextDrawImage(con, CGRectMake(b.size.width-sz.width/2.0, 0, sz.width/2.0, sz.height),marsRight);

  阴影

  为了在绘图上加入阴影,可在绘图以前设置上下文的阴影值。阴影的位置表示为CGSize,若是CGSize的两个值都是正数,则表示阴影是朝下和朝右的。模糊度被表示为任何一个正数。苹果没有解释缩放的工做方式,但实验代表12是最佳的模糊度,99及以上的模糊度会让阴影变得不成形。

    我在图9的基础上给上下文加了一个阴影:

~~~~~~~~~~~~

con = UIGraphicsGetCurrentContext();

CGContextSetShadow(con, CGSizeMake(7, 7), 12);

[im drawAtPoint:CGPointMake(0,0)];

~~~~~~~~~~~~~~~

  然而,使用这种方法有一个不太明显的问题。咱们是在每绘制一个箭头的时候加上的阴影。所以,箭头的阴影会投射在另外一个箭头上面。咱们想要的是让全部的箭头集体地投射出一个阴影。解决方法是使用一个透明的图层;该图层相似一个先是叠加全部绘图而后加上阴影的一个子上下文。代码以下:

con = UIGraphicsGetCurrentContext();

CGContextSetShadow(con, CGSizeMake(7, 7), 12);

CGContextBeginTransparencyLayer(con, NULL);

[im drawAtPoint:CGPointMake(0,0)];

for (int i=0; i<3; i++) {

CGContextTranslateCTM(con, 20, 100);

CGContextRotateCTM(con, 30 * M_PI/180.0);

CGContextTranslateCTM(con, -20, -100);

[im drawAtPoint:CGPointMake(0,0)];

}

// 在调用了CGContextEndTransparencyLayer函数以后,

// 图层内容会在应用全局alpha和上下文阴影状态以后被合成到上下文中

CGContextEndTransparencyLayer(con);

  

图11 阴影效果

  点与像素

  一个点是由xy坐标描述的一个无穷小量的位置。经过指定点实如今图形上下文中的绘图。咱们并无关心设备的分辨率,由于Core Graphics已经精细地将绘图映射到物理输出设备(基于CTM、反锯齿和平滑技术)。所以,文章以前的讨论只关心图形上下文的点,不关注点与屏幕像素的关系。

  然而像素是真实存在的。一个像素是真实世界中一个具备完整物理尺寸的显示单元。整数的点实际上介于像素之间。在单分辨率设备上,这可能会让人感到迷惑。比方说,若是使用线宽为1的线条对一个整数坐标的垂直路径描边,那么线条将会被分为两半,分别落在路径的两侧。因此在单分辨率设备上线宽会变成2px(由于设备没法表示半个像素)。

 

图12 整数的点坐标与偏移0.5点的坐标对应的描边处理

       当你遇到显示效果不佳的时,可能会被建议经过对坐标增减0.5让它在像素中居中。这个建议可能有效,如图11。但它只是作了一些头脑简单的假设。一个复杂的作法是得到UIView的contentScaleFactor属性。这个值为1.0或2.0,因此你能够除以这个属性值获得从像素到点的转换。还能够想一想用最精确的方式绘制一条水平或垂直的线条的方式不是描边路径,而是填充路径。使用这种方法UIView的子类代码将能够在任何设备上绘制一条完美的1px宽的垂线,代码以下:

CGContextFillRect(con, CGRectMake(100,0,1.0/self.contentScaleFactor,100));

  内容模式

  一个视图向它自身绘图,相对于只有背景颜色和子视图,它还有内容。这意味着每当视图被调整大小它的contentMode属性就变得很是重要。正如我以前提到的,绘图系统会尽量避免重头开始绘制视图。相反,绘图系统将使用以前绘图操做的缓存结果(位图回填)。因此,若是视图被从新调整大小,系统可能简单的伸缩或重定位缓存绘图,前提是你的contentMode设置指令是是这样设置的。

  说明这一点略有点复杂。由于我须要安排调整视图大小而不引发重绘操做(调用drawRect:方法)。当程序启动时,我将建立一个MyView实例,并将它放在window上。接着将执行调整MyView尺寸的操做延迟到window出现和界面初次显示以后:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

  self.window.rootViewController = [UIViewController new];

  MyView* mv =[[MyView alloc] initWithFrame:CGRectMake(0, 0, self.window.bounds.size.width - 50, 150)];

  mv.center = self.window.center;

  [self.window.rootViewController.view addSubview: mv];

  mv.opaque = NO;

  mv.tag = 111; // so I can get a reference to this view later

  [self performSelector:@selector(resize:) withObject:nil afterDelay:0.1];

  self.window.backgroundColor = [UIColor whiteColor];

  [self.window makeKeyAndVisible];

  return YES;

}

  咱们将视图的高度调成以前的2倍。没有触发drawRect:方法的调用。若是咱们视图的drawRect:方法代码和生成图9的代码相同,则咱们获得如图12的结果,视图被显示在正确高度上。

 

图13 内容自动伸展

  但是迟早drawRect:方法会被调用,绘图将按照drawRect:方法中的代码被刷新。代码不会将箭头绘制在相对于视图边界的高度。它是在一个固定的高度。所以箭头会伸展,并且会在之后某个时间返回到原始的尺寸。

  一般咱们的视图的contentMode属性须要与视图绘制本身的方式一致。假设咱们的drawRect:方法中的代码让箭头的尺寸和位置相对于视图的边界原点,即它的左上方。因此咱们能够设置它的contentModeUIViewContentModeTopLeft。又或者,咱们能够将contentMode设置为UIVIewContentModeRedraw,这将引发缓存内容的自动缩放和重定位被关闭,最终结果是视图的setNeedsDisplay方法将被调用,触发drawRect:方法重绘视图内容。

  在另外一方面,若是一个视图只是暂时被调整大小。假设是做为动画的一部分,那么伸缩行为正是你所想要的。假设咱们的动画是想要让视图变大而后还原回原始大小以达到做为吸引用户的一种手段。这就须要视图伸缩的时候视图的内容也跟着伸缩,正确的contentMode的值是UIViewContentModeScaleToFill,被伸缩的内容仅仅是视图内容的一副缓存图片,因此它运行起来十分的高效。 

  完。

  本文由海水的味道翻译,转载请注明译者和出处,请勿用于商业用途!

    译者说明:译文中的错误或不当之处望不吝指出。

  Drop me a line: xdreamarshal@gmail.com, http://weibo.com/xdream86 

相关文章
相关标签/搜索