iOS 图形 API 入门:第 1 部分






4.91/5 (38投票s)
iOS 图形 API 简介的第一部分。在本文中,我将介绍一些 Quartz 2D / Core Graphics API。
引言
在本系列的第一篇文章中,我快速介绍了 Objective-C,并谈论了一些内存管理、控件的使用以及信息持久化到文件。在本文中,我想介绍一些图形功能。我将使用 iPad 模拟器作为本文的目标设备,因为它提供了更好的显示表面。但这里展示的 API 将在 iPad、iPhone 和 iPod Touch 上运行。由于这些 API 是从 Mac OS X 移植的,因此它们也可以在 Macintosh 上运行。
必备组件
要充分利用本文,您需要熟悉 Objective-C 和 iPhone 开发。如果您不熟悉,那么您需要查看我编写的关于 iPhone 开发的第一篇文章。您还需要熟悉数学(代数和一些三角函数),因为图形和数学是密不可分的。本文所需的硬件只有一台运行 Snow Leopard 和 iOS SDK 的基于 Intel 的 Macintosh。
可用 API
iPhone 支持两种图形 API 系列:Core Graphics/Quartz 2D 和 OpenGL ES。OpenGL ES 是一个跨平台图形 API。Quartz 2D 是 Apple 专用 API。它是 Core Graphics 框架的一部分。OpenGL ES 是一个更大图形 API (OpenGL) 的精简版本。请记住,OpenGL ES 是一个应用程序编程接口;它描述了可用的函数、结构、它们的使用语义以及函数应该具有的行为。设备制造商如何选择实现这些行为并符合此规范将是他们的实现。我指出这一点是因为我遇到了很多基于对接口和实现之间差异的误解的对话。如果这种差异难以理解,请考虑这个类比:一个发条时钟和一个电子时钟都具有相同的视觉接口和相同的行为,但它们的内部工作原理(实现)是不同的。由于制造商在实现 OpenGL ES 时具有很大的自由度,因此您会发现在不同系统之间存在广泛的性能差异。值得庆幸的是,在 iOS 设备上,与其他一些支持 OpenGL ES 的设备相比,性能的下限仍然相当高。
表示颜色
有几种不同的方法来数字表示颜色。典型的方法是通过表示原色的强度来表示单个颜色,当这些原色混合在一起时会产生相同的颜色。原色是红色、绿色和蓝色。如果您认为黄色是原色而不是绿色,那么您可能正在考虑原色减色(在使用纸上绘画时相关,但在照亮像素时无关)。Quartz 2D 支持其他数字表示颜色的系统,但我不会在这里讨论它们。我只会使用红色、绿色和蓝色表示的颜色。这也称为 RGB 颜色空间。这些颜色的每个分量都表示为一个浮点数。最低强度为 0,最高强度为 1.0。
除了这些像素强度之外,还有第四个颜色分量,通常命名为“Alpha”。Alpha 分量用于表示透明度级别。如果颜色完全不透明(非透明),则此值为 1.0。如果颜色完全透明(因此不可见),则值为 0。当 RGB 颜色还具有 alpha 分量时,根据所查看的系统,这将被称为 ARGB 颜色空间或 RGBA 颜色空间(区别在于 alpha 分量的位置)。在本文的其余部分中,RGBA 将用于描述此类颜色。虽然 Quartz 2D 支持多种不同的颜色格式,但 OpenGL ES 只支持 RGBA。
屏幕坐标
在屏幕上定位项目时,您通常会使用点 (CGPoint
) 来定位屏幕上的项目。自然会认为点坐标和像素坐标是相同的。但在 iOS 中,情况并非总是如此。一个点不一定映射到相同坐标的像素。映射由系统处理。当您查看一个应用程序如何在具有不同像素分辨率的设备上运行时,您会看到它发挥作用。如果您想查看像素和点之间的关系,可以查看 UIImage
、UIScreen
或 UIView
类公开的比例因子。
Quartz 2D 和 Core Graphics 简介
使用 Quartz 2D,您可以渲染到视图或内存中的图像。您绘制的表面具有颜色,如果您调用各种函数在表面上渲染,如果您使用透明颜色绘制,则颜色将与它下面的任何内容混合。在示例程序中,我们将从绘制到 UIView
开始,以便您可以立即了解 Quartz 2D 的工作原理。为此,我们将创建一个派生自 UIView
的新视图类,并将在对象的 (void)drawRect:(CGRect)rect
方法中调用 Quartz 2D 进行绘制。
Core Graphics API 都在上下文中运行。您需要获取视图的上下文并将其传递给 Quartz 2D 函数以进行渲染。如果您要渲染到内存中的图像,那么您将传递其上下文。您视图的上下文可以通过以下函数调用获取
CGContextRef context = UIGraphicsGetCurrentContext();
构建您的第一个 Quartz 2D 应用程序
打开 Xcode 并创建一个名为 MyDrawingApp 的新 iOS 基于视图的应用程序。创建应用程序后,单击Classes文件夹。我们将创建一个新的 UIView
控件并在该视图中执行渲染。通过右键单击Classes文件夹并选择“Add New File”来创建一个新的 Cocoa Touch 类文件。选择Objective-C class并选择 UIView
作为Subclass of设置。(默认是 NSObject。确保未选择此项。)单击“Next”,当提示您输入文件名时,输入“MyDrawingView.m”。将创建*.h和*.m文件。
对于这个第一个程序,我只想在屏幕上绘制一些东西;除了在屏幕上绘制一些东西,这个程序不会做更多的事情。打开您刚刚添加的类的*.m文件。我们将从重写类的初始化方法开始。我们这个类的实例将在 Interface Builder 中创建。以这种方式创建的对象通过调用 initWithCoder:
而不是 init
进行初始化。因此,我们需要重写这个方法。
-(id) initWithCoder:(NSCoder*)sourceCoder
{
if( ( self = [super initWithCoder:sourceCoder]))
{
//place any other initialization here
}
return self;
}
目前,我们不需要在初始化方法中做任何事情。但我已经让您将其包含在此处作为其他代码的占位符。要在手机上显示此视图,我们将它设置为应用程序的基类。在 Xcode 中,找到MyDrawingAppViewController.xib并在 Interface Builder 中打开它。按 command-4 以确保身份检查器已打开。您会看到当前视图设置为从 UIView
继承。我们希望它改为从我们的类 MyDrawingView
继承。保存您的更改并关闭 Interface Builder。编译并运行您的代码以确保一切正常。完成此操作后,我们就可以开始绘制了!
在MyDrawingView.m中,有一个名为 drawRect:
的方法,其中没有代码。我们将把绘图代码放在那里。我们需要获取我们的图形上下文,设置我们的绘图颜色和其他属性,然后在屏幕上绘制我们的形状。现在,让我们绘制一条简单的线。
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
UIColor* currentColor = [UIColor redColor];
CGContextRef context = UIGraphicsGetCurrentContext();
//Set the width of the "pen" that will be used for drawing
CGContextSetLineWidth(context,4);
//Set the color of the pen to be used
CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
//Move the pen to the upper left hand corner
CGContextMoveToPoint(context, 0, 0);
//and draw a line to position 100,100 (down and to the right)
CGContextAddLineToPoint(context, 100, 100);
//Apply our stroke settings to the line.
CGContextStrokePath(context);
[currentColor release];
}
打开MyDrawingAppViewController.xib并单点“View”图标。当它高亮显示时,按 command-4 以确保选中身份检查器。在 Class 设置旁边,将下拉菜单从 UIView
更改为 MyDrawingView
。关闭 Interface Builder 并保存您的更改。返回 Xcode 并运行您的项目。您将在屏幕的左上角看到一条红线。
虽然与图形没有直接关系,但我想稍微深入探讨一下触摸交互。如果这个程序是交互式的,它可能会更有趣。我们将对其进行更改,以便通过在屏幕上拖动手指来选择的两个点之间绘制线条。我们还将更改程序以保留其对颜色的引用,而不是每次屏幕刷新时都获取新颜色。打开MyDrawingViewView.h文件并进行以下添加
#import <uikit/uikit.h>
@interface MyDrawingView : UIView {
CGPoint fromPoint;
CGPoint toPoint;
UIColor* currentColor;
}
@property CGPoint fromPoint;
@property CGPoint toPoint;
@property UIColor* currentColor;
@end
适当的 @synthasize
语句需要添加到 MyDrawingView.m 文件的顶部。将以下内容添加到该文件
#import "MyDrawingView.h"
@implementation MyDrawingView
@synthesize fromPoint;
@synthesize toPoint;
@synthesize currentColor;
到目前为止,我还没有提及触摸交互。我将在另一篇文章中讨论触摸事件和其他事件处理;现在,我将采取令人满意的路线,快速介绍感兴趣的交互。我们需要响应三个事件才能将触摸交互添加到程序中:touchesBegan:
、touchesEnded:
和 touchesMoved:
。所需事件的代码如下。将其添加到您的 MyDrawingView.m 文件中。
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
UITouch* touchPoint = [touches anyObject];
fromPoint = [touchPoint locationInView:self];
toPoint = [touchPoint locationInView:self];
[self setNeedsDisplay];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
toPoint=[touch locationInView:self];
[self setNeedsDisplay];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
toPoint = [touch locationInView:self];
[self setNeedsDisplay];
}
剩下的唯一事情就是修改我们的绘图代码,使其不再在两个固定点之间绘制,而是在我们触摸的点之间绘制,并删除绘图代码中 currentColor
的声明和释放(因为我们现在使用成员属性来存储颜色)。
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context,4);
CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
CGContextMoveToPoint(context,fromPoint.x , fromPoint.y);
CGContextAddLineToPoint(context, toPoint.x, toPoint.y);
CGContextStrokePath(context);
}
运行程序,尝试在屏幕上的不同点拖动手指(或鼠标)。您将看到线条在您触摸的点之间绘制。
使用图像
iPhone 上有两种可用的图像:CGImage
和 UIImage
。CGImage
是一个结构体,包含图像数据,可以传递给各种 Core Graphics 函数。UIImage
是一个 Objective-C 类。到目前为止,UIImage
类更容易使用,所以让我们从使用它在程序中绘制图像开始。在您的计算机上找到一张小于 500x500 像素的图像。图像可以是 PNG 或 JPEG 文件。在 Xcode 的项目中,您将看到一个名为 Resources 的文件夹。将您的图像拖放到 Xcode 中的 Resources 文件夹,当提示时,选择 Copy items into destination group's folder (if needed) 选项。我使用的是一个名为 office.jpg 的文件,我将以这个名称来引用我的图像文件。请记住用您的图像名称替换它。
在MyDrawingView.h文件中,声明一个名为backgroundImage
的新UIImage*
变量。在MyDrawingView.m实现文件中,添加@syntasize backgrounImage;
语句。我们需要在视图初始化时从资源加载图像。在-(id)initWithCoder:
方法中,添加backgroundImage = [UIImage imageNamed:@"office.jpg"];
。请记住将@"office.jpg"
替换为您的图像文件名。此行将从资源加载图像。在-(void)drawRect:
方法的顶部,添加以下两行
CGPoint drawingTargetPoint = CGPointMake(0,0);
[backgroundImage drawAtPoint:drawingTargetPoint];
如果您现在运行程序,它将在您绘制的线条后面渲染背景图像。
点与像素
iOS 设备屏幕的物理分辨率与您用于绘图的坐标之间存在一个概念层面的分离。在许多图形环境中,点和像素可以互换使用。在 iOS 设备上,操作系统会将点映射到像素。在位置 (10,25) 绘制某个对象可能不会导致对象出现在左侧 10 像素、顶部 25 像素处。点与实际像素之间的关系可以通过从 UIScreen
、UIView
或 UIImage
读取的比例因子进行查询。当您查看在 iPhone 3Gs 和 iPhone 4 上运行的相同程序时,可以看到这种逻辑坐标与物理坐标分离的结果。假设开发人员没有做任何事情来利用 iPhone 4 屏幕的更高分辨率,当代码在设备屏幕上绘制线条或图像时,它将在设备屏幕上占据相同的比例空间。
矢量操作,如绘制矩形、线条和其他几何形状,在标准分辨率和更高分辨率设备上都能正常工作,无需调整代码。对于位图,您需要做一些额外的工作。您需要拥有图像的标准分辨率和高分辨率版本才能获得最佳结果。您的资源名称应符合特定模式。标准分辨率设备有一种模式,高分辨率设备有另一种模式。
标准分辨率 | <ImageName>[DeviceModifier].<fileExtention> |
高分辨率 | <ImageName>@2[DeviceModifier].<fileExtention> |
资源名称的 [DeviceModifier]
部分是可选的。它可以是字符串 ~iphone
或 ~ipad
。图像的低分辨率和高分辨率版本名称之间的主要区别在于名称中的 '@2
'。高分辨率图像的宽度和高度应该是标准分辨率图像的两倍。(对于熟悉 MIP 贴图的人来说,这会很熟悉。)
路径
路径描述了一个形状。路径可以由线条、矩形、椭圆和其他形状组成。绘图空间内的坐标使用点指定。很容易将点视为像素,但它们不是一回事(更多内容请参阅点与像素部分)。通常,您将通过传递一对浮点数或使用 CGPoint
结构来传达点。您已经在本程序中看到了 CGContextAddLineToPoint
。还有一个 CGContextAddLines
函数用于绘制多个线条,其点作为数组传递。CGContextAddEllipseInRect
添加椭圆。这两个函数都接受一个 CGRect
,该 CGRect
定义了要绘制的形状的边界矩形。
弯曲的线条(更具体地说是贝塞尔曲线)可以使用函数 CGContextAddCurveToPoint
生成。弯曲的线条将从上一次绘图操作发生点开始(记住您可以使用 CGMovePointToDraw
更改此点),并将在函数调用中指定的点结束,其曲线将受到函数调用中传递的两个控制点的影响。如果您以前从未接触过贝塞尔曲线,Wikipedia.org 上有一篇关于它们的好文章。
如果您需要创建复杂的路径(由多个路径组成的路径),您首先要调用 CGContextBeginPath
,然后通过调用 CGContextMoveToPoint
设置路径的起点。然后调用函数将形状添加到路径中。完成后,使用 CGContextClosePath
关闭路径。创建路径不会将其渲染到屏幕上。直到您绘制它,它才会被渲染。一旦绘制完成,路径将从图形上下文中移除,您可以开始渲染新路径(或某些其他操作)。
要绘制路径,您可以使用 CGContextStrokePath
或 CGContextFillPath
对其应用描边和/或填充。描边会影响路径周围线条的显示方式(又名边框)。除了其他函数之外,使用 CGContextSetLineWidth
和 CGContextSetStrokeColor
或 CGContextSetStrokeColorWithColor
函数来设置线条的颜色。调用 CGStrokePath
会将描边应用于当前路径。
简单几何图形的填充规则直截了当,不需要太多解释;线条内部的区域会被填充。当使用重叠边框创建自己的自定义路径时,填充区域的规则会稍微复杂一些。根据 Apple 文档,使用的规则称为非零缠绕数规则(此处)。描述用于判断某个点是否在要填充区域内的过程有点抽象。选择要测试的点,并从该点绘制一条线到绘图边界之外,计算它相交的路径段的数量。从零开始计数,每次线与从左到右的路径段相交时加一,每次线与从右到左的路径段相交时减一。如果结果是奇数,则该点应该被填充。如果结果是偶数,则该点不应该被填充。另一种规则是简单地计算上述过程中绘制的线与路径段相交的次数,而不考虑段的方向。如果结果是偶数,则不填充该点。否则,该点将被填充。这称为奇偶规则。
剪裁
上下文自动拥有一个剪裁表面,其大小与它正在绘制的表面相同。如果需要进一步限制绘图区域,可以创建额外的表面区域。要创建新的剪裁区域,您需要创建一个路径,然后调用剪裁函数而不是绘图函数。结果剪裁区域是当前剪裁区域和正在应用的剪裁区域的交集。剪裁被视为图形状态的一部分。如果需要设置和恢复剪裁区域,则需要保存并恢复上下文。
CGContextClip
将当前路径应用于当前剪裁区域。CGContextClipToRect
将一个矩形应用于剪裁区域。CGContectClipToRects
将多个矩形应用于剪裁区域。
渐变
渐变是颜色逐渐变化的区域。Quartz 2D 提供了两种类型的渐变:线性(或轴向)渐变和径向渐变。渐变颜色的变化还可以包括 alpha 值的变化。有两种对象可用于创建渐变:CGShadingRef
和 CGGradient
。
CGGradient
类型是创建渐变两种方法中较简单的一种。它接受位置和颜色的列表,并根据该列表为您计算渐变中每个点的颜色。我只在我的代码示例中使用 RGB 颜色空间,所以这也就是我将用于渐变颜色空间选项。一些 Apple 文档会引导您使用 CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
来完成此操作,但请忽略它。该函数已弃用。相反,请使用 CGColorSpaceCreateDeviceRGB();
。如果您将以下代码添加到 -(void)drawRect
函数的开头并重新运行程序,您将在背景中看到线性渐变。
//Gradient related variables
CGGradientRef myGradient;
CGColorSpaceRef myColorSpace;
size_t locationCount = 3;
CGFloat locationList[] = {0.0, 0.5, 1.0};
CGFloat colorList[] = {
1.0, 0.0, 0.5, 1.0, //red, green, blue, alpha
1.0, 0.0, 1.0, 1.0,
0.3, 0.5, 1.0, 1.0
};
myColorSpace = CGColorSpaceCreateDeviceRGB();
// CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
myGradient = CGGradientCreateWithColorComponents(myColorSpace, colorList,
locationList, locationCount);
//Paint a linear gradient
CGPoint startPoint, endPoint;
startPoint.x = 0;
startPoint.y = 0;
endPoint.x = CGRectGetMaxX(self.bounds)/2;
endPoint.y = CGRectGetMaxY(self.bounds)/2;
CGContextDrawLinearGradient(context, myGradient, startPoint, endPoint,0);
如果您想做径向渐变而不是线性渐变,那么您需要调用 CGContextDrawRadialGradient()
而不是 CGContextDrawLinearGradient
。
//Radia Gradient Rendering
float startRadius = 20;
float endRadius = 210;
CGContextDrawRadialGradient(context, myGradient, startRadius,
startPoint, endRadius, endPoint, 0);
这个径向渐变的第二个圆心与屏幕中心对齐。因此,渐变止于圆。可选地,渐变可以设置为超出圆继续或在第一个圆开始之前延伸。为此,最后一个参数应包含选项 kCGGradientDrawsAfterEndLocation
以将渐变扩展到终点之外,或选项 kCGGradientDrawsBeforeStartLocation
以将渐变拉伸到起点之前的区域。使用此选项与线性渐变和径向渐变的结果如下所示。
|
|
CGContextDrawRadialGradient(context, myGradient, startPoint, startRadius, endPoint, endRadius, kCGGradientDrawsAfterEndLocation); |
CGContextDrawLinearGradient(context, myGradient, startPoint, endPoint, kCGGradientDrawsAfterEndLocation); |
使用 CGShadingRef
CGShadingRef
接受您创建的 CGFunction
,其中包含一个用于计算渐变颜色的函数。CGShading
对象还包含有关正在生成渐变的类型(线性或径向)以及渐变的起始和结束点的信息。创建并填充 CGShading
对象后,通过调用函数 CGContextDrawShading
渲染渐变。
创建着色函数时,您需要定义三个参数。函数的返回类型为 void
。
void *info
- 指向您决定传递给函数的数据的指针。const float *inValue
- 函数的输入值。您为此参数定义输入范围。float* outValues
- 函数输出值的数组。您的颜色空间的每个分量以及 alpha 分量都必须有一个输出值。每个分量的范围在 0 到 1 之间。
您的函数将被多次调用,其值范围从渐变长度上定义的输入范围的低端到高端。我的示例中,我将对输入值应用一个 sin
函数。
static void myCGFunction ( void * info, const float *in, float * outValue)
{
int componentCount = (int)info;
float phaseDelta = 2*3.1415963/(componentCount-1);
outValue[componentCount-1] = 1; //Set the alpha value to 1
for(int n=0;n<componentCount-1;++n)
{
outValue[n] = sin(phaseDelta*n+3.0*(*in));
}
}
定义此函数后,您需要将其打包到 CGFunctionref
结构中。您可以使用 CGFunctionCreate
函数来完成此操作。在以下代码中,我初始化了一些变量以用作 CGFunctionCreate
的参数,并传递指向我的函数的指针。最终结果存储在 myFunctionRef
中。
static const float inputRange[] = {0,1};
static const float outputRange[] = {0,1, 0,1, 0,1, 0,1 };
static const CGFunctionCallbacks callback = { 0, &myCGFunction, NULL};
// The total number of components needed is going to be one greater than
// the number of components in the selected color space.
int colorComponentCount = 1 + CGColorSpaceGetNumberOfComponents(myColorspace);
CGFunctionRef myFunctionRef =
//I'm passing the number of components as the option value
CGFunctionCreate((void*)colorComponentCount,
1,//The callback function takes one value for its input
inputRange, //The range of the values for
colorComponentCount,
outputRange,
&callback
);
有了 CGFunctionRef
对象,您可以使用 CGShadingCreateAxial
或 CGShadingCreateRadial
创建相应的 CGSharingRef
结构。然后使用 CGContextDrawShading
渲染渐变。
CGShadingRef myShading = CGShadingCreateAxial(myColorSpace,
startPoint, endPoint, myFunctionRef, false, false);
CGContextDrawShading(context, myShading);
模式
模式是一组图形操作,在表面上重复进行。Quartz 2D 会将区域划分为单元格的子区域,并使用您程序中定义的 callback 函数来渲染每个单元格。单元格大小均匀,并且单元格中的每行和每列之间可能存在一些间距(间距大小由您决定)。模式有两种类型:颜色模式和模板模式。模板模式就像蒙版;它们本身没有颜色,但可以应用于颜色。将它们想象成橡皮图章;您可以将任何颜色的墨水应用于图章,图章本身没有固有颜色。一旦定义了模式,它的使用方式与使用纯色大致相同。
首先,您需要定义一个渲染模式的函数。与着色函数(在渐变部分讨论)非常相似,第一个参数将是您定义的数据。下一个参数是您的模式要渲染的上下文。函数的原型定义如下
typedef void (*CGPatternDrawPatternCallback) (
void *info,
CGContextRef context
);
使用模式时,必须设置颜色空间。这通过 CGContextSetFillColorSpace
函数完成。除了上下文之外,此函数还接受一个 CGColorSpaceRef
对象。您可以使用 CGContextSetFillColorSpace
创建此对象,并将其唯一的参数设置为 NULL
。设置颜色空间后,可以通过调用 CGColorSpaceRelease
释放它。
void SetPatternColorSpace(CGContextRef context)
{
CGColorSpaceRef myColorSpace = CGContextSetFillColorSpace(NULL);
CGContextSetFillColorSpace(context, myColorSpace);
CGColorSpaceRelease(myColorSpace);
}
创建模式的函数接受许多参数。让我们来看看函数的原型,然后逐一解释每个参数的含义
CGPatternRef CGPatternCreate ( void *info,
CGRect bounds,
CGAffineTransform matrix,
float xStep,
float yStep,
CGPatternTiling tiling,
int isColored,
const CGPatternCallbacks *callbacks );
一如既往,info
参数包含您想传递给回调的数据。bounds
参数包含模式中一个单元格的大小。matrix
参数包含要应用于模式的变换矩阵。这可以用于缩放或旋转模式等操作。xStep
和 yStep
参数包含模式单元格之间水平和垂直间距的大小。平铺参数可以有三个值之一。
kCGPatternTilingNoDistortion
- 渲染时模式不失真,间距最多可变化一个设备像素。kCGPatternTilingConstantSpacingMinimalDistortion
- 模式最多可能失真一个设备像素。kCGPatternTilingConstantSpacing
- 模式可能会失真以提高效率。
如果模式是颜色模式,则 isColored
设置为 true
,如果是模板模式,则设置为 false
。最后一个参数是 CGPatternCallbacks
结构体。该结构体定义如下
struct CGPatternCallbacks
{
unsigned int version;
CGPatternDrawPatternCallback drawPattern;
CGPatternReleaseInfoCallback releaseInfo;
};
version
字段应设置为 0。drawPattern
是指向渲染函数的指针。如果您的模式渲染完成后需要进行任何清理(释放内存),则指向清理函数的指针将放在 releaseInfo
中。否则,此参数应为 NULL
。在我的示例中,我正在创建一个由正方形内的圆形组成的简单模式。我将模式的大小作为 info
参数传递。
svoid MyPatternFunction(void* info, CGContextRef context)
{
CGRect* patternBoundaries = (CGRect*)info;
float myFillColor[] = {1,0,0,1}; //red;
CGContextSaveGState(context);
CGContextSetRGBFillColor(context, 0,1,1,1);
CGContextFillRect(context, *patternBoundaries);
CGContextSetFillColor(context, myFillColor);
CGContextFillEllipseInRect(context, *patternBoundaries);
CGContextFillPath(context);
CGContextRestoreGState(context);
}
将其全部归结为一个工作示例。我创建了一个名为 PaintMyPattern(CGContextRef, CGRect)
的函数,它接受要进行渲染的内容和要渲染的矩形区域。该函数及其所依赖的函数如下所示
//Forward declaration for top of implementation file. May not be necessary
//depending on where in your file that you past these functions
void MyPatternFunction(void* info, CGContextRef context);
void PaintMyPattern(CGContextRef context, CGRect targetRect);
void SetPatternColorSpace(CGContextRef context);
//The function definitions
void PaintMyPattern(CGContextRef context, CGRect targetRect)
{
CGPatternCallbacks callbacks = { 0, &MyPatternFunction, NULL };
CGContextSaveGState(context);
CGPatternRef myPattern;
SetPatternColorSpace(context);
CGRect patternRect = CGRectMake(0,0,32,32);
myPattern = CGPatternCreate((void*)&patternRect,
targetRect,
CGAffineTransformMake(1, 0, 0, 1, 0, 0),
32,
32,
kCGPatternTilingConstantSpacing,
true,
&callbacks
);
float alpha = 1;
CGContextSetFillPattern(context, myPattern, &alpha);
CGPatternRelease(myPattern);
CGContextFillRect(context, targetRect);
CGContextRestoreGState(context);
}
void SetPatternColorSpace(CGContextRef context)
{
CGColorSpaceRef myColorSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace(context, myColorSpace);
CGColorSpaceRelease(myColorSpace);
}
void MyPatternFunction(void* info, CGContextRef context)
{
CGRect* patternBoundaries = (CGRect*)info;
float myFillColor[] = {1,0,0,1}; //red;
CGContextSaveGState(context);
CGContextSetRGBFillColor(context, 0,1,1,1);
CGContextFillRect(context, *patternBoundaries);
CGContextSetFillColor(context, myFillColor);
CGContextFillEllipseInRect(context, *patternBoundaries);
CGContextFillPath(context);
CGContextRestoreGState(context);
}
融会贯通
作为最后一个示例,我将重制我很久以前在 Zune HD 上制作的一个程序(该程序也出现在 CodeProject.com 上)。该程序是一个简单的气泡水平仪。我希望该程序的界面与 Zune HD 上的界面几乎相同。然而,与我的 Zune HD 不同,我希望在不使用任何图形资产的情况下渲染所有界面。因此,所有界面都将使用 Core Graphics 调用来渲染渐变和图案。
粗略一看,您会发现我需要渲染许多东西。一个垂直和水平的水平仪,以及一个位于中心的圆形水平仪。我可以使用相同的代码渲染垂直和水平水平仪。它只需要旋转方向即可。因此,将此程序的渲染分解后,将产生三个渲染代码块:一个用于背景,一个用于垂直/水平水平仪,一个用于气泡水平仪。
在我开始渲染之前,我想计算每个屏幕元素的位置。布局实际上是围绕一个正方形屏幕设计的,并旨在水平或垂直拉伸。目前还没有带正方形屏幕的 iOS 设备,但通过这种方式,UI 似乎能够很好地适应纵向和横向模式(这是我从 Windows Mobile 开发中学到的一个习惯)。对于不存在的正方形屏幕,我希望垂直和水平水平仪占据可用水平空间的四分之一和垂直空间的四分之一。圆形水平仪将占用剩余空间中心的方形区域。为了保持这些位置,我创建了三个名为 verticalLevelPosition
、horizontalLevelPosition
和 circularLevelPosition
的成员 CGRect
元素。我的所有计算都在一个名为 -(void)updateElementPositioning
的方法中完成。
-(void)updateElementPositioning
{
float barWidth;
float circleWidth;
CGRect viewRect = self.bounds;
barWidth = MIN(viewRect.size.width, viewRect.size.height)/4;
circleWidth = barWidth*3;
verticalLevelPosition.size.width=barWidth;
verticalLevelPosition.size.height=viewRect.size.height-barWidth;
verticalLevelPosition.origin.y=barWidth;
verticalLevelPosition.origin.x=0;
horizontalLevelPosition.size.height=barWidth;
horizontalLevelPosition.size.width = viewRect.size.width;
horizontalLevelPosition.origin.x=0;
horizontalLevelPosition.origin.y=0;
circularLevelPosition.size.width =
circularLevelPosition.size.height = circleWidth;
circularLevelPosition.origin.x =
verticalLevelPosition.size.width+verticalLevelPosition.origin.x+
((viewRect.size.width - verticalLevelPosition.size.width-circleWidth)/2);
circularLevelPosition.origin.y =
horizontalLevelPosition.size.height+horizontalLevelPosition.origin.y+
((viewRect.size.height-horizontalLevelPosition.size.height-circleWidth)/2);
}
为了确保我的计算是正确的,我实现了一个 -(void)drawRect:
方法,该方法只是用颜色填充这些矩形,以便我可以看到它们的定位方式。结果与我需要的一样。
-(void)drawRect:(CGRect)rect
{
float verticalRectColor[] = {1,0,0,1};
float horizontalRectColor[] = {0,1,0,1};
float circularRectColor[] = {0,0,1,1};
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColor(context,verticalRectColor );
CGContextFillRect(context, verticalLevelPosition);
CGContextSetFillColor(context, horizontalRectColor);
CGContextFillRect(context, horizontalLevelPosition);
CGContextSetFillColor(context, circularRectColor);
CGContextFillRect(context, circularLevelPosition);
}
如果你是一个 Apple 纯粹主义者,并且认为所有 Apple 开发都应该使用 Apple 软件完成,那么你可能会不同意我接下来要做的事情,因为我将在一台 Windows 系统上使用 Microsoft 软件。对于接下来的步骤,你可以使用你拥有的任何矢量编辑软件,因为这只是为了构思我需要做什么。下一步生成的文件不会被任何东西消耗。
我已经启动了 Microsoft Expressions Design,这样我就可以用它来勾勒我正在组装的界面。对于您在矢量编辑程序中执行的许多操作,您会发现将大多数操作转换为几个 API 调用很容易。玩了一会儿,我想出了以下设计。它由五个同心圆组成;三个带有线性渐变,一个带有径向渐变,一个带有纯色填充。最外面的圆圈有一个直径,里面有一个直径略小的另一个圆圈。剩下的三个圆圈直径相同(略小于第二个圆圈的直径)。
我正在创建一个新方法来渲染圆形水平仪。该函数需要渲染上下文、水平仪边界矩形以及圆形水平仪周围的边距。目前,我只想确保我正确计算了绑定矩形。
-(void) drawCircularLevel:(CGContextRef)context :(CGRect)rect :(float) circleMargin
{
CGRect outerCircle;
CGRect middleCircle;
CGRect innerCircle;
float innerCircleColor[] = {1,0,0,0.7};
float middleCircleColor[] = {0, 1, 0, 0.7};
float outerCircleColor[] = {0, 0, 1, 0.7};
const float middleCircleFactor = 0.95;
const float innerCircleFactor = 0.90;
//calculate the rectangle binding the outer circle.
outerCircle = rect;
outerCircle.origin.x+=(outerCircle.size.width*circleMargin)/2;
outerCircle.origin.y+=(outerCircle.size.height*circleMargin)/2;
outerCircle.size.width*=(1-circleMargin);
outerCircle.size.height*=(1-circleMargin);
//calculate the rectangle binding the midle circle
middleCircle = outerCircle;
middleCircle.origin.x+=(outerCircle.size.width*(1-middleCircleFactor)/2);
middleCircle.origin.y+=(outerCircle.size.height*(1-middleCircleFactor)/2);
middleCircle.size.width=outerCircle.size.width*middleCircleFactor;
middleCircle.size.height=outerCircle.size.height*middleCircleFactor;
innerCircle = outerCircle;
innerCircle.origin.x+=(innerCircle.size.width*(1-innerCircleFactor)/2);
innerCircle.origin.y+=(innerCircle.size.height*(1-innerCircleFactor)/2);
innerCircle.size.width*=innerCircleFactor;
innerCircle.size.height*=innerCircleFactor;
CGContextSetFillColor(context, outerCircleColor);
CGContextFillEllipseInRect(context, outerCircle);
CGContextSetFillColor(context, middleCircleColor);
CGContextFillEllipseInRect(context, middleCircle);
CGContextSetFillColor(context, innerCircleColor);
CGContextFillEllipseInRect(context, innerCircle);
}
位置很好。现在我需要创建我的渐变。Microsoft Expressions Design 以 AARRGGBB 格式表示颜色,其中每对字母都是一个介于 00 和 FF 之间的十六进制数字,表示颜色分量的强度。iOS 接受浮点值表示的颜色分量。因此,要将这些颜色中的每一个转换为浮点值,我必须将其除以 255。我使用的第一个渐变有 4 个颜色点。
颜色位置 | 十六进制颜色 | CGFloat 值 |
---|---|---|
0.000 | 0xFF949494 | { 0.148, 0.148, 0.148, 1.000 } |
0.071 | 0xFF000000 | { 0.000, 0.000, 0.000, 1.000 } |
0.079 | 0xFF000000 | { 0.000, 0.000, 0.000, 1.000 } |
0.071 | 0xFF000000 | { 0.000, 0.000, 0.000, 1.000 } |
创建渐变并将其应用于渲染的圆形后,我得到了一个与 Expressions Design 中几乎相同的结果。
我喜欢我在圆形水平仪上得到的结果,并继续制作垂直和水平水平仪。我希望水平仪的两端比中间部分略暗。为了实现这一点,我在水平和垂直水平仪周围设置了一个剪裁区域,并在两端渲染了一个渐变圆形。
CGContextSaveGState(context);
CGContextBeginPath(context);
CGContextAddRect(context, targetRect);
CGContextClip(context);
CGContextSetFillColor(context, levelBackgroundColor);
CGContextFillRect(context, targetRect);
CGContextSetFillColor(context, levelReflectionColor);
CGContextFillRect(context, reflectionRect);
CGContextDrawRadialGradient(context, shadingGradient,
gradientCenter1, 0, gradientCenter1, shadingRadius, 0);
CGContextDrawRadialGradient(context, shadingGradient,
gradientCenter2, 0, gradientCenter2, shadingRadius, 0);
我还没有渲染水平仪中的气泡。在渲染气泡之前,我需要首先决定它们应该放置在哪里。气泡的放置将取决于加速度计的读数。(如果您不熟悉如何使用加速度计,请参阅我的第一篇文章。)一旦我获得了加速度计读数,我就可以使用我在 Zune HD 加速度计中使用的相同数学方法。设备倾斜的角度可以使用 atan2
函数计算,倾斜的幅度可以使用勾股定理计算。幅度可以在 0 到 1 之间(包括)。实际上,如果一个人用力摇晃他们的设备,读数可能会超过 1。我通过使用 MIN
函数将幅度限制为最大值 1。我给 MIN
函数常量值 1 和勾股定理的结果。一旦勾股定理的结果超过 1,MIN
值将只返回 1,因为它是它拥有的两个值中较小的一个。我还有一个名为 levelPosition
的 CGPoint
对象,它将包含水平和垂直水平仪的气泡的相对位置,分别在它的 x
和 y
成员中。这些计算是在我为处理加速度计消息而创建的函数中完成的。计算完成后,代码调用 [self setNeedsDisplay];
以通知系统我们需要重绘屏幕。
-(void)accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
tiltDirection = atan2(acceleration.y, acceleration.x);
tiltMagnitude = MIN(1, sqrt( acceleration.x*acceleration.x+
acceleration.y*acceleration.y));
levelPosition.y = sin(tiltDirection)*tiltMagnitude;
levelPosition.x = -cos(tiltDirection)*tiltMagnitude;
[self setNeedsDisplay];
}
气泡本身只是填充了渐变的椭圆。完成的水平仪看起来如下
由于此程序使用加速度计,您需要将其部署到真实设备上才能看到它工作。但是当您运行程序时,虽然您会得到视觉结果,但有些东西明显是错误的。此程序的 Zune HD 版本运行流畅(在此处查看视频这里)。但此版本的程序运行不那么流畅。我们如何解决这个问题?我将其留到本文的下一部分,关于使用 Core Animation 功能。
下一步
正如您可能已经从上一节的结尾方式中得出的那样,Core Animation 将是我接下来要研究的一组图形 API。一如既往,请在下面的评论区留下您的想法、请求或任何您认为需要的更正。
历史
- 2010 年 7 月 13 日 - 初次发布。
- 2010 年 7 月 19 日 - 添加了额外的评论,进一步说明这些 API 适用于 iPod Touch、iPad 和 iPhone。