首页 » iOS编程(第4版) » iOS编程(第4版)全文在线阅读

《iOS编程(第4版)》4.4 在drawRect:方法中自定义绘图

关灯直达底部

前面编写了一个名为BNRHypnosisView的UIView子类,创建了两个BNRHypnosisView对象,设置了它们的frame属性和backgroundColor属性,并将这两个对象加入了Hypnosister的视图层次结构。本节开始学习如何在drawRect:方法中为BNRHypnosisView编写自定义的绘图代码。

视图根据drawRect:方法将自己绘制到图层上。UIView的子类可以覆盖drawRect:,完成自定义的绘图任务。例如,UIButton的drawRect:方法默认会在frame表示的矩形区域中心画出一行浅蓝色的文字。

覆盖drawRect:后首先应该获取视图从UIView继承而来的bounds属性,该属性定义了一个矩形范围,表示视图的绘制区域。

视图在绘制自己时,会参考一个坐标系,bounds表示的矩形位于自己的坐标系,而frame表示的矩形位于父视图的坐标系,但是两个矩形的大小是相同的。

读者可能会疑惑,在表示的矩形大小相同的情况下,为什么除了frame属性之外,还要定义bounds属性?

frame和bounds表示的矩形用法不同。frame用于确定与视图层次结构中其他视图的相对位置,从而将自己的图层与其他视图的图层正确组合成屏幕上的图像。而bounds属性用于确定绘制区域,避免将自己绘制到图层边界之外(见图4-14)。

图4-14 bounds和frame

在BNRAppDelegate.m文件中,将UIWindow对象的bounds属性赋给firstView的frame属性,这样可以让firstView充满屏幕。

- (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

self.window = [[UIWindow alloc] initWithFrame:

 [[UIScreen mainScreen] bounds]];

// 在这里添加应用启动后的初始化代码

CGRect firstFrame = CGRectMake(160, 240, 100, 150);

CGRect firstFrame = self.window.bounds;

BNRHypnosisView *firstView = [[BNRHypnosisView alloc]

initWithFrame:firstFrame];

[self.window addSubview:firstView];

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

构建并运行应用,屏幕上会显示一个充满屏幕的红色视图。

绘制圆形

接下来在BNRHypnosisView.m的drawRect:方法中添加绘图代码,画出一个尽可能大的圆形,但是不超过视图的绘制区域。

首先需要根据视图的bounds属性找到绘制区域的中心点,代码如下:

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// 根据bounds计算中心点

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

}

然后比较视图的宽和高,将较小值的二分之一设置为圆形的半径(使用较小值可以保证无论设备处于横握还是竖握模式都能画出不超过绘制区域的圆形),代码如下:

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// 根据bounds计算中心点

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// 根据视图宽和高中的较小值计算圆形的半径

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

}

UIBezierPath

现在可以开始使用UIBezierPath类绘制圆形了。UIBezierPath用来绘制直线或曲线,从而组成各种形状,例如圆形。

首先需要创建一个UIBezierPath对象,代码如下:

- (void)drawRect:(CGRect)rect

{

...

// 根据视图宽和高中的较小值计算圆形的半径

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

UIBezierPath *path = [[UIBezierPath alloc] init];

}

接下来定义UIBezierPath对象需要绘制的路径。如何定义一个可以绘制圆形的路径?解决问题的最佳方式就是查看Apple提供的开发者文档。文档中详细介绍了UIBezierPath的使用方法。

使用开发者文档

在Xcode菜单中选择Help→Documentation and API Reference(文档和API参考手册)。也可以使用键盘快捷键Option-Command-?(其实还需要按住Shift,以便选择“?”)。

(查看文档时,Xcode可能会从Apple服务器上获取最新内容,有时会要求读者输入Apple ID和密码。)

这时Xcode会打开文档浏览器,在搜索框中输入UIBezierPath,然后在搜索结果中选择UIBezierPath Class Reference(UIBezierPath参考手册),如图4-15所示。

图4-15 在文档中搜索UIBezierPath

UIBezierPath参考手册打开时会显示类的概览页面,但是关于定义绘制路径的内容位于Task部分。Task部分位于参考手册左边的内容列表中,(如果没有看到内容列表,请点击浏览器左上角的按钮。)可以根据需要实现的功能在Task部分查找对应的方法。

