与直接在UIView控件上绘图不同,在内存中绘图时,需要开发者自己准备绘图环境,Quartz 2D提供了一个非常便捷的函数:UIGraphicsBeginImageContext(CGSize size),该函数用于准备绘图环境。当图形绘制完成后,可调用UIGraphicsEndImageContext()函数结束绘图和关闭绘图环境。

总结来说,在内存中绘图的步骤如下。

调用UIGraphicsBeginImageContext(CGSize size)函数准备绘图环境。

调用UIGraphicsGetCurrentContext()函数获取绘图CGContextRef。

用前面介绍的绘制集合图形、使用路径等方式进行绘图。

调用UIGraphicsGetImageFromCurrentImageContext()函数获取当前绘制的图形,该方法返回一个UIImage对象。

调用UIGraphicsEndImageContext()函数结束绘图,并关闭绘图环境。

除了使用前面介绍需要CGContextRef参数的方法绘图之外,还可调用如下函数进行绘图。

Ø UIRectFill(CGRect rect):向当前绘图环境所创建的内存中的图片上填充一个矩形。

Ø UIRectFillUsingBlendMode(CGRect rect , CGBlendMode blendMode):向当前绘图环境所创建的内存中的图片上填充一个矩形,绘制使用指定的混合模式。

Ø UIRectFrame(CGRect rect):向当前绘图环境所创建的内存中的图片上绘制一个矩形边框。

Ø UIRectFrameUsingBlendMode(CGRect rect , CGBlendMode blendMode):向当前绘图环境所创建的内存中的图片上绘制一个矩形边框,绘制使用指定的混合模式。

上面4个方法都是直接绘制在当前绘图环境所创建的内存中的图片上,因此,这些方法都不需要传入CGContextRef作为参数。

下面的程序示范了在内存中绘图,并将图片输出到手机本地。首先创建一个Single View Application,该Application包含一个应用程序委托代理类、一个视图控制器和配套的Storyboard界面设计文件。本程序无须修改界面设计文件,也无须自定义UIView控件,该程序只是向该UIView上添加一个UIImageView,并控制该UIImageView显示内存中绘制的图片即可。因此,该程序只要修改视图控制器类,该视图控制器类的实现代码如下。

程序清单:codes/12/12.2/DrawImage/DrawImage/FKViewController.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
@implementation FKViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    UIImageView* iv = [[UIImageView alloc]
        initWithImage: [self drawImage:self.view.frame.size]];
    [self.view addSubview:iv];
}
- (UIImage*) drawImage:(CGSize) size
{  
    UIGraphicsBeginImageContext(size);  // 创建内存中的图片
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(ctx, 8);  // 设置线宽
    // ---------下面开始向内存中绘制图形---------  
    CGContextSetRGBStrokeColor(ctx, 0 , 1, 0 , 1);  // 设置线条颜色
    CGContextStrokeRect(ctx , CGRectMake(30 , 30 , 120 , 60));  // 绘制一个矩形边框
    CGContextSetRGBFillColor(ctx, 1, 1, 0 , 1);  // 设置填充颜色
    CGContextFillRect(ctx , CGRectMake(180 , 30 , 120 , 60));  // 绘制一个矩形边框
    CGContextSetRGBStrokeColor(ctx, 0, 1 , 1 , 1);  // 设置线条颜色
    // 绘制一个椭圆
    CGContextStrokeEllipseInRect(ctx , CGRectMake(30 , 120 , 120 , 60));
    CGContextSetRGBFillColor(ctx, 1, 0 , 1 , 1);  // 设置填充颜色
    CGContextFillEllipseInRect(ctx , CGRectMake(180 , 120 , 120 , 60));  // 填充一个椭圆
    // 获取该绘图Context中的图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    // ---------结束绘图---------
    UIGraphicsEndImageContext();
    // 获取当前应用路径中Documents目录下的指定文件名对应的文件路径
    NSString *path = [[NSHomeDirectory()
        stringByAppendingPathComponent:@"Documents"]
        stringByAppendingPathComponent:@"newPng.png"];
    // 保存PNG图片
    [UIImagePNGRepresentation(newImage) writeToFile:path atomically:YES];
    return newImage;
}
@end

程序中的第一行粗体字代码用于创建内存中图片的绘制环境,接着调用UIGraphics- GetCurrentContext()方法获取绘图的CGContextRef,剩下的绘图操作与前面介绍的各种绘图代码完全相同。

图形绘制完成后,第二行粗体字代码调用UIGraphicsEndImageContext()方法结束绘图,绘图结束后,即可调用UIGraphicsGetImageFromCurrentImageContext()函数获取当前绘制的图片,程序既可使用UIImageView显示该图片(如程序的viewDidLoad方法所示),也可将图片输出到手机本地。

