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

《iOS编程(第4版)》12.4 处理触摸事件并创建线条对象

关灯直达底部

因为通过两个点可以定义一条直线,所以BNRLine对象需要用begin属性和end属性来保存这两个点。当触摸事件开始时,BNRDrawView对象需要创建一个BNRLine对象,并将begin和end都设置为触摸发生时的手指位置。当触摸事件继续时(手指在屏幕上移动),BNRDrawView对象要将end设置为手指的当前位置。当触摸结束时,这个BNRLine对象就能代表完成后的线条。

在BNRDrawView.m中实现touchesBegan:withEvent:方法,创建BNRLine对象:

- (void)touchesBegan:(NSSet *)touches

withEvent:(UIEvent *)event

{

UITouch *t = [touches anyObject];

// 根据触摸位置创建BNRLine对象

CGPoint location = [t locationInView:self];

self.currentLine = [[BNRLine alloc] init];

self.currentLine.begin = location;

self.currentLine.end = location;

[self setNeedsDisplay];

}

接下来实现touchesMoved:withEvent:方法,获取currentLine的终点:

- (void)touchesMoved:(NSSet *)touches

withEvent:(UIEvent *)event

{

UITouch *t = [touches anyObject];

CGPoint location = [t locationInView:self];

self.currentLine.end = location;

[self setNeedsDisplay];

}

最后实现touchesEnded:withEvent:方法,将currentLine加入finishedLines:

- (void)touchesEnded:(NSSet *)touches

withEvent:(UIEvent *)event

{

[self.finishedLines addObject:self.currentLine];

self.currentLine = nil;

[self setNeedsDisplay];

}

构建并运行应用,在屏幕上绘制线条。可以发现,当手指正在屏幕上绘制时,绘制的线条是红色的;当手指离开屏幕时,线条的颜色会变为黑色。

处理多点触摸

读者可能已经注意到,如果在使用一根手指绘制的同时使用别的手指触摸屏幕,并不会同时画出多根线条。接下来将更新BNRDrawView,使TouchTracker可以处理多点触摸。

默认情况下,视图在同一时刻只能接受一个触摸事件。如果一根手指已经触发了touchesBegan:withEvent:方法,那么在手指离开屏幕之前(触发touchesEnded: withEvent:方法之前),其他触摸事件都会被忽略——对于BNRDrawView来说,“忽略”是指touchesBegan:withEvent:或其他UIResponder消息都不会再发送给BNRDrawView。

为了使BNRDrawView同时接受多个触摸事件,需要在BNRDrawView.m中添加以下代码:

- (instancetype)initWithFrame:(CGRect)r

{

self = [super initWithFrame:r];

if (self) {

self.finishedLines = [[NSMutableArray alloc] init];

self.backgroundColor = [UIColor grayColor];

self.multipleTouchEnabled = YES;

}

return self;

}

现在当多根手指在屏幕上触摸、移动、离开时,BNRDrawView都将收到相应的UIResponder消息。但是现有代码并不能正确处理这些消息:现有代码在同一时刻只能处理一个触摸消息(只能画出一根线条)。

之前实现的触摸方法中,代码向NSSet类型的touches发送了anyObject消息——在只能接受单点触摸的视图中,touches在同一时刻只会包含一个触摸事件,因此anyObject可以正确返回唯一的触摸事件。但是在可以接受多点触摸的视图中,touches在同一时刻可能包含一个或多个触摸事件,必须修改现有代码,依次处理所有触摸事件。

目前,代码中只有一个currentLine属性用于保存正在绘制的直线。读者可能会考虑为BNRDrawView添加多个属性保存多条直线,例如currentLine1和currentLine2,但是这种方法很难处理众多BNRLine属性与触摸事件之间的对应关系。假设用户用三根手指同时触摸屏幕并因此创建了三个BNRLine对象,当其中一根手指移动时就难以知道应该更新哪个BNRLine属性。

更好的解决方案是使用NSMutableDictionary对象来保存正在绘制的线条:发生触摸事件时,BNRDrawView可以根据传入的UITouch对象创建BNRLine对象并将两者关联存入NSMutableDictionary(其实并不能直接使用UITouch对象作为NSMutableDictionary的键,后面会介绍如何使用UITouch对象的内存地址存取BNRLine对象)。当BNRDrawView再次收到触摸事件时,可以根据传入的UITouch对象在NSMutableDictionary中找到并更新相应的BNRLine对象。

在BNRDrawView.m中,添加一个NSMutableDictionary类型的属性,名为linesIn- Progress,用于代替currentLine。然后在initWithFrame:中初始化linesInProgress:

@interface BNRDrawView ()

@property (nonatomic, strong) BNRLine *currentLine;

@property (nonatomic, strong) NSMutableDictionary *linesInProgress;

@property (nonatomic, strong) NSMutableArray *finishedLines;

@end

@implementation BNRDrawView

- (instancetype)initWithFrame:(CGRect)r

