因为通过两个点可以定义一条直线,所以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];
}