最后一行粗体字代码调用了UIImagePNGRepresentation()函数获取UIImage图像的数据。该方法返回NSData对象,接下来就可利用NSData的方法执行输出了。

提示:

UIImagePNGRepresentation()函数的作用是获取UIImage图片转换为PNG格式的图片数据,还有一个与之类似的UIImageJPEGRepresentation()函数,其作用是获取UIImage图片转换为JPEG格式的图片数据,UIImageJPEGRepresentation()函数多一个参数,用于指定图片的压缩质量。

编译、运行该程序,即可看到如图12.13所示的效果。

该程序还将绘制的图片输出到该应用程序沙盒的Documents文件夹下。对模拟器而言,该Documents文件夹位于OS X系统的一个隐藏文件夹下。为了查看iOS应用程序沙盒的文件夹,请按如下步骤进行。

提示:

为了保证系统安全,iOS应用程序只能在系统为该应用所分配的文件区域下读、写文件,该文件区域被称为该应用程序的沙盒。iOS应用的所有非代码文件都要保存在此,例如,图像、图标、声音、映像、属性列表、文本文件等。iOS的每个应用程序都有自己独立的沙盒,该应用程序不能访问其他应用的沙盒。

(1)打开Mac OS X系统的Finder。

(2)单击Finder应用中主菜单的“前往”→“前往文件夹”菜单项,或单击command+Shift+G快捷键,系统弹出如图12.14所示的对话框。

(3)在图12.14所示的对话框中输入/users/登录用户名/library/application suport/iPhone Simulator,然后单击“前往”按钮进入该文件夹,即可看到5.0、5.1、6.0、6.1、7.0等文件夹,这就是该电脑上已经安装过的iOS模拟器文件夹——不同的文件夹对应不同版本的iOS模拟器。

(4)进入当前iOS模拟器版本对应的文件夹,比如当前使用iOS 7.0模拟器,则经过7.0文件夹进入该模拟器,接下来即可看到该模拟器目录下包括Applications、Media、Root、tmp、资源库等文件夹,其中,Applications文件夹下保存了该模拟器上安装的所有iOS应用。

(5)进入Applications文件夹,即可在该文件夹下看到大量的子文件夹,每个子文件夹对应一个应用程序,找到本应用对应的子文件夹,并进入该子文件夹,即可看到包含Documents、DrawImage.app、Library、tmp内容,在Documents文件夹下即可看到刚刚创建的newPng.png图片,如图12.15所示。

图12.15  查看模拟器沙盒下的文件

提示:

读者可能会想,如果可以直接查看OS X系统的隐藏文件,是不是就可以直接通过Finder进入该模拟器沙盒目录?实际上,OS X并没有提供图形化界面来设置显示隐藏文件,不过可以通过命令行进行修改,在OS X系统命令行窗口输入defaults write com.apple.finder AppleShowAllFiles -bool true,然后退出所有的Finder,重启Finder程序,即可看到隐藏文件;如果希望恢复隐藏,在命令行窗口输入defaults write com.apple.finder AppleShowAllFiles -bool false,然后退出所有的Finder,并重启Finder程序即可

†† 实例:绘图板

该实例将会实现一个绘图板,用户可以根据喜好随心所欲地在手机上“涂鸦”,涂鸦完成后,即可得到自己想要的图片——这张图片既可保存到手机本地,也可通过网络分享。

为了实现这个应用,仅仅通过重写UIView的drawRect:方法并不适合——如果仅通过重写UIView的drawRect:方法来实现绘图,用户每次绘图的时候就会丢失上一次绘图的内容。这显然不是本实例要实现的效果。

为了保证用户每次绘图的内容不会丢失,将会在内存中创建一张图片,当用户开始绘图时,程序会通过重写drawRect:方法进行实时绘制,当用户想要绘制的图形确定下来后,将该图形绘制到内存中的图片上。

举例来说,当用户想要在屏幕上绘制直线时,程序会把用户开始触碰屏幕的第一个点作为绘图的起始点,当用户手指不离开屏幕而是在屏幕上拖动时,程序会不断地获取拖动点的坐标,并调用该UIView的drawRect:方法,该方法会从起始点绘制到当前拖动点——由于drawRect:每次重绘都只绘制起始点到当前拖动点的直线,因此,用户可以实时看到拖动绘制的直线。当用户松开手指时(表明用户确定了最终绘制点),在内存中的图片上绘制从起始点到手指松开点的直线即可。

