iOS绘图思想

原文链接

iOS绘图思想

高质量图像是应用UI中重要的一部分,提供高质量图像不仅会让应用看起来更棒,还会使它看起来像原生系统的扩展。iOS系统提供两种基础的方式来创建高质量图像:openGL原生渲染(Quartz,Core Animation,UIkit)。本文描述的是用原生渲染(如果您想学习OpenGL绘图,请看链接)

Quartz是主要的绘画接口,提供支持如:基于路径的绘图,抗锯齿渲染,渐变填充图案,图片,颜色,坐标空间变换,以及PDF创建、显示、解析。UIKit提供了通过OC封装的文字艺术(line art),Quartz 图片以及颜色处理。Core Animation为UIKit中view的property的基础动画变换提供了底层支持并且可以用来实现自定义动画。

本章节提供了iOS应用绘画的过程概述,伴随着每一种绘画技术都会有特定的绘画技巧。你将找到在iOS平台上优化绘画代码的提示和指导。

不是所有的UIkit类都是线程安全的。在主线程外的其他线程中执行绘画操作时请先检查文档。


UIKit 图像系统

在iOS中,所有在屏幕上绘画出来的-不管是通过OpenGL,Quartz,UIKit还是Core Animation - 都是在UIView的一个实例或子类范围中操作。Views决定了在屏幕上绘制发生的部位。如果你使用系统提供的views,系统会为你自动绘制。如果你定义的是自定义views,你必须自己提供绘制的代码。如果使用Quartz,Core Animation,UIkit去绘制,你将会用到在接下来的章节中描述的绘图思想。

除了立刻在屏幕上绘制,UIKit也允许你画到离屏的位图或者PDF图形的上下文中。当你在离屏上下文中绘制时,不是在一个view中绘制,也意味着在view的绘图周期中是不推荐这么做的(除非你随后取出图像并且在imageView或相似空间中绘制出来)。

View绘图循环

子类化的UIView的基础绘画模式是在必要时更新内容。UIView类使得更新过程更加简单有效:通过收集你发出的更新请求并将它们在合适的时机传输到你的绘制代码中。

当一个view第一次被展示出来或者view的一部分需要被重新绘制,iOS通过调用view的drawRect:方法让view绘制它的内容。还有很多行为可以触发view的更新:

  • 移动或移除一个正在遮挡view的另一个view
  • 通过设置一个view的hidden为No来使得先前隐藏的view可见
  • 将一个view从屏幕中划出后划入屏幕
  • 立刻调用view的方法:setNeedsDisplaysetNeedsDisplayInRect:

系统views是自动重绘的。对于自定义views来说,必须重载drawRect:方法并在其中执行绘制代码。在drawRect:方法中,使用原生绘制技术来绘画图形,文本,图像,渐变或其他你想要的视觉内容。你的view第一次变得可见时,iOS将一个矩形传入view的drawRect:方法,这个矩形包含了view的整个可见区域。在随后的调用中,这个矩形只包括需要重绘的区域。为了使性能最大化,你应该仅重绘受影响的区域。

在调用drawRect:之后,view将自己标记成已更新(updated)并且等待新操作来临以及触发另外一个更新循环。如果view展示的是静态内容,在由滑动和其它views的存在导致你的view的能见变化时,你唯一需要做的就是响应这些变化。

如果你想改变view的内容,必须通知view去重绘内容。调用setNeedsDisplaysetNeedsDisplayInrect:来触发一个更新。举个栗子,如果一秒内需要多次更新内容,你可能会想创建一个定时器来刷新view。也可以在响应用户操作或在view中创建新内容来更新view。

不要手动调用drawRect:,该方法应在iOS屏幕重绘期间通过代码创建。在其他情况下,图形上下文不存在,所以也不可能绘制(图像上下文会在下一章节中解释)

####iOS中的坐标系统以及绘制
当一个应用在iOS中绘制时,它必须在定义为坐标系统的二维空间内定位出绘制的内容位置。这个概念在第一眼看到时会觉得很直观,但事实上却很麻烦。iOS应用在绘制时有时需要应对不同的坐标系。

