Frameworks
iOS 绘图基于两个库:
1. UIKit
2. Quartz 2D
UIKit我们很熟悉,常用的控件UIView,UIButton以及各种ViewController,均来自于UIKit库。
Quartz 2D则是基于C语音的绘图库,我们应该听说过,但是真正应用的并不多。Quartz 2D的底层实现是基于Core Graphics的。因此Quartz的一些数据类型,均以‘CG’开头,如CGRect,CGColorRef等。
Quartz不仅仅提供了绘图功能,同时,其还提供了动画,图片编辑,PDF文件创建、编辑等功能。(在Quartz的内部图像模型,其实与PDF文件模型的定义很类似)
Drawing Context
无论我们使用UIKit还是Quartz库,所有的操作均是作用于Context上的。
Context的作用类似于油画中的画布,我们可以设置画布的属性,Pop/Push画布,以及获取当前画布等。
When to Draw?
话说回来,UIKit库为我们封装了大量的绘图控件,如UIButton,我们可以仅用几行代码就创建出一个能够响应事件的图片文字按钮。那么,什么时候需要我们大费周章的去draw UI呢?
如果符合下面4种情况之一,你可以考虑去draw:
- Creating custom views
- building images
- create PDFs
- building with Core Graphics
Creating Custom Views
一个UIView就像一个空白的画布,当我们需要自定义UIView的内容时,可以重写
- (void)drawRect:(CGRect)rect
方法。
与之匹配的,专门用于绘制打印内容的方法:
- (void)drawRect:(CGRect)rect forViewPrintFormatter:(UIViewPrintFormatter *)formatter
Building Images
利用UIKit,你可以在UIKit image context中,生成一个UIImage或者对其进行修改。
虽然生成(或修改)UIImage会使用运算能力,但是能够使我们的APP体积更小,显示更为灵活。
Creating PDFs
类似于Image,你可以在UIKit PDF context中,生成,编辑一个PDF文件。
Build with Core Graphics
当UIKit的功能不能够满足我们时,就需要转向Quartz了。我们可以在Core Graphic 的context中进行一些基于Core Graphics操作。
Contexts
通过上面我们已经知道,iOS中要进行任何的绘图操作,均要基于Context。
Context记录了当前绘画的所有信息——画布是否翻转,绘图的颜色,亮度等。
在iOS中,我们大致会使用如下几种Context:
- bitmap contexts
- PDF contexts
- image contexts
Bitmap Contexts
Bitmap contexts 实际上是一个矩形data数组,数组的大小取决于图片的颜色类型。如普通的图片,一个像素可能会占用4bytes,分别表示RGB值以及透明度。而不透明图片可能仅有RGB值,而忽略透明度以节省空间。对于黑白图片,则仅会用1到2个bytes。
在UIView的方法
- (void)drawRect:(CGRect)rect
中,系统会自动创建一个Context,来接受绘图。利用UIGraphicsGetCurrentContext()查看Context,实际上其类型就是Bitmap Context
PDF Contexts
PDF contexts 和 Bitmap context的操作是一模一样的。但是还是有些许的差别,PDF contexts的数据信息包括矢量数据,这可以使PDF文件在不同的解析度下不会发生形变。同时PDF context还是有page的概念,一个PDF文件包括若干Pages。
Core Image Contexts
Core Image Contexts 是用来讲Core Image 对象转换为Quartz 2D和OpenGL。它会利用GPU加速,Core Image的对象类型为CIColor何CIImage.
在UIKit中建立Contexts
在UIKit中创建一个Contexts很简单,仅需执行如下代码
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
NSLog(@"IN Context is %@", UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
为了深入理解这一对函数,我们做如下实验:
NSLog(@"OUT 1 Context is %@", UIGraphicsGetCurrentContext());
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
NSLog(@"IN Context is %@", UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
NSLog(@"OUT 2 Context is %@", UIGraphicsGetCurrentContext());
输出为:
可见,UIKit创建的Contexts生命周期仅存在于BeginContext于EndContext之间。
device scale
UIKit创建Image Contexts还有另一个更详细的接口,
UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
这里可以指定更详细的Contexts。
Creating PDF Context
当需要创建PDF文件时,我们可以创建一个PDF context,其类似Bitmap context:
UIGraphicsBeginPDFContextToFile(NSString * _Nonnull path, CGRect bounds, NSDictionary * _Nullable documentInfo);
UIGraphicsBeginPDFPage();
// Draw PDF here
UIGraphicsEndPDFContext();
创建一个PDF文件,你必须将它draw到一个文件或Data对象中。因此,BeginPDFContext有两个版本:
UIGraphicsBeginPDFContextToFile(NSString * _Nonnull path, CGRect bounds, NSDictionary * _Nullable documentInfo);
UIGraphicsBeginPDFContextToData(NSMutableData * _Nonnull data, CGRect bounds, NSDictionary * _Nullable documentInfo)
同时,我们需要创建PDF的‘页’,用方法:
UIGraphicsBeginPDFPage();
这里,我们不需要endPDFPage。
在Quartz中创建Contexts
在Quartz中创建Contexts,相较于UIKit,更为复杂。主要体现在:
- 时刻需要注意手动Release Quartz创建的C类型对象
Quartz的C语言接口更为复杂
在Quartz中创建Context:
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
NSLog(@"Error allocating color space");
return nil;
}
#define BITS_PER_COMPONENT 8
#define ARGB_COUNT 4
// Create bitmap context
CGContextRef context = CGBitmapContextCreate(NULL, width, height,
BITS_PER_COMPONENT,
width * ARGB_COUNT,
colorSpace,
(CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
if (context == NULL) {
// 注意这里要释放之前创建的CG对象
CGColorSpaceRelease(colorSpace);
return nil;
}
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:imageRef];
// 释放CG对象
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);
Drawing into Contexts
大部分的Quartz方法都需要一个Context作为参数。
获取Quartz Context有两个方法:
- 如上面所示,用CGCreateContext方法创建一个Context
有UIKit的Context转换为Quartz Context
下面是一个使用Quartz Context的例子
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
NSLog(@"Error allocating color space");
return;
}
#define BITS_PER_COMPONENT 8
#define ARGB_COUNT 4
// Create bitmap context
CGContextRef context = CGBitmapContextCreate(NULL, 100, 200,
BITS_PER_COMPONENT,
100 * ARGB_COUNT,
colorSpace,
(CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
if (context == NULL) {
CGColorSpaceRelease(colorSpace);
return;
}
CGContextSetLineWidth(context, 4);
CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
CGContextStrokeEllipseInRect(context, CGRectMake(0, 0, 100, 200));
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);
将UIKit Context转换为Quartz Context
相较于Quartz,我们运用UIKit能够更简便地运用UIKit所创建的Context。
而我们可以使用函数
CGContextRef __nullable UIGraphicsGetCurrentContext(void)
将UIKit context转换为Quartz contest。
例如对于上面一段画椭圆的代码,我们可以用UIKit简写为:
UIGraphicsBeginImageContextWithOptions(CGSize size,
BOOL opaque,
CGFloat scale);
// Retrieve the current context
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 4);
CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
CGContextStrokeEllipseInRect(context, CGRectMake(0, 0, 100, 200));
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:imageRef];
UIGraphicsEndImageContext();
Push/Pop Context
我们在使用UIKit Context时,会成对的使用
UIGraphicsBegin…Context
// operation with Context
UIGraphicsEnd…Context
其中会创建Context,而创建的Context的生命周期会在EndContext结束。
但是,我们可以通过Push/Pop Context的方式,来保留Context,使其在End…Context之外,仍能够使用。
如下代码:
NSLog(@"Context0 is %@", UIGraphicsGetCurrentContext());
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100),
YES,
0);
// Retrieve the current context
CGContextRef context = UIGraphicsGetCurrentContext();
NSLog(@"Context1 is %@", UIGraphicsGetCurrentContext());
UIGraphicsPushContext(context);
NSLog(@"Context2 is %@", UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
NSLog(@"Context3 is %@", UIGraphicsGetCurrentContext());
UIGraphicsPopContext();
NSLog(@"Context4 is %@", UIGraphicsGetCurrentContext());
UIGraphicsPopContext();
NSLog(@"Context5 is %@", UIGraphicsGetCurrentContext());
输出为:
PS:
在UIView的方法drawRect之前,系统会自动为我们Push一个context 到 UIKit的Context Stack中。因此,在drawRect方法中,我们不需要显示的创建Context。
UIKit Current Context
基本上说有的Quartz 函数都接受context参数,而UIKit版本的方法却不用。因为UIKit会自动维护一个graphics Stack,UIKit的方法会自动使用处于stack top的Context。
如果要混合使用Core Graphics与UIKit,则需按照如下流程:
- Create a Core Graphics context
- Push the context with UIKit method UIGraphicsPushContext()
- Use UIKit method
- Use Core Craphics method with context parameters
- Pop the context with UIGraphicsPopContext()
- Release the cotontext’s momery
UIKit and Quartz Colors
UIKit的对象是支持ARC的,而Quartz对象则是C语言形式,需要人工的retain 与 release。基本每一个UIKit对象都有对应的Quartz对象。
通常,UIKit与Quartz对象的转换是toll free的,仅需要关键字__bridge。
而有些对象,是不可以直接用__bridge的。幸运的是,我们可以通过属性来获取对应的对象。如UIKit与Quartz中Colors对象的转换,则应该是
UIColor *red = [UIColor redColor];
CGColorRef redRef = red.CGColor;
这里要注意redRef对象的保持,不要让red由于ARC自动释放掉。
The Painter’s Model
iOS默认用一种被称为‘Painter’s Model’来绘制内容。形象的说,就像是画油画一样,后绘制的内容,会覆盖在之前绘制的内容之上。
如图,先绘制了紫色的圆形,在绘制了绿色圆形,绿色圆形会覆盖在紫色圆形之上。
Context State
我们在绘制图形时,可以指定图形的边框颜色与填充色等信息。
[greenColor setFill];
[purpleColor setStroke];
[bunnyPath fill];
[bunnyPath stroke];
我们将这些绘图的状态,称为graphic state。
Applying State
每一个Context均有一个graphic state的stack。当我们创建新的context的时候,其graphic state stack是新的。
我们可以通过Push/Pop来操作graphic state stack(GState)。
GState stack与UIKit所管理的Context stack有所不同。
UIKit管理的context stack是针对多个context之间切换而言的。而GState stack则是对于某一个context来说的。
我们可以使用
CGContextSaveGState();
CGContextRestoreGState();
来push,pop所存储的GState。
Context的state包括如下内容:
Context 坐标系统
在UIKit中,坐标原点是以左上角开始的,而在Quartz中,则是以左下角开始的。
坐标系统取决于Context是如何被创建的,如果由UIKit函数创建,则符合UIKit坐标系,反之,符合Quartz坐标系。
我们可以通过垂直翻转Quartz context的坐标系,使得Quartz的Context 坐标系和UIKit的Context坐标系相同。代码如下:
- (void)FlipContextVertically:(CGSize) size{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) {
NSLog(@"Error:NO context to flip");
return;
}
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1.0f, -1.0f);
transform = CGAffineTransformTranslate(transform, 1.0f, -size.height);
CGContextConcatCTM(context, transform);
}