需要指出的是,为了保证用户能看到之前绘制的图形,重写UIView的drawRect:方法时一定要先把内存中的图片绘制到UIView上。

首先创建一个Single View Application,该Application包含一个应用程序委托代理类、一个视图控制器和配套的Storyboard界面设计文件。在Interface Builder中打开界面设计文件,将该界面设计文件中最大的UIView改为使用自定义的FKDrawView类。在界面上方放置一个UISegmentedControl控件,该控件用于控制绘图颜色;在界面下方放置一个工具条,并向工具条中添加一个UISegmentedControl控件,该控件用于控制绘图形状。本应用的界面设计如图12.16所示。

为了让界面上的两个UISegmentedControl控件能控制用户绘制的颜色和形状,需要在Interface Builder中为这两个UISegmentedControl控件分别绑定changeColor:和changeShape:两个IBAction方法。

为了能记录该应用当前需要绘制的图形,本程序先创建一个头文件,该文件中仅定义一个枚举类型,代码如下。

程序清单:codes/12/12.2/HandDraw/HandDraw/Constant.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef HandDraw_Constant_h
#define HandDraw_Constant_h
typedef enum
{
    kLineShape = 0,
    kRectShape,
    kEllipseShape,
    kRoundRectShape,
    kPenShape
} ShapeType;
#endif

本应用的视图控制器类比较简单,主要就是实现changeColor:和changeShape:两个IBAction方法。下面是该视图控制器类的实现代码。

程序清单:codes/12/12.2/HandDraw/HandDraw/FKViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@implementation FKViewController
NSArray* colors;
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    colors = [NSArray arrayWithObjects:
        [UIColor redColor],[UIColor greenColor],
        [UIColor blueColor],[UIColor yellowColor],
        [UIColor purpleColor],[UIColor cyanColor],
        [UIColor blackColor] , nil];
}
- (IBAction)changeColor:(UISegmentedControl*)sender {
    // 根据用户的选择来修改FKDrawView的当前颜色
    ((FKDrawView*)self.view).currentColor = [colors objectAtIndex:
        sender.selectedSegmentIndex];
}
- (IBAction)changeShape:(UISegmentedControl*)sender
{
    // 修改FKDrawView控件的shape属性
    ((FKDrawView*)self.view).shape = sender.selectedSegmentIndex;
}
@end

从上面程序中的两行粗体字代码可以看出,该视图控制器类的view并不是UIView,而是自定义的FKDrawView,这个FKDrawView就是实现本应用的关键,FKDrawView不仅要重写drawRect:方法,该方法还完成两件事情:将内存中的图片绘制出来;用户手指拖动进行“实时”绘制。

FKDrawView类的接口代码比较简单,只是定义currentColor、shape两个属性即可,这两个属性用于接收控制器传入的绘制颜色和绘制形状。FKDrawView类的实现代码如下。