在iOS中,所有的绘制都在图形上下文中完成。从概念上讲,图形上下文是一个描述哪里、以及怎么绘制的对象,包含基础的绘制属性,例如绘制中使用的颜色,裁剪区域,线条宽度和样式信息,字体信息,合成选项等等。

此外,如图1-1中所示,每一个图形上下文都有一个坐标系。更确切的说,每一个图形上下文有三个坐标系:

  • 绘图坐标系(用户)。该坐标系是用于绘图任务的执行。
  • view坐标系(基础空间)。该坐标系是与view相关的固定的坐标系。
  • 设备坐标系(物理)。该坐标系代表屋里屏幕上的像素。

图1-1 绘图坐标,view坐标,硬件坐标的关系

iOS绘图框架可以创建一系列绘制的具体目标的上下文 - 屏幕,位图,PDF内容等等 - 并且这些图形上下文为该目标建立了具体的初始坐标系统。这个初始坐标系统被称作默认坐标系,它是1:1映射到view底层坐标系统的。

每一个view拥有一个当前变换矩阵(current transformation matrix(CTM)),是一个数学矩阵,将当前绘制坐标系中的点映射到(固定的)view坐标系中。应用可以更改这个矩阵来改变接下来绘制操作的表现。

每一个iOS绘制框架都会在当前图形上下文中建立一个默认坐标系。在iOS中,有两种主要的坐标系统:

  • 左上原点坐标系(upper-left-origin(ULO)),绘制操作的原点在绘制区域的左上角,向下和向右都是正值。用于UIKit和Core Animation的默认坐标系是基于ULO的。
  • 左下原点坐标系(lower-left-origin(LLO)),绘制操作的原点在绘制区域的左下角,向上和向右都是正值。用于Core Graphics框架的默认坐标系是基于LLO的。

iOS中的默认坐标系

OS X中的默认坐标系是基于LLO的。尽管Core Graphics以及AppKit框架的绘制方式和方法都完美适用于该坐标系,AppKit为左上角坐标系提供翻转坐标系的程序支持。

在调用drawRect:之前,UIkit为绘图操作建立了默认坐标系,(本句原文:UIKit establishes the default coordinate system for drawing to the screen by making a graphics context available for drawing operations)。在drawRect:中,应用可以设置图形状态参数(例如填充的颜色)以及在不需要明确指出图形上下文的前提下在当前的图形上下文中绘制。这种隐式的图形上下文建立了一个ULO默认坐标系

点与像素

在iOS中 在绘图代码中你指定的坐标系和实际底层设备的像素点是不同的。当使用原生绘图技术例如Quartz,UIKit,Core Animation,绘制坐标空间和view的坐标空间都是逻辑坐标空间。以points作为衡量单位。这些逻辑坐标系是在设备坐标空间使用系统框架在屏幕上管理pixels来解耦出来的。

系统自动的将view坐标空间的point映射为设备坐标空间的pixels,但不总是一对一的映射。这种特性引出了一个你需要记住的重要事实:

point不一定与pixel相同

使用points的目的是在无关设备间提供一个相同尺寸的输出。在大多数情况下,point的实际尺寸是不相干的。使用point的目的是提供一个相对一致的尺寸,你可以在代码中指定view的尺寸和位置并且渲染内容。point与pixel的映射方式实际是由系统框架处理的。举个栗子,在高分辨率屏幕的设备上,一个一point宽的线实际是两个pixel那么宽。结果就是如果你在两个相似的设备上绘制相同的内容,其中一个是高质量屏幕,在这两个设备上看到的内容看起来尺寸相同。

在PDF渲染和打印的上下文中,Core Graphics定义了point 是使用工业标准映射一英尺的1/72。

在iOS中,UIScreen,UIVew,UIImage,CALayer提供property来获取(或设置)缩放因子(scale factor),缩放因子是描述point和pixel之间关系的一个特定对象。例如,每一个UIkit view有contentScaleFactor属性变量。在标准分辨率屏幕上,缩放因子一般为1.0.而在高分辨率屏幕上,缩放因子一般为2.0。在将来可能会出现其它缩放因子。(在iOS4以上,缩放因子应该被认作1.0)。

