不同的绘图系统###
iOS主要的绘图系统有UIKit,Core Graphics(Quartz), Core Animation,Core Image和OpenGL ES,每一个都能针对不同的问题其作用。
UIKit 这是最高级的界面,是Objectiv-C中唯一的界面。它能用于轻松的访问布局,组成,绘图,字体,图片,动画等。可以通过UI前缀来识别UIKit元素,比如UIView和UIBezierPath。UIKit也扩展NSString来利用方法(比如drawInRect:withFont:)简化文本绘制。
Core Graphics (也称Quartz 2D) UIkit下的主要绘图系统,频繁用于绘制自定义视图。Core Graphics 是高度集成UIView和其他UIKit部分的。Core Graphics 数据结构和函数可以通过CG前缀来识别。
Core Animation这个提供了强大的2D与3D动画服务。它也与UIView高度集成。
Core Image最早在iOS5中出现的Mac技术。Core Image提供了非常快的图片过滤方式,比如切图,锐化,扭曲和其他你能想象的变形效果。
OpenGL ES 主要用来编写高性能游戏(尤其是3D游戏),OpenGL ES是OpenGL绘图语言的子集。对于iOS的其他应用来说,Core Animation通常是更好的选择。OpenGL ES在多个平台可兼容。
UIKit和视图绘制周期###
当你要改变视图的大小或者显示,画一条线,或者改变一个对象的颜色,这些改变在屏幕上不会立即显示出来。有事,这回让编写如下不恰当代码的人感到迷惑:
progreessView.hidden = NO;
[self doSomethingTimeConsuming];
progressView.hidden = YES;
第一行(progressView.hidden = NO)实际上根本没有作用,理解这一点很重要。这个代码不会使进度视图在执行耗时操作时显示出来。无论这个方法运行了多久,你都不会看到视图显示出来。
所有的绘制都发生在主线程,只要代码运行在主线程,就没有东西可以绘制了这就是不要子啊主线程中执行长时间运行操作的一个原因。这不仅会阻碍绘制更新,还会阻碍事件处理(比如影响触摸事件)。只要代码运行在主线程程,应用对于用户来说就是“功能挂起来的”。如果主线程历程返回足够快,这些变化根本就察觉不到。
可能会想:“那就在后台线程运行我的绘图指令”。但是通常是无法做到这一点的,因为对于当前的UIKit上下文来说绘图不是线程安全的。任何在后台线程修改视图的尝试都会导致未定义的行为,包括绘制出错或崩溃。
这个行为并不需要克服。绘图事件实际是iOS在有限的硬件上渲染复杂绘图的功能。如你会在本章看到的,UIKit中很多东西都要避免不必要的绘制,这是最开始步骤中的一个环节。那么,要如何开始和停止一个长时间运行操作的活动的指示器呢?可以采用调度(dispatch)或执行队列事将耗时的任务放入后台,同时创建如下在主线程中进行UIKit调用的代码。
ViewController.m(TimeConsuming)###
- (IBAction)doSomething: (id)sender {
[sender setEnabled:NO];
[self.activity startAnimatiing];
dispatch_queue_t bgQueue = dispatch_get_global_queue{DISPATCH_QUEUE_PRIORITY_DEFAULT,0};
dispatch_async(bgQueue,^{
[self somethingTimeConsuming];
dispatch_async(dispatch_get_main_queue(),^{
[self.activity stopAnimating];
[sender setEnabled:YES];
}) ;
});
}
调用IBAction后,便可以创建活动指示器的动画效果。然后,将对somethingTimeConsuming的调用放入默认的后台调度队列。完成后,可以把stopAnimating的调度放入主调度队列。调度和执行队列会在之后介绍。
简而言之,我们得出了一下结论
1.iOS 在运行循环(run loop)中整合所有的绘制请求,并以此将它绘制出来。
2.不能在主线程中进行复杂的处理。
3.不能在主线程之外的主视图上线文中绘制。你需要检查每个UIKit方法以确保它没有主线程需求。只要不是在主视图上下文中绘制,一些UIKit方法是可以在后台线程中使用的。
视图绘制与视图布局###
UIView将子视图的布局(“重新排列”)从绘图(“显示”)中独立出来。这对于最大程度低优化性能很重要,因为布局的成本要比绘制低。布局之所以成本低,是因为UIView的缓存铜鼓GPU优化的位图进行绘图操作。使用GPU,可以使这些位图的移动,显示,隐藏,旋转,甚至变形和合并的成本都非常低。
当你对一个视图调用了setNeedsDisplay方法,它就被标记为“需要刷新的”,并且会在下一次绘图周期中重新绘制。除非视图的内容真的会发生变化,否则请不要调用它。大部分UIKit视图会在其数据发生变化时自动管理重绘操作,因此除了自定义的视图,一般并不需要调用setNeedsDisplay方法。
当旋转设备或者滚动视图时,子视图就需要重新排列了,UIKit会调用setNeedsLayout方法;也就是对发生变化的视图逐次调用layoutSubviews方法。重写layoutSubviews方法,就会让应用在设备旋转或者视图滚动的时候更加流畅。你可以不必重绘他们,就可以重新排列子视图的位置,而且还可以根据设备方向隐藏或者显示视图。如果数据改变后,只需要进行视图更新(而不是绘制),只需要调用setNeedsLayout方法。
自定义视图的绘制###
视图可以通过子视图,图层,drawRect:方法来表现内容。通常来说,如果实现了drawRect:方法,最好就不要在混用图层和子视图了,即使这样做合法而且有很大帮助。大部分自定义绘图都是用UIKit或者Core Graphics实现的,虽然OpenGL ES 更易于集成。
2D绘图一般可以拆分成以下几个步骤操作:
线条:
路径(填充或轮廓图形)
文本
图标
渐变
2D绘图不能够操作单独的像素,因为像素是依赖于目标的。你可以从位图上下文中读取它,但无法使用UIKit或者Core Graphics函数来直接作用于它。
UIKit和Core Graphics都是用"pointer"绘图模型。这就意味着每个命令都是依次绘制并且在事件循环中在上一次绘图上叠加内容。在这个模型中顺序是非常重要的,必须从底层开始向上绘制。每次调用drawRect方法,都要对所需要的区域进行绘制。在调用drawRect方法时,绘图画布并不受到保护。
通过UIKit绘图###
在iPad出来之前,大部分自定义绘图都只能使用Core Graphics,因为使用UIKit并不能绘制任意形状。在iPhone OS 3.2系统中,苹果添加了UIBezierPath并使其更易于通过Objective-C来绘制。UIKit依然缺乏对渐变,线条,阴影以及一些高级特性(比如控制反锯齿和精确颜色管理)的支持。即便如此,UIKit如今仍然是一个非常方便实现大部分自定义绘图需要的方式。
绘制矩形的最简单办法是使用UIRectFrame或UIRectFill,如以下代码所示:
- (void)drawrRect:(CGRect)rect{
[[UIColor redColor] setFill];
UIRectFill(CGRectMake(10,10,100,10));
}
需要注意,你首先是如何通过[UIColor setFill]设置画笔颜色的。绘图在调用drawRect方法前,会在系统提供的上下文中完成。这个上下文中含有大量的信息,包括画笔颜色,填充颜色,文本颜色,字体,形状等。同一时间内,只有一支画笔和一支填充笔。他们的颜色可以绘制任何东西。
绘图是依赖顺序的。其中更包括画笔的命令。
体重drawRect方法的图形上下文,是特殊的视图图形上下文。还有其他类型的图形上下文,包括PDF以及位图上下文。所有这些都是使用同样的绘制技术,不过视图上下文是针对屏幕的绘制进行优化的。
路径###
UIKit包含了很多要比他的矩形绘制函数更加强大的绘图命令。它可以通过UIBezizerPath绘制任意曲线和线条。贝塞尔曲线是使用了一些触点的线条和曲线的数学表达方式。一般情况下,你不需要担心自己的数学水平,以为UIBezizerPath拥有处理大部分常见路径(线条,弧线,矩形或圆角矩形,椭圆)的简单方法。通过这些路径,你可以快速绘制大部分UI元素形状。以下代码是一个简单相撞缩放填充图形的实例。
FlowerView.m(Paths)######
- (void)drawRect:(CGRect)rect {
CGSize size = self.bounds.size;
CGFloat margin = 10;
CGFloat radius = rint(MIN(size.height - margin, size.width - margin)/4);
CGFloat xOffset,yOffset;
CGFloat offset = rint((size.height - size.height)/2);
if (offset > 0) {
xOffset = rint(margin / 2);
yOffset = offset;
}
else {
xoffset = -offset;
yoffset = rint(margin / 2);
}
[[UIColor redColor] setFill];
UIBezierPath * path = [UIBezier bezierPath];
[path addArchWithCenter:CGPointMake(radius * 2 + xOffset,adius + yOffset) radius startAngle:-M_PI endAngle:0 colckwise:YES];
[path addArcWithCenter:CGPointMake(radius * 3 + xoffset, radius * 2 + yOffset) radius:radius startAngle:-M_PI_2 endAngle:-M_PI_2 clockwise:YES];
[path addArcWithCenter:CGPointMake(radius * 2 + xOffset, adius * 3 + yOffset) radius:radius startAngle:0 endAngle:M_PI colokwise:YES];
[paht addArcWithcenter:CGPointMake(radius + xOffset,adius * 2 + yOffset) radius:radius startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:YES];
[path closePath];
[path fill];
}
FlowerView创建了由一系列弧线组成的路径,并用红色进行填充。创建路径并不会导致绘制任何内容。UIBezierPath只是一系列弧线,就好像NSString是一系列的字符。只有调用fill,弧线才会被绘制在当前的上下文中。
请注意M_PI(π)以及M_PI/2(π/2)常量的使用。弧线是由弧度表示的。因此π以及它的分数很重要。math.h定义了很多这样的常量,你可以直接使用它们而不必要在计算出来。弧线使用顺时针角度,认为0弧度指向右边,π/2弧度指向下方,π(或者-π)指向左边,而-π/2弧度指向上方。如果你愿意的话,也可以使用3π/2来表示向上,不过我认为-M_PI_2比3*M_PI_2更易于理解。如果弧度让你头疼的话,可以创建这个函数:
CGFloat RadiansFromDegrees(CGFloat d)
{
return d * M_PI / 180;
}
我觉得习惯使用弧度要比做数学计算好多了,不过你如果需要特殊的角度,那么使用角度要更简单一些。
在计算radius 和 offset时,可以使用rint(四舍五入)来确保对齐(这样就会像素对齐)。这可以帮你改善性能,并且可以避免模糊的边缘。所以大部分情况下都如你所愿,不过万一某一条弧线碰到了线条,它会导致差一错误。通常最好的办法是移动线条以便所有的值都是整数。
理解坐标系###
坐标,点和像素之间的微妙转化,也可能降低绘制的性能。导致线条和文字模糊。观察一下代码。
CGContextSetLineWidth(context,3.);
//绘制从坐标{10,100}到{200,100}的3像素水平线条
CGContextMoveToPoint(context,10.,100.);
CGContextAddLineToPoint(context,200.,100.);
CGContextStrokePath(context);
//绘制从坐标{10,105.5} 到 {200,105.5}的3像素宽水平线条
CGContextMoveToPoint(context,10.,105.5);
CGContextAddLineTopoint(context,200.,105.);
CGContextStokePath(context);
比较从{10,100} 和{10,105.5}出发的两条线。从 {10,100}到{200,100}的线条要比{10,100.5} 到{200,100}的线条要模糊的多,原因就是iOS对坐标系的解读方式。
构造一个CGPath时,便是使用了所谓的几何坐标系。这与数学中使用的坐标系是一样的。以两条网格线的交点来表示零坐标点。你无法绘制出真正的几何点或者几何线条。因为他们都是无限小和无限细的。iOS绘制中,必须将这些几何对象转换成像素坐标。这是一个可以指定颜色的2D网络。像素是设备能控制的最小的显示区域单位。
当调用了CGContextStokePath,iOS会让线条路径居中。理想情况下,线条有3像素宽,从y = 98.5到y=101.5。danshi ,但是这个线条仍然能不绘制。每个像素必须是唯一的颜色。一半是画笔颜色,一半是背景颜色。iOS通过两个颜色的平均值解决了这个问题。同样的技术也用在了反锯齿上。
在屏幕上,线条看起来会有些模糊。解决这个问题的方法是将水平或者垂直的线条移动到半个点的位置。这样当iOS 线条居中的时候。边缘刚好就是像素的边界。或者可以让线条更粗一些。
使用非整形宽度的线条,或者坐标系不是整形或者半整形时候,也可能遇到这样的问题。让iOS绘制小数像素时,都可能导致模糊。
填充工具与画笔不一样。画笔的线条是中心对齐路径的。而填充是基于路径的。如果填充从{10,100}到{200,103}的矩形,每个像素都会被正确填充。
目前讨论的视点与像素相同。而在Retain屏幕上,他们就不一样了。iPhone 4的每个点有4个像素,缩放比例为2。这样的事情就有了一些微妙的变化。而且通常是情况更好了。因为Core Graphics 和 UIKit的坐标都是用点来表示的。所有整数宽度的线条都以偶数个像素的个数来表示了。比如说,如果需要一个点宽的画笔,实际上就是一个两像素的画笔。绘制这条线,iOS需要填充路径两边的像素。这样就是整数的像素了。因此不需要反锯齿处理。当然,如果使用的坐标系不是整数或半整数。依然有可能遇到模糊的情况。
在Retina屏幕上,并不需要位移半个点的位置。不过这不会有影响。若是要支持iPhone 3GS或iPad2。便需要对水平或垂直线条使用半个点的位移。
只能对水平或者垂直线条使用这些方法。斜线和曲线应该进行反锯齿处理以便不会出现缺口。没有必要为他们进行偏移操作。