第一个Task是Creating a UIBezierPath Object(创建UIBezierPath对象),之前的代码中已经完成了。第二个是Constructing a Path(构建路径),选择该Task,查看UIBezierPath中与之相关的一系列方法(见图4-16)。

图4-16 与构建路径有关的方法

请注意addArcWithCenter:radius:startAngle:endAngle:clockwise:方法,可以根据角度和半径定义弧形路径的方法定义圆形路径。点击该方法查看参数使用说明,之前已经计算出了圆形的圆心和半径,圆形的起止角度分别是0和M_PI * 2(startAngle和endAngle参数值的单位是弧度)。

(如果读者对弧度和角度的转换有疑问,可以先简单地传入这两个值,或者在方法文档中找到Discussion部分,点击Figure 1链接,查看圆形中有关弧度的示意图。)

最后,由于我们绘制的是一个整圆,顺时针还是逆时针都是可以的,因此为clockwise传入YES或NO。

在BNRHypnosisView.m中,向UIBezierPath对象发送消息,定义绘制路径:

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// 根据bounds计算中心点

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// 根据视图宽和高中的较小值计算圆形的半径

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

UIBezierPath *path = [[UIBezierPath alloc] init];

// 以中心点为圆心、radius的值为半径,定义一个0到M_PI * 2.0弧度的路径(整圆)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

}

路径已经定义好了,但是只定义路径不会进行实际的绘制。回到UIBezierPath参考手册,在Task中选择Drawing Paths(绘制路径)。在可以绘制路径的方法中,选择stroke。(其他绘制方法会为图形填充颜色或者需要其他参数,例如CGBlendMode。)

现在向UIBezierPath对象发送消息,绘制之前定义的路径,代码如下:

- (void)drawRect:(CGRect)rect

{

...

UIBezierPath *path = [[UIBezierPath alloc] init];

// 以中心点为圆心、radius的值为半径,定义一个0到M_PI * 2.0弧度的路径(整圆)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

// 绘制路径!

[path stroke];

}

构建并运行应用,可以看到一个由黑色细线构成的圆形,其直径和屏幕宽度相同(如果设备处于横握状态,那么圆形的直径和屏幕高度相同),如图4-17所示。

图4-17 BNRHypnosisView中画出了一个圆形

根据Hypnosister应用最初的设计方案,构成圆形的轮廓线应该比现在更粗,同时颜色是浅灰色而不是黑色。

为了改变线条的粗细和颜色,请回到UIBezierPath参考手册,在内容列表中找到Properties(属性)部分。在属性列表中找到lineWidth属性,该属性是CGFloat类型,默认值为1.0。

在BNRHypnosisView.m中,设置线条的宽度为10点,代码如下:

- (void)drawRect:(CGRect)rect

{

...

// 以中心点为圆心、radius的值为半径,定义一个0到M_PI * 2.0弧度的路径(整圆)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

// 设置线条宽度为10点

path.lineWidth = 10;

// 绘制路径!

[path stroke];

}

构建并运行应用,现在圆形的轮廓线应该比之前更粗。

UIBezierPath中没有设置线条颜色的属性,但是UIBezierPath参考手册中的Overview部分介绍了设置线条颜色的方法。点击内容列表中的Overview,在第五段(随着Apple修订文档可能会有变动)中有一句话:“You set the stroke and fill color using the UIColor class.(请您使用UIColor类设置线条颜色和填充颜色。)”

可以点击UIColor链接进入UIColor参考手册,在UIColor的Task部分,选择Drawing Operations,查看与绘制操作相关的方法。可以使用set或setStroke方法设置线条颜色,请读者选择用法更加明确的setStroke方法。

setStroke方法是一个实例方法,因此需要创建一个UIColor对象。前面提到过,UIColor有一系列对应于常见颜色的简便方法,可以在UIColor参考手册中的Class Methods部分找到这些方法,其中包括lightGrayColor。

现在可以设置线条颜色了,在BNRHypnosisView.m中,创建一个表示浅灰色的UIColor对象并向其发送setStroke消息,这样就可以画出一个浅灰色轮廓的圆形。

- (void)drawRect:(CGRect)rect

{

...

// 设置线条宽度为10点

path.lineWidth = 10;

// 设置绘制颜色为浅灰色

[[UIColor lightGrayColor] setStroke];

// 绘制路径!

[path stroke];

}