原生绘图技术,例如Core Graphics,考虑一下当前的缩放因子。例如,如果一个view实现了drawRect:方法,UIKit自动将view的缩放因子设置为屏幕的缩放因子。另外 UIKit通过考虑缩放因子画图可以自动修复图形上下文的当前变换矩阵。在drawRect:中绘制的内容会根据底层设备屏幕来适当的缩放。

由于自动映射,当写绘制的代码时,通常不必关注pixel。然而有些时候你需要根据point与pixel映射规则来改变应用的绘制行为-在高分辨率屏幕下载高质量图片或在低分辨率屏幕上绘制时避免异常的缩放。

在iOS中,当你在屏幕上绘制事物时,图像子系统使用一种抗锯齿技术将高质量图片显示在低分辨率屏幕上。最好的解释就是举例。当你在纯白色背景下绘制一条黑色竖线,如果线条正好落在1px上,你会看到在白色区域出现一系列黑色像素点。如果它在两个px之间展示,看起来就是两个紧邻的灰色像素,如下图所示。

(右边的称为whole-numbered point)

位置是由whole-numbered points在pixels中点决定的。距离,如果你从(1.0,1.0)到(1.0,10.0)画了一条1px宽的竖线,会得到一条模糊的灰线。如果你绘制了一条2px宽的线,会得到一条纯黑线,因为它完全的覆盖了2个px。一般来说,以px为单位,奇数为值 的宽度的线条看起来比偶数值px的线条更柔和,除非你调整他们的位置使线条与pixel格子重合。

比例因子的作用就是决定1point宽的线条覆盖了几个pixel。

在低分辨率的情况下(1.0缩放因子),宽度 : 1 point 线条= 1 pixel 线条。在画1point宽的线条时为了避免抗锯齿,除非是偶数pixels宽,否则你必须将位置向任一边偏移0.5point。如果线条是偶数points宽,为了避免模糊的线条一定不要这么做。

在高分辨率情况下(缩放因子2.0),1point宽的线条是不会产生抗锯齿情况,因为它占满了两个px格子(-0.5~+0.5)。如果想绘制1pixel的线条,将宽度设置为0.5points 并且偏移量设为0.25,对照如上图所示。

当然,改变基于缩放因子的绘图特性可能会导致悲剧。1px宽的线条在某些设备上看起来完美但是在高分辨率设备上可能会太细导致无法看清。决定权在你手上!


获取图像上下文

大多情况下,图形上下文已经为你配置好。每一个view对象会在drawRect:被调用时自动创建图形上下文来使你的代码立即绘制自定义的内容。作为配置的一部分,UIView的底层为当前绘制环境创建图形上下文(一个CGContextRef不透明的类型)
如果你想在view外绘制东西(例如在位图或PDF中捕获一系列绘制操作),如果调用Core Graphics方法是需要一个上下文对象的,你必须进行额外的步骤来获取上下文对象。以下部分解释了原因。

更多关于图形上下文,修改图像状态(graphics state)信息以及使用图形上下文来创建自定义内容,查看Quartz 2D Programming Guide。关于图形上下文的一系列函数请参考这里CGContext Reference,CGBitmapContext Reference,CGPDFContext Reference.

向屏幕上绘制

如果使用Core Graphics函数绘制view,无论在drawRect:方法或者别的地方,前提是需要一个图形上下文(大多数这些方法的第一个参数一定是CGContectRef对象)。你可以调用UIGraphicsGetCurrentContext取得与在drawRect:中隐式创建的同一个上下文。由于是同一个图形上下文,绘制函数应该参考ULO默认坐标系

如果想使用Core Graphics函数在UIKit的view中绘制,应该使用ULO默认坐标系来进行绘图操作。此外,你可以将当前变换矩阵翻转并且在UIKit的view中使用CoreGraphics原生的LLO坐标系进行绘制。Flipping the Default Coordinate System中讨论了翻转的详情。

UIGraphicsGetCurrentContext方法总是会返回一个当前有效的图形上下文。比如,如果你创建了一个PDF上下文并且调用了UIGraphicsGetCurrentContext,可以接收到一个PDF上下文。如果想使用CoreGraphics方法来在view上绘制就必须使用UIGraphicsGetCurrentContext返回的图形上下文。

UIPrintPageRenderer类声明了绘制可印刷内容的方法。在一个类似于drawRect:方法中,UIKit在这些方法的实现中设置了隐式的图形上下文。这个图形上下文建立了一个ULO默认坐标系