程序清单:codes/12/12.2/HandDraw/HandDraw/FKDrawView.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
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
100
101
102
103
104
105
@implementation FKDrawView
CGPoint firstTouch, prevTouch, lastTouch;
// 定义向内存中的图片执行绘图的CGContextRef
CGContextRef buffCtx;
UIImage * image ;
- (id)initWithCoder:(NSCoder*)aCoder
{
    self = [super initWithCoder:aCoder];
    if (self) {
        self.currentColor = [UIColor redColor];  // 初始化时将当前颜色设为红色
        UIGraphicsBeginImageContext(self.bounds.size);  // 创建内存中的图片
        // 获取向内存中的图片执行绘图的CGContextRef
        buffCtx = UIGraphicsGetCurrentContext();
    }
    return self;
}
// 当用户手指开始触碰时激发该方法
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    firstTouch = [touch locationInView:self];  // 获取触碰点坐标
    // 如果当前正在进行自由绘制,prevTouch代表第一个触碰点
    if (self.shape == kPenShape)
    {
        prevTouch = firstTouch;
    }
}
// 当用户手指在控件上拖动时不断激发该方法
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    lastTouch = [touch locationInView:self];  // 获取触碰点坐标
    // 如果当前正在进行自由绘制
    if (self.shape == kPenShape)
    {
        [self draw:buffCtx];  // 向内存中的图片执行绘制
        // 取出内存中的图片,保存到image中
        image = UIGraphicsGetImageFromCurrentImageContext();
    }
    // 通知该控件重绘,此时会实时绘制起始点与用户手指拖动点之间的形状
    [self setNeedsDisplay];
}
// 当用户手指离开控件时激发该方法
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    lastTouch = [touch locationInView:self];  // 获取离开触碰的点坐标
    // 向内存中的图片执行绘制,即把最终确定的图形绘制到内存中的图片上
    [self draw:buffCtx];
    image = UIGraphicsGetImageFromCurrentImageContext();
    [self setNeedsDisplay];  // 通知重绘
}
- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();  // 获取绘图上下文
    [image drawAtPoint:CGPointZero];  // 将内存中的图片绘制出来
    [self draw:ctx];  // 调用draw:方法执行绘制
}
// 定义一个函数,用于根据firstTouch、lastTouch来确定矩形区域
- (CGRect) curRect
{
    return CGRectMake(firstTouch.x, firstTouch.y,
        lastTouch.x - firstTouch.x ,
        lastTouch.y - firstTouch.y);
}
- (void)draw:(CGContextRef)ctx
{
    // 设置线条颜色
    CGContextSetStrokeColorWithColor(ctx, self.currentColor.CGColor);
    CGContextSetFillColorWithColor(ctx, self.currentColor.CGColor);  // 设置填充颜色
    CGContextSetLineWidth(ctx, 2.0);  // 设置线宽
    CGContextSetShouldAntialias(ctx, YES);
    switch (self.shape) {
        CGFloat leftTopX , leftTopY;
        case kLineShape:
            // 添加从firstTouch到lastTouch的路径
            CGContextMoveToPoint(ctx, firstTouch.x, firstTouch.y);
            CGContextAddLineToPoint(ctx, lastTouch.x, lastTouch.y);
            CGContextStrokePath(ctx);  // 绘制路径
            break;
        case kRectShape:
            CGContextFillRect(ctx ,[self curRect]);  // 填充矩形
            break;
        case kEllipseShape:
            CGContextFillEllipseInRect(ctx ,[self curRect]);  // 填充椭圆
            break;
        case kRoundRectShape:
            // 计算左上角的坐标
            leftTopX = firstTouch.x < lastTouch.x ? firstTouch.x : lastTouch.x;
            leftTopY = firstTouch.y < lastTouch.y ? firstTouch.y : lastTouch.y;
            // 添加圆角矩形的路径
            CGContextAddRoundRect(ctx ,leftTopX ,leftTopY ,
                fabs(lastTouch.x - firstTouch.x), fabs(lastTouch.y - firstTouch.y), 16);
            CGContextFillPath(ctx);  // 填充路径
            break;
        case kPenShape:
            // 添加从prevTouch到lastTouch的路径
            CGContextMoveToPoint(ctx, prevTouch.x, prevTouch.y);
            CGContextAddLineToPoint(ctx, lastTouch.x, lastTouch.y);
            CGContextStrokePath(ctx);  // 绘制路径
            prevTouch = lastTouch;  // 使用prevTouch保存当前点
            break;
    }
}
@end

该程序的关键是上面的粗体字draw:方法,该方法会根据想要绘制的图形类型,绘制不同的形状。需要读者留意的是,该方法在两个地方被调用过——当用户拖动手指时,激发touchesMoved: withEvent:方法,该方法中会通知该控件重绘自己,该控件会调用drawRect:方法进行重绘,drawRect:方法中调用了这个粗体字draw:方法执行实时绘制。除此之外,当用户手指结束触碰时,激发touchesEnded: withEvent:方法,该方法中调用了[self draw:buffCtx];代码,这行代码将会把起始点到结束触碰点的形状绘制在内存中的图片上。

程序中还有一个稍微有点复杂的地方,就是所谓的“自由绘制”。即当用户手指在屏幕
上拖动时,程序需要绘制手指拖动的轨迹,这条轨迹表面上看是一条不规则的曲线,但实际上它由很多很短的线段组成——当用户手指在屏幕上拖动时,程序会不断
地从上一个触碰点绘制到当前触碰点,由于这些线段都非常短,因此,用户会以为我们绘制了一条光滑的曲线。程序为了完成这个绘制过程,使用prevPoint保存上一个触碰点的坐标,并不断地绘制从prevPoint到lastPoint的线段,每次绘制完成后,使用prevPoint保存当前触碰点的坐标,这对下一次绘制而言,当前触碰点就变成了上一个触碰点。

编译、运行该程序,用户即可在屏幕上绘制任意的形状,绘制效果如图12.17所示。

