iOS中能够用来绘图的框架一般是 UIKit , Quartz 2D(CoreGraphic), OpenGL, 三个框架的是从高阶framework -> 底层framework, 一般情况下, iOS中常用的是前两个.
UIKit(OC)/Quartz(C风格)两者代码风格不一样, 绘制同一个内切圆图, 两者代码如下:
// UIKit
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:inset cornerRadius:12];
[bezierPath stroke];
// Quartz 2D
CGContextFillEllipseInRect(context, rect);
使用的场景
自定义view
我们在自定义view的drawRect方法中可以绘制我们需要的view样式, 我们可以在该方法中使用UIKit或者Quartz中的方法来定制.
绘制image
app或者sdk中使用image资源时候,为了省时省力, 我们可能需要自己通过代码绘制一些简单的图,或者对已经存在的image资源进行小幅度的处理,然后使用.
例如有时候需要纯色资源图, 只需要几行代码就能够生成. 还有的时候需要改变图片的渲染颜色, 改变图片的大小以及获取镜像图片等等需求, 都能使用UIKit/Quartz绘制.
创建PDF
日常开发中使用比较少
使用CoreGraphic
UIKit中的绘图和处理图片的方法有限, 并且很多比较复杂的需求, 例如将图片灰度化等等.
Context
iOS的每次绘图操作都需要一个context, 这个context就比较像现实中的一张纸(Android中对应的canvase). 我们用context表示绘制以后的目的地, 里面保存着绘画中需要的所有的东西, 例如使用何种颜色绘制边框, 用什么颜色填充, 画布是否需要旋转等等.
我们在iOS绘画过程中一般需要接触两种context: 一种是bitmap context, 另外一种是PDF context. 其实还有一种CoreImage框架中的context 类型, 但是它用于图像处理而不是绘画.
bitmap context
BitmapContext是一个长方形的数组的内容, 其中数组是一个二维数组, 成员就是图形的每个像素,像素的size用来表示每个像素所代表的颜色类型所占用的空间. 一般而言, 有些图片使用3或4个bytes表示一个像素, 两者不同是是否有alpha通道(bitmap 的opaque).
一个 opaque 的bitmap会忽略图片的alpha透明属性,以此来优化存储大小. 通常透明度alpha,又表示该像素点的亮度信息.
我们通常用的灰度图, 使用1个或者2个bytes表示一个像素,其中有一个就是亮度.
PDF context
Core Image Contexts
如何在UIKit中获取Contexts
UIKit中获取image context方法非常简单, 值得注意的是,使用下面代码的方法,获取的图片的绘制比例是1:1, 即context的大小是就是像素大小:
UIGraphicsBeginImageContext(size);
// Perform drawing here
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
设备的Device scale
还有另外一个方法用来获取image context, 并且创建的图片是和不同屏幕分辨率相匹配的. Device scale表示了 逻辑空间(iOS中的point) 与 物理空间(像素pixel)之间的比例.
一个图片的scale在iOS开发中非常重要. @2x, @3x图的意义就在这里. 下面代码中的deviceScale可以填写成0.0 或者 [UIScreen mainScreen].scale, 系统会根据当前设备自动匹配, 强烈建议在开发中使用如下方法, 因为如果使用airplay等其他的方法,会很好的进行频幕分辨率的适配.
UIGraphicsBeginImageContextWithOptions(targetSize, isOpaque, deviceScale);
// Perform drawing here
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
更加底层的API方法创建context
CoreGraphic 提供了一些方法用于创建bitmap context:
// Create a color space
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
NSLog(@"Error allocating color space");
return nil;
}
// Create the bitmap context. (Note: in new versions of
// Xcode, you need to cast the alpha setting.)
CGContextRef context = CGBitmapContextCreate(
NULL, width, height,
BITS_PER_COMPONENT, // bits = 8 per component
width * ARGB_COUNT, // 4 bytes for ARGB colorSpace,
(CGBitmapInfo) kCGImageAlphaPremultipliedFirst);
if (context == NULL) {
NSLog(@"Error: Context not created!"); CGColorSpaceRelease(colorSpace ); return nil;
}
// Push the context.
// (This is optional. Read on for an explanation of this.)
// UIGraphicsPushContext(context);
// Perform drawing here
// Balance the context push if used.
// UIGraphicsPopContext();
// Convert to image
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:imageRef];
// Clean up CGColorSpaceRelease(colorSpace ); CGContextRelease(context);
CFRelease(imageRef);
在context中绘制
Quartz中有很多函数让你在context中绘制, 当然前提是你持有了一个CGContextRef对象:
// Set the line width
CGContextSetLineWidth(context, 4);
// Set the line color
CGContextSetStrokeColorWithColor(context,
[UIColor grayColor].CGColor);
// Draw an ellipse
CGContextStrokeEllipseInRect(context, rect);
在UIKit中生成context并绘制
UIKit中很容易创建生成bitmap context:
// 建立image context
UIGraphicBeginImageContextWithOptions(targetSize, isOpaque, 0.0);
// 获取当前的context
CGContextRef context = UIGraphicsGetCurrentContext();
//perform the drawing
CGContextSetLineWidth(context, 4);
CGContextSetStrokeColorWithColor(context, [UIColor grayColor]).cgColor;
CGContextStrokeEllipseInRect(context, rect);
//获取绘制的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 结束image context - 注意这里无需清理context这个局部变量, 因为不是我们create出来的, 在End方法里面系统会将image context对象内存回收
UIGraphicsEndImageContext();
注意, iOS系统会维护一个context栈,在UIGraphicBeginImageContextWithOptions
方法调用时候, 系统就会创建一个bitmap context, 并且将该context压入栈顶, 在使用UIGraphicsGetCurrentContext
时候,获取的栈顶的context, 如果我们调用UIGraphicsEndImageContext
,系统会将栈顶的context移除, 此时如果我们再次调用UIGraphicsGetCurrentContext
那么可能拿到一个nil对象.
当然我们也可以通过方法UIGraphicsPushContext(context)
手动将某一个context压入栈顶, 通过UIGraphicsPopContext()
将其出栈.这种手动的方法常常用于UIKit中的-drawRect:
方法, 因为这种可以切换当前的context, 让我可以同时在不同的地方进行绘制.
在自定义UIView中, 当系统调用-drawRect:
方法时, 系统会push一个context到栈顶,因此,在我们重写该方法时候, 我们可以直接调用UIGraphicsGetCurrentContext()
获取栈顶的context, 而不必担心栈顶没有context:
-(void)drawRect:(CGRect)rect{
// Perform drawing here
// If called, UIGraphicsGetCurrentContext()
// returns a valid context
}
UIKit中的当前 Context
前面我们注意到, CoreGraphic中的每个绘制相关方法都需要第一个参数就是context, 但是使用UIKit框架的绘制方法, 不需要我们传入context.
// Stroke an ellipse using a Bezier path
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:rect]; path.lineWidth = 4;
// UIKit的绘制方法 - 设置描边颜色时候, 无需传入context
[[UIColor grayColor] setStroke];
[path stroke];
那么在使用UIKit中的绘制方法时候, UIKit会维护graphic context的栈. 对于上面的UIKit的绘制代码. 下面是系统所做的内容:
- 创建CoreGraphic context(系统)
- 调用UIGraphicsPushContext(), 将该context入栈(系统)
- 使用UIKit的绘制方法 -[[UIColor grayColor] setStroke]等等(自己的代码)
- (可能获取当前的图片) (自己的代码)
- 调用UIGraphicsPopContext(), 将context出栈(系统)
- 清理前面创建的context的内存(系统)
绘画的模式
iOS 的绘画模式是painter's model
, 除非指定, 我们后绘制的内容,总是会覆盖在之前绘制的内容之上, 并且我们在某一时候改变context的某些属性,例如strokeColor,只会影响后面绘制的内容, 并不会影响前面绘制的内容.
Context state状态
Context是有维护一个栈来管理自己的状态的, 例如设置颜色, 线宽等等,都需要设置状态, 下面有实例表示:
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
// Set initial stroke/fill colors
[greenColor setFill];
[purpleColor setStroke];
// Draw the bunny
[bunnyPath fill];
[bunnyPath stroke];
// Save the state
CGContextSaveGState(context);
// Change the fill/stroke colors
[[UIColor orangeColor] setFill];
[[UIColor blueColor] setStroke];
// Move then draw again
[bunnyPath applyTransform:
CGAffineTransformMakeTranslation(50, 0)];
[bunnyPath fill];
[bunnyPath stroke];
// Restore the previous state
CGContextRestoreGState(context);
// Move then draw again
[bunnyPath applyTransform:
CGAffineTransformMakeTranslation(50, 0)];
[bunnyPath fill];
[bunnyPath stroke];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();
一个context会保存很多类型的状态, 常见的有如下几个: Color, tramsformation matrices, Clipping, Line parameters, Flatness, Alpha levels, Text traits, Blend modes等等
Context 坐标系
UIKit的坐标系是top-left, 而在Quartz中坐标系是bottom-left. 有一点非常重要: context的坐标系是是用的哪种, 关键在于我们通过哪里获取的context,如果我们使用任何UIKit的函数获取的Context,那么它的坐标系就是top-left,如果我们使用的CGBitmapContextCreate()方法创建的Context,那么坐标系就是bottom-left
翻转context坐标系
就算UIKit和Quartz的坐标系不匹配, 我们可以通过tramsform翻转一个坐标系, 使得两者映射到设备上时保持一致.
- 将CGContextRef push进入UIkit的stack
- 沿着水平轴对称翻转context
- 上移context
- 在新的坐标系中drawing
- 将context pop出栈
具体代码如下:
// Flip context by supplying the size
void FlipContextVertically(CGSize size) {
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL){
NSLog(@"Error: No context to flip");
return;
}
CGAffineTransform transform = CGAffineTransformIdentity;
// y轴翻转
transform = CGAffineTransformScale(transform, 1.0f, -1.0f);
// 上移动context的高度
transform = CGAffineTransformTranslate(transform, 0.0f, - size.height);
// 应用到当前context
CGContextConcatCTM(context, transform);
}
裁剪context坐标系
我们可以通过context 的clip, 将不需要的内容清除, 通常使用剪裁时候, 需要先保存当前context的状态, 因为剪切以后, 后面可以方便恢复原来的context 的状态.
- CGContextSaveGState(context) 保存当前context状态(可以省略)
- 在context中添加一个你需要clip的path.这个path是用于你clip当前context的mask, 这个mask以外的内容,就会被裁剪掉. 对于path,可以使用CGPathRef或者UIBezierPath, 然后调用对应的clip方法
- 做其他的drawing操作, 都是在裁剪以后的context上进行的,如果绘图操作在裁剪区域以外, 将被忽略.
- CGContextRestoreGState(context)(可以忽略), 然后做其他的绘制操作
具体的实例代码如下:
// Save the state
CGContextSaveGState(context);
// Add the path and clip
CGContextAddPath(context, path.CGPath);
CGContextClip(context);
// Perform clipped drawing here
// Restore the state
CGContextRestoreGState(context);
// Drawing done here is not clipped
Transfrom变换
我们设想一种场景, 需要一个绘制一串字母"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 绘制成圆形, 均匀分布. 因此每个字母的基于原点的弧度就是 (2 × Pi / 26) radians, 如下实例代码能够完成:
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for(inti=0;i<26;i++) {
NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)];
CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}];
CGFloat theta = M_PI - i * (2 * M_PI / 26.0);
CGFloat x = center.x + r * sin(theta) - letterSize.width / 2.0; CGFloat y = center.y + r * cos(theta) - letterSize.height / 2.0;
[letter drawAtPoint:CGPointMake(x, y) withAttributes:@{NSFontAttributeName:font}];
}
但是,所有绘制的字符都是正的,并没有完全满足需要如果需要, 实际上我们可以根据当前context transforms来完成这个需求.
Transform 状态
context中有一个状态中affine transform非常重要, 一般称为current transform matrix, 这个指定当前状态需要旋转,平移,缩放.
这里参考book上的代码