绘制位图上下文和PDF上下文

UIKit提供了以下行为的方法:在位图图形上下文渲染图片,通过在PDF图形上下文中绘制产生PDF内容。调用这些方法的第一步都是创建一个图形上下文-分别是一个位图上下文或者PDF上下文。返回的对象作为当前(以及隐式)图形上下文进行连续的绘制和状态设置。当完成在上下文中的绘制,调用另外一个方法来关闭上下文。

UIKit的位图上下文和PDF上下文都建立了一个ULO默认坐标系。CoreGraphics也有对应的方法来在位图上下文中渲染和在PDF图形上下文中绘制。通过CoreGraphics创建的上下文是基于LLO默认坐标系的

在iOS中,建议你使用UIKit框架来绘制位图上下文和PDF上下文。如果你使用CoreGraphics作为备胎并打算显示渲染结果,你需要通过调整你的代码来弥补两个不同的默认坐标系的差异。更多信息请查看Flipping the Default Coordinate System


颜色和色彩空间

iOS在Quartz中支持全系列色彩空间,然而大多数应用只需要RGB色彩空间,因为iOS被设计用于运行在嵌入式硬件并且在屏幕上显示图像,RGB色彩空间是最合适的选择。

UIColor对象提供了便捷的方法指定RGB,HSB,灰阶色值。如果以该方式创建颜色,不需要指定色彩空间,因为UIColor自动为你定义了一个。

也可以使用CoreGraphics框架中的CGContextSetRGBStrokeColorCGContextSetRGBFillColor来创建和设置颜色。尽管CoreGraphics包含了对(通过创建其它色彩空间以及自定义色彩空间来创建颜色)的支持,我们还是不推荐这么做。绘制的代码应该一直使用RGB颜色。

使用Quartz和UIKit绘制

Quartz在iOS中是原生绘画技术的通用名。CoreGraphics框架是Quartz的核心,也是绘制内容最基础的接口。该框架提供了数据类型和函数可以操作以下内容:

  • 图形上下文
  • 路径
  • 图片和位图
  • 透明层
  • 颜色,图案颜色和色彩空间
  • 渐变和阴影
  • 字体
  • PDF内容

Quartz是专注于设计图像相关操作的类而UIKit正是基于Quartz的基础特性设计的。UIKit 图像类并不是作为全能的绘图工具而设计的-CoreGraphics已经做到了。相反的,CG为其它的UIKit类提供绘图支持。UIKit支持以下类和方法:
|class | description|
|:—|—:|
|UIImage|显示图片|
|UIColor|为设备颜色提供基础支持|
|UIFont|为需要的类提供字体信息|
|UIScreen|提供屏幕相关基础信息|
|UIBezierPath|允许应用绘制线条、弧线、椭圆和其它|

  • 产生JPEG或PNG格式的UIImage对象的方法
  • 绘制位图图形上下文的方法
  • 向PDF图形上下文内绘制来得到PDF数据的方法
  • 绘制矩形,裁剪绘制区域的方法
  • 改变和获得当前图形上下文的方法

更多UIKit的方法和类,查看UIKit Framework Reference。更多关于CoreGraphics框架的类型和函数,查看Core Graphics Framework Reference

配置图形上下文

在调用drawRect:之前,view对象创建了一个图形上下文并设置为默认。该上下文只存在于drawRect:被调用的生命周期中。可以调用UIGraphicsGetCurrentContext来获得该图形上下文的指针。返回值是CGContextRef类型,将它传入CoreGraphics函数中修改当前图形状态。下表列举出不同方面的图像状态。完整函数请看CGContext Reference

图形上下文拥有一个存放图像状态的栈。当Quartz创建了一个图形上下文,此时栈是空的。使用CGContextSaveGState将当前图形上下文状态压栈。随后,你可以通过更改图像状态来影响接下来的绘制操作,但不会影响栈中存储的操作。当你完成修改后,你可以调用CGContextRestoreGState弹出栈内上下文来返回之前的上下文状态。这样弹栈和压栈是一种返回之前状态的快速方法并且无需解开每个状态变化。(Pushing and popping graphics states in this manner is a fast way to return to a previous state and eliminates the need to undo each state change individually)。但这也是唯一用来存储一些状态的方式,例如裁剪路径,返回初始设置。
更多关于图形上下文的信息和使用它们的配置环境,查看Graphics Contexts,Quartz 2D Programming Guide