iOS之在内存中绘图的更多相关文章

  1. iOS图片加载到内存中占用内存情况

    我的测试结果: 图片占用内存   图片尺寸           .png文件大小 1MB              512*512          316KB 4MB              10 ...

  2. IOS性能调优系列:使用Zombies动态分析内存中的僵尸对象

    硬广:<IOS性能调优系列>第四篇,预计会有二十多篇,持续更新,欢迎关注. 前两篇<IOS性能调优系列:Analyze静态分析>.<IOS性能调优系列:使用Instrum ...

  3. 深入浅出-iOS Block原理和内存中位置

    Posted by 微博@Yangsc_o 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 #简介 今天回顾一下blcok,基本 ...

  4. iOS MRC ARC 内存管理

    转自:http://www.jianshu.com/p/48665652e4e4 1. 什么是内存管理 程序在运行的过程中通常通过以下行为,来增加程序的的内存占用 创建一个OC对象 定义一个变量 调用 ...

  5. 用NSData和NSFileManager保存内存中的对象

    曾经接触过iOS开发,并且开发过两个应用,纵然青涩,也算是一断美好的回忆.转眼就已经一年多了!现在回过头来决定再次拿起iOS开发. 下面讲NSData: NSdata的概念 1.使用文件时需要频繁地将 ...

  6. iOS已发布应用中对异常信息捕获和处理

    iOS已发布应用中对异常信息捕获和处理 iOS开发中我们会遇到程序抛出异常退出的情况,如果是在调试的过程中,异常的信息是一目了然,但是如果是在已经发布的程序中,获取异常的信息有时候是比较困难的. iO ...

  7. iOS夯实:内存管理

    iOS夯实:内存管理 文章转自 内存管理 最近的学习计划是将iOS的机制原理好好重新打磨学习一下,总结和加入自己的思考. 有不正确的地方,多多指正. 目录: 基本信息 旧时代的细节 新时代 基本信息 ...

  8. IOS开发之内存管理--dealloc该写些什么

    在非ARC开发环境中,dealloc是类释放前,清理内存的最后机会.到底那些变量和属性该释放呢,一些特殊的类(nstimer,observer)该怎么释放.需要注意的是不释放会引起内存泄露,过度释放也 ...

  9. (续)一个demo弄清楚位图在内存中的存储结构

    本来续---数字图像处理之位图在计算机中的存储结构一文,通过参考别人的代码,进行修改和测试终于成功运行. 该实例未使用任何API和相关类,相信如果对此实例能够完全理解那么将有进一步进行数字图像处理的能 ...

随机推荐

  1. 【HDOJ】1050 Moving Tables

    贪心问题,其实我觉得贪心就是合理的考虑最优情况,证明贪心可行即可.这题目没话多久一次ac.这道题需要注意房间号的奇偶性.1 3.2 4的测试数据.答案应该为20. #include <stdio ...

  2. BZOJ_1013_[JSOI2008]_球形空间产生器_(高斯消元)

    描述 http://www.lydsy.com/JudgeOnline/problem.php?id=1013 n维空间,给出球上n+1个点的n维坐标,求球心坐标. 提示:给出两个定义:1. 球心:到 ...

  3. angular.extend

    function f1() {} var f2 = angular.extend(f1, { active: false, toggle: function() { this.active = !th ...

  4. 【转】MFC中用CFile读取和写入文件2

    原文网址:http://blog.sina.com.cn/s/blog_623a7fa40100hh1u.html CFile提供了一些常用的操作函数,如表1-2所示. 表1-2  CFile操作函数 ...

  5. c# 模拟http post 带cookie

    下面的代码是自动向cnblogs中的小组发帖.........注意小组ID,主题ID,小组类型 首先采用firebug分析到发帖时的post地址以及参数,其中在headers中包含了cookies,把 ...

  6. acm位运算应用 搜索

    acm位运算应用 搜索 搜索    此处不讲题目,只讲位运算是怎样在这些题中实现和应用的.由于搜索题往往是基于对状态的操作,位运算往往特别有效,优化之后的效果可以有目共睹.    例1.POJ 132 ...

  7. 测试一个函数的运行时间(C++)

    #include <ctime> static clock_t Start,Finish; Start=clock(); fun(); Finish = clock(); double t ...

  8. Cocos2d-x获取随机数

    计算机是无法产生真正的随机数的,都是伪随机.获取随机数的方式和算法多种多样,这里只给出一种方法,基于最新的C++11. 1 2 3 4 5 #include <random> std::u ...

  9. 【原】CAVLC的个人理解

    4x4数据块经过预测.变换.量化后,非零系数主要集中在低频部分,而高频部分大部分是零.数据经过zig-zag扫描后,从左->右(低频->高频),DC系数附近的系数非常大,而高频的非零系数大 ...

  10. Java笔记(八)……数组

    数组的概念 同一种类型数据的集合.其实数组就是一个容器. 数组的好处 可以自动给数组中的元素从0开始编号,方便操作这些元素. 数组的格式 元素类型[] 数组名 = new 元素类型[个数]; int[ ...