美文网首页
iOS绘图教程(一)

iOS绘图教程(一)

作者: 默默_David | 来源:发表于2017-04-24 19:49 被阅读407次

    原文地址:http://www.cocoachina.com/industry/20140115/7703.html

    本文是《Programming iOS5》中Drawing一章的翻译,考虑到主题完整性,翻译版本中加入了一些书中未涉及到的内容。希望本文能够对你有所帮助。(本文由海水的味道翻译整理,转载请注明译者和出处,请勿用于商业用途!原文

    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函数关闭图形上下文。

    第二种方法是利用cocoa为你生成的图形上下文。当你子类化了一个UIView并实现了自己的drawRect:方法后,一旦drawRect:方法被调用,Cocoa就会为你创建一个图形上下文,此时你对图形上下文的所有绘图操作都会显示在UIView上。

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

    1.UIGraphicsBeginImageContextWithOptions函数不仅仅是创建了一个适用于图形操作的上下文,并且该上下文也属于当前上下文。

    2.当drawRect方法被调用时,UIView的绘图上下文属于当前图形上下文。

    3.回调方法所持有的context:参数并不会让任何上下文成为当前图形上下文。此参数仅仅是对一个图形上下文的引用罢了。

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

    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文件中声明如下代码:

    @interfaceMyLayerDelegate : 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:

    @interfaceMyView () {

    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();

    returnresult;

    }

    现在将之前的代码修改如下:

    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中,这样做有两大优点:

    1.当UIImage绘图时它会自动修复倒置问题

    2.当你从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对象时就会暴露出倒置问题。

    CIFilter与CIImage

    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。每一个滤镜拥有一小部分用来确定其行为的键值。如果你想修改某一个键(比如亮度键)对应的值,你可以调用setValue:forKey:方法或当你指定一个滤镜名时提供所有键值对。

    需要处理的图片必须是CIImage类型,调用initWithCGImage:方法可获得CIImage。因为CGImage又是作为滤镜的输出,因此滤镜之间可被连接在一起(将滤镜的输出作为initWithCGImage:方法的输入参数)

    当你构建一个滤镜链时,并没有做复杂的运算。只有当整个滤镜链需要输出一个CGImage时,密集型计算才会发生。调用contextWithOptions:和createCGImage:

    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同样是调用这些函数操纵上下文状态。

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

    CGContextSetLineWidth、CGContextSetLineDash

    线帽和线条联接点样式

    CGContextSetLineCap、CGContextSetLineJoin、CGContextSetMiterLimit

    线条颜色和线条模式

    CGContextSetRGBStrokeColor、CGContextSetGrayStrokeColor、CGContextSetStrokeColorWithColor、CGContextSetStrokePattern

    填充颜色和模式

    CGContextSetRGBFillColor,CGContextSetGrayFillColor,CGContextSetFillColorWithColor, CGContextSetFillPattern

    阴影

    CGContextSetShadow、CGContextSetShadowWithColor

    混合模式

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

    整体透明度

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

    文本属性

    CGContextSelectFont、CGContextSetFont、CGContextSetFontSize、CGContextSetTextDrawingMode、CGContextSetCharacterSpacing

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

    CGContextSetShouldAntialias、CGContextSetShouldSmoothFonts

    相关文章

      网友评论

          本文标题:iOS绘图教程(一)

          本文链接:https://www.haomeiwen.com/subject/usopzttx.html