创建并绘制路径

路径是从一系列线条和贝塞尔曲线的基于向量的图形。UIKit拥有UIRectFrameUIRectFill方法来绘制简单的路径,例如你view中的矩形。CoreGraphics 也提供了创造简单路径譬如矩形和椭圆的便捷函数。

对于复杂的路径,需要使用UIBezierPath来自己创建路径,或使用CoreGraphics框架中操作CGPathRef的函数。尽管可以不用图形上下文来创建路径,路径中的点必须参考默认坐标系(ULO或LLO),你仍旧需要使用图形上下文来渲染路径。

当绘制路径时,必须有当前上下文集。该图集可以是自定义view的(drawRect:中)上下文,位图上下文或者PDF上下文。坐标系决定了路径的渲染方式。UIBezierPath默认使用ULO坐标系,也就是说如果你的view被翻转使用了LLO坐标系,图形渲染结果可能会与预期不同。最好是指定点与用于渲染的图形上下文的当前坐标系原点绑定。

  • 即使遵循该规则,弧线仍然需要更多额外的工作。如果使用Core Graphics函数通过在ULO坐标系中定位点来创建路径,随后在UIKit的view中渲染路径,弧线的方向是不同的。查看更多相关信息Side Effects of Drawing with Different Coordinate Systems

在iOS中创建路径,更推荐使用UIBezierPath来代替CGPath方法,除非需要使用CoreGraphics的特性,例如在路径中添加椭圆。更多创建和渲染路径的方法,点击Drawing Shapes Using Bézier Paths

更多关于使用UIBezierPath绘制路径的方法,查看Drawing Shapes Using Bézier Paths。更多关于使用CoreGraphics绘制路径,例如如何指定点到复杂路径中,查看Quartz 2D中的Paths。更多关于创建路径的信息CGContext ReferenceCGPath Reference

创建图案,渐变和阴影

CoreGraphics框架包含了创建图案,渐变,阴影的功能。你可以使用这些来创建非黑白色并且使用使用它们填充你创建的路径。图案是从重复的图片或内容中创建出来的。渐变和阴影可提供不同的方案来创建颜色到颜色的平滑的过度。

创建,使用图案,渐变和阴影的更多细节,查看Quartz 2D Programming Guide

自定义坐标空间

UIKit默认直接创建用于映射point到px的当前转换矩阵。尽管在绘制中完全不需要修改这个矩阵,但有时候更改该矩阵会更方便。

当view第一次调用drawRect:时,CTM(当前转换矩阵)已经配置好所以坐标系原点与view的原点相匹配,X轴向右为正,Y轴向下为正。然而你可以通过添加缩放,旋转和转换因子来改变CTM从而改变大小,方向,并且默认坐标系的位置与底层的view或window有关。

使用坐标变换来提升绘制表现

修改CTM是绘制内容的一个基础技巧,因为它允许你重用路径,这样可能会减少绘制时的计算量。举个栗子,如果你想在(20,20)画一个正方形,需要创建一个路径移动到(20,20)随后绘制必要的线条。然而,如果你又想把正方形移动到(10,10),可能需要重新创建新起点的路径。因为创建路径实在是一个消耗较大的操作,它会更倾向于创建一个原点在(0,0)的正方形,然后修改CTM使得正方形绘制在正确区域。

在CoreGraphics框架中有两种方式可以修改CTM。可以使用CGContext Reference中定义的CTM操作函数立刻修改CTM。也可以创建一个CGAffineTransform结构体,应用各种你想使用的变换,然后将这个变换应用到CTM中。使用仿射变换可以让你的组(group)变换随后立刻应用到CTM上。你也可以评估,倒置仿射变换并用它们来修正代码中的点,大小和矩形值。更多仿射变换信息Quartz 2D Programming GuideCGAffineTransform Reference

翻转默认坐标系

Flipping in UIKit drawing modifies the backing CALayer to align a drawing environment having a LLO coordinate system with the default coordinate system of UIKit。如果仅使用UIKit来绘制,就不需要翻转CTM。然而如果是CoreGraphics和UIKit调用的图片I/O 函数混合使用,这时候翻转就派上用场了。