构建并运行应用,可以看到圆形的轮廓线已经变成浅灰色了。

读者可能已经注意到,视图的backgroundColor属性不会受drawRect:中代码的影响,通常应该将重写了drawRect:方法的视图的背景颜色设置为透明(对应于clearColor),这样可以让视图只显示drawRect:方法中绘制的内容。

在BNRAppDelegate.m中,删除设置视图背景颜色的代码:

BNRHypnosisView *firstView = [[BNRHypnosisView alloc]

initWithFrame:firstFrame];

firstView.backgroundColor = [UIColor redColor];

[self.window addSubview:view];

然后,在BNRHypnosisView.m的initWithFrame:方法中,设置BNRHypnosisView对象的背景颜色为透明:

- (instancetype)initWithFrame:(CGRect)frame

{

self = [super initWithFrame:frame];

if (self) {

// 设置BNRHypnosisView对象的背景颜色为透明

self.backgroundColor = [UIColor clearColor];

}

return self;

}

构建并运行应用,可以看到一个透明背景的圆形,如图4-18所示。

图4-18 透明背景的圆形

绘制同心圆

在BNRHypnosisView中绘制多个同心圆有两个方法,第一个方法是创建多个UIBezierPath对象,每个对象代表一个圆形;第二个方法是使用一个UIBezierPath对象绘制多个圆形,为每个圆形定义一个绘制路径。显然,第二个方法更好,只创建一个UIBezierPath对象可以减少内存占用。

首先需要确定最外层圆形的直径,然后从这个直径递减,确定其他圆形的直径。请注意直径必须大于零。

这里使用视图的对角线作为最外层圆形的直径,使最外层圆形成为视图的外接圆——只能在视图的四个角上看到该圆形的浅灰色轮廓。

在BNRHypnosisView.m中,删除绘制单个圆形的代码,改为绘制多个同心圆:

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// 根据bounds计算中心点

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// 根据视图宽和高中的较小值计算圆形的半径

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

// 使最外层圆形成为视图的外接圆

float maxRadius = hypot(bounds.size.width, bounds.size.height) / 2.0;

UIBezierPath *path = [[UIBezierPath alloc] init];

// 以中心点为圆心,radius的值为半径,

// 定义一个0到M_PI * 2.0弧度的路径(整圆)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

for (float currentRadius = maxRadius; currentRadius > 0;

currentRadius -= 20) {

[path addArcWithCenter:center

radius:currentRadius // 半径为currentRadius!

startAngle:0.0

endAngle:M_PI * 2.0

>clockwise:YES];

}

// 设置线条宽度为10点

path.lineWidth = 10;

// 设置绘制颜色为浅灰色

[[UIColor lightGrayColor] setStroke];

// 绘制路径!

[path stroke];

}

构建并运行应用,虽然BNRHypnosisView画出了一系列同心圆,但是屏幕右边多出了一条奇怪的直线,如图4-19所示。

图4-19 BNRHypnosisView绘制了不正确的同心圆

这是因为单个UIBezierPath对象将多个路径(每个路径可以画出一个圆形)连接起来,形成了一个完整的路径。可以将UIBezierPath对象想象为一支在纸上画画的铅笔——在绘制完某个圆后去绘制另一个圆时,铅笔并没有抬起,仍然停留在纸上,因此会继续留下笔迹。正确的做法是每次绘制新的圆形之前,必须抬起笔。

在drawRect:方法的for循环中,当绘制下一个圆形时,抬起笔并移动到正确的位置:

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// 根据bounds计算中心点

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// 使最外层圆形成为视图的外接圆

float maxRadius = hypot(bounds.size.width, bounds.size.height) / 2.0;

UIBezierPath *path = [[UIBezierPath alloc] init];

for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {

[path moveToPoint:CGPointMake(center.x + currentRadius, center.y)];

[path addArcWithCenter:center

radius:currentRadius // 半径为currentRadius!

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

}

// 设置线条宽度为10点

path.lineWidth = 10.0;

// 绘制路径

[path stroke];

}

构建并运行应用,现在绘制的同心圆完全正确,如图4-20所示。

图4-20 绘制同心圆

Hypnosister只使用了UIBezierPath的少数功能,读者可以查看UIBezierPath参考手册完成本章后面的练习,学习使用圆弧、直线和曲线组合起来画出更多有创意的图案。