{

self = [super initWithFrame:r];

if (self) {

self.linesInProgress = [[NSMutableDictionary alloc] init];

self.finishedLines = [[NSMutableArray alloc] init];

self.backgroundColor = [UIColor grayColor];

self.multipleTouchEnabled = YES;

}

return self;

}

现在更新UIResponder方法,将所有正在绘制的线条加入linesInProgress。在BNRDrawView.m中更新touchesBegan:withEvent::

- (void)touchesBegan:(NSSet *)touches

withEvent:(UIEvent *)event

{

// 向控制台输出日志,查看触摸事件发生顺序

NSLog(@“%@”, NSStringFromSelector(_cmd));

for (UITouch *t in touches) {

CGPoint location = [t locationInView:self];

BNRLine *line = [[BNRLine alloc] init];

line.begin = location;

line.end = location;

NSValue *key = [NSValue valueWithNonretainedObject:t];

self.linesInProgress[key] = line;

}

UITouch *t = [touches anyObject];

CGPoint location = [t locationInView:self];

self.currentLine = [[BNRLine alloc] init];

self.currentLine.begin = location;

self.currentLine.end = location;

[self setNeedsDisplay];

}

以上代码使用快速枚举将所有已经开始的触摸事件加入linesInProgress——同时请读者注意,加入linesInProgress之前,需要使用valueWithNonretainedObject:方法将UITouch对象的内存地址封装为NSValue对象,作为BNRLine对象的键。使用内存地址分辨UITouch对象的原因是,在触摸事件开始、移动、结束的整个过程中,其内存地址是不会改变的,内存地址相同的UITouch对象一定是同一个对象。现在TouchTracker的对象图如图12-4所示。

图12-4 TouchTracker对象图

继续更新其余UIResponder方法,在touchesMoved:withEvent:中根据UITouch对象查找对应的线条:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

{

// 向控制台输出日志,查看触摸事件发生的顺序

NSLog(@“%@”, NSStringFromSelector(_cmd));

for (UITouch *t in touches) {

NSValue *key = [NSValue valueWithNonretainedObject:t];

BNRLine *line = self.linesInProgress[key];

line.end = [t locationInView:self];

}

UITouch *t = [touches anyObject];

CGPoint location = [t locationInView:self];

self.currentLine.end = location;

[self setNeedsDisplay];

}

然后在touchesEnded:withEvent:中将所有绘制完成的线条移动到_finishedLines数组中:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

{

// 向控制台输出日志,查看触摸事件发生的顺序

NSLog(@“%@”, NSStringFromSelector(_cmd));

for (UITouch *t in touches) {

NSValue *key = [NSValue valueWithNonretainedObject:t];

BNRLine *line = self.linesInProgress[key];

[self.finishedLines addObject:line];

[self.linesInProgress removeObjectForKey:key];

}

[self.finishedLines addObject:self.currentLine];

self.currentLine = nil;

[self setNeedsDisplay];

}

最后更新drawRect:方法,绘制linesInProgress中的所有线条:

// 用黑色绘制已经完成的线条

[[UIColor blackColor] set];

for (BNRLine *line in self.finishedLines) {

[self strokeLine:line];

}

// 用红色绘制正在画的线条

[[UIColor redColor] set];

for (NSValue *key in self.linesInProgress) {

[self strokeLine:self.linesInProgress[key]];

}

if (self.currentLine) {

// 用红色绘制正在画的线条

[[UIColor redColor] set];

[self strokeLine:self.currentLine];

}

}

构建并运行应用,同时使用多个手指在TouchTracker中绘制线条,检查运行结果(在模拟器中按住Option并拖曳可以模拟多点触摸)。

读者可能会问,为什么UITouch对象自身不能用作NSMutableDictionary的键?这是由于NSDictionary及其子类NSMutableDictionary的键必须遵守NSCopying协议——键必须可以复制(可以响应copy消息)。UITouch并不遵守NSCopying协议,因为每一个触摸事件都是唯一的,不应该被复制。相反,NSValue遵守NSCopying协议,同一个UITouch对象会在触摸过程中创建包含相同内存地址的NSValue对象。

读者还应该知道,当视图收到touchesMoved:withEvent:消息时,touches中只会包含正在移动的UITouch对象。也就是说,如果使用三个手指同时触摸视图,但是只移动其中一个手指,其他两个手指保持不动,那么touches中只会包含一个UITouch对象。

最后还需要处理触摸取消事件。如果系统中断了应用,触摸事件就会被取消(例如iPhone接到电话)。这时应该将应用恢复到触摸事件发生前的状态,对于TouchTracker来说,需要清除所有正在绘制的线条。

在BNRDrawView.m中实现touchesCancelled:withEvent:方法:

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

{

// 向控制台输出日志,查看触摸事件发生的顺序

NSLog(@“%@”, NSStringFromSelector(_cmd));

for (UITouch *t in touches) {

NSValue *key = [NSValue valueWithNonretainedObject:t];

[self.linesInProgress removeObjectForKey:key];

}

[self setNeedsDisplay];

}