如果你调用CoreGraphics绘制了图片或PDF文档,对象会在view的上下文中渲染的乱七八糟。你必须翻转CTM让图片和文稿正确显示。

翻转绘制在CoreGraphics上下文中的对象以便于在UIKit的view中正确显示,修改CTM一共分两步。将原点变换到绘制区域的左上角,随后进行尺寸变换,修改y-coordinate为-1,代码如下:

1
2
3
4
5
CGContextSaveGState(graphicsContext);
CGContextTranslateCTM(graphicsContext, 0.0, imageHeight);
CGContextScaleCTM(graphicsContext, 1.0, -1.0);
CGContextDrawImage(graphicsContext, image, CGRectMake(0, 0, imageWidth, imageHeight));
CGContextRestoreGState(graphicsContext);

如果通过CoreGraphics image对象初始化了一个UIImage对象,UIKit自动帮你翻转。每一个UIImage对象是由CGImageRef类型构成的。可以通过CGImage属性获取CoreGraphics对象并且做一些操作。当完成后,从修改后的CGImageRef对象中可以重建UIImage对象

可用CoreGraphics方法CGContextDrawImage来绘制渲染目标。它有两个参数,第一个是图形上下文,第二个是矩形区域(既是图片的大小同时也是绘制表面的位置,比如View)。当使用该方法时,如果不调整当前坐标系为LLO,图片在UIKit的view中会颠倒。另外,矩形的原点是相对于函数调用时刻当前坐标原点。

不同坐标系绘制的副作用

假设使用LLO坐标系调用CGContextAddArcCGPathAddArc绘制路径,需要翻转CTM来在UIKit的view中正确渲染弧线。如果使用如果在ULO坐标系中渲染到UIKit的view上,你会发现弧线会和初始的不一样。当使用UIBezierPath绘制弧线时,弧线的终点与LLO相反,例如,一个向下的箭头现在向上,并且顺逆相反。你必须将CoreGraphics中绘制的弧线改成基于ULO坐标系;这些函数的startAngleendAngle控制了弧线方向。

(通过调用CGContextRotateCTM)可以旋转对象,你会观察出它们完全是镜像效果。如果旋转一个ULO坐标系的CoreGraphics中的对象,它的方向是和UIKit中的旋转方向相反。你必须为在代码中为他们设置不同的旋转方向(CGContextRotateCTM),反转angle的参数

阴影

盈盈的方向是对象指定偏移量,也就是说偏移量在绘制框架中是一个惯例。在UIKit中,xy轴的正向偏移量似的阴影向下、向右。在CoreGraphics中,则是向上、向右。翻转CTM使得默认坐标系对齐不会影响对象的阴影,阴影也不会正确的追踪对象。如果想正确的追踪,必须修改当前坐标系的偏移量。

在iOS3.2以上,CoreGraphics和UIKit拥有共同的阴影方向:正向偏移量使得阴影向下、向右。


应用CoreAnimation效果

CoreAnimation是OC中提供快速便捷创造流动,实时的动画。CoreAnimation本身不是一个绘制技术,也就是说它不提供创建图形,图片或其他内容。相反的,它是一个操纵并展示由其他技术创建的内容。

在很多案例中,付出一点就可以从CoreAnimation中获得好处。例如,UIView的很多属性(包括view的frame,center,color,opacity)可以配置在值变化时触发动画。你需要做一些事情来让UIkit知道你想执行这些动画,但它们却是自动为你运行的。更多关于如何触发view内置动画,查看Animating ViewsUIView Class Reference

如果想查看更多高级动画教程,查看Core Animation Programming Guide

关于Layer

CoreAnimation的核心技术就是layer对象。layer是和view相似的轻量级对象,但它们实际上是一个模型对象。封装了几何、时间、视觉属性等你想展示的内容。

  • 将CGImageRef赋值到layer对象的contents属性
  • 给layer赋值一个delegate并处理绘制
  • 可以子类化CALayer并重载其中的显示方法。

Layer相关信息请看Core Animation Programming Guide

坚持原创技术分享,您的支持将鼓励我继续创作!