Touch Events and UIResponder
Touch Events
做为UIResponder的子类,UIView可以重写以下四个触摸事件:
Event | Description |
---|---|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event |
手指触摸到屏幕 |
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event |
手指在屏幕上移动,当手指在屏幕上移动,这个消息被重复发送 |
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event |
手指离开屏幕 |
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event |
系统事件,比如正在触摸时来电话了,触摸被取消 |
当一个手指触摸屏幕,一个UITouch实例被创建,被触摸的view收到touchesBegan:withEvent:消息,UITouch实例在NSSet集合中。
当手指在屏幕上移动,UITouch实例被更新,包含了手指当前的位置信息,然后同一个view(和触摸事件开始的view)收到touchesMoved:withEvent:消息,此时的NSSet中包含同一个UITouch实例(和手指刚触摸屏幕时的UITouch是一个实例,只不过有了最新的位置信息)。
当手指离开屏幕,UITouch实例被更新为手指最后的位置,然后同一个view(和触摸开始时的view是一个)收到touchesEnded:withEvent:消息,当此方法执行结束,UITouch实例被销毁。
得出以下结论:
- 一个UITouch实例对应触摸在屏幕上的一根手指,只要手指还触摸在屏幕上,这个实例就存在,并且包含手指当前的位置信息。
- 触摸开始时被触摸的view,会收到所有的触摸事件,如果手指触摸到了这个view's frame的外面,那么这个view仍然会收到touchesMoved:withEvent:和touchesEnded:withEvent:消息。
- 你不必去保持一个UITouch对象的引用,当UITouch状态发生变成,应用会让你访问到此对象。
每当触摸事件发生,会被添加到一个由UIApplication维护的队列中,实际上这个队列很少被充满,事件通常被立即触发。
Handling multiple touches
You can simulate multiple fingers on the simulator by holding down the option key as you drag.
#import "BKDrawView.h"
#import "BKLine.h"
@interface BKDrawView ()
//@property (nonatomic, strong) BKLine *currentLine;
// 保存当前的多个line
@property (nonatomic, strong) NSMutableDictionary *linesInProgress;
@property (nonatomic, strong) NSMutableArray *finishedLines;
@end
@implementation BKDrawView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.linesInProgress = [[NSMutableDictionary alloc] init];
self.finishedLines = [[NSMutableArray alloc] init];
self.backgroundColor = [UIColor grayColor];
// 启用multiple touches
self.multipleTouchEnabled = YES;
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%@", NSStringFromSelector(_cmd));
for (UITouch *t in touches) {
CGPoint location = [t locationInView:self];
BKLine *line = [[BKLine alloc] init];
line.begin = location;
line.end = location;
// key为UITouch对象的内存地址
NSValue *key = [NSValue valueWithNonretainedObject:t];
self.linesInProgress[key] = line;
}
// UITouch *t = [touches anyObject];
// CGPoint location = [t locationInView:self];
//
// self.currentLine = [[BKLine alloc] init];
// self.currentLine.begin = location;
// self.currentLine.end = location;
[self setNeedsDisplay];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%@", NSStringFromSelector(_cmd));
for (UITouch *t in touches) {
NSValue *key = [NSValue valueWithNonretainedObject:t];
BKLine *line = self.linesInProgress[key];
line.end = [t locationInView:self];
}
// UITouch *t = [touches anyObject];
//
// CGPoint location = [t locationInView:self];
// self.currentLine.end = location;
[self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%@", NSStringFromSelector(_cmd));
for (UITouch *t in touches) {
NSValue *key = [NSValue valueWithNonretainedObject:t];
BKLine *line = self.linesInProgress[key];
[self.finishedLines addObject:line];
[self.linesInProgress removeObjectForKey:key];
}
// [self.finishedLines addObject:self.currentLine];
// self.currentLine = nil;
[self setNeedsDisplay];
}
- (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];
}
- (void)strokeLine:(BKLine *)line{
UIBezierPath *bp = [UIBezierPath bezierPath];
bp.lineWidth = 10;
bp.lineCapStyle = kCGLineCapRound;
[bp moveToPoint:line.begin];
[bp addLineToPoint:line.end];
[bp stroke];
}
- (void)drawRect:(CGRect)rect{
[[UIColor blackColor] set];
for (BKLine *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];
// }
}
@end
Responder Chain
第七章提到过UIResponder和first responder,UIView, UIViewController, UIApplication, UIWindow都是UIResponder的子类。UIResponder可以处理触摸事件,view controller不是view,不会被直接触摸,但是通过responder chain他也可以处理触摸事件。
每个UIResponder有一个指针nextResponder,指向下一个responder,形成responder chain。
view的nextResponder通常是其view controller,如果没有view controller则是其superview。view controller的nextResponder通常是view的superview,最顶层的superview是UIWindow,window的nextResponder是单例的UIApplication对象。
如果一个responder不处理事件,则next responder会尝试处理,如果最后一个responder(application)也不处理,则事件被丢弃。
你可以明确地向next responder发送消息,现在假如next responder需要处理双击事件,代码如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
if (touch.tapCount == 2) { //双击
[[self nextResponder] touchesBegan:touches withEvent:event];
return;
}
// Go on to handle touches that are not double taps
}
UIControl
UIControl是UIButton和UISlider的父类,之前已经学过如何为其设置target 和 action,现在看下UIControl如何重写UIResponder的方法。
在UIControl中,每个可能的事件都被定义成常量,比如UIControlEventTouchUpInside事件,如果target被注册了此事件,则当此事件被触发时,target会收到action消息。
还可以让一个action处理多个event:
[rButton addTarget:tempController
action:@selector(resetTemperature:)
forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
下面为UIControl重写UIResponder的方法:
// Not the exact code. There is a bit more going on!
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// Reference to the touch that is ending
UITouch *touch = [touches anyObject];
// Location of that point in this control's coordinate system
CGPoint touchLocation = [touch locationInView:self];
// Is that point still in my viewing bounds?
if (CGRectContainsPoint(self.bounds, touchLocation))
{
// Send out action messages to all targets registered for this event!
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
} else {
// The touch ended outside the bounds, different control event
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
}
}
At the end of the UIResponder method implementations, the control sends the message sendActionsForControlEvents: to itself. This method looks at all of the target-action pairs the control has, and if any of them are registered for the control event passed as the argument, those targets are sent an action message.
However, a control never sends a message directly to its targets. Instead, it routes these messages through the UIApplication object. Why not have controls send the action messages directly to the targets? Controls can also have nil-targeted actions. If a UIControl’s target is nil, the UIApplication finds the first responder of its UIWindow and sends the action message to it.
当调用sendActionsForControlEvents:方法,会查找control的target-action,如果有action被注册为响应指定的event,那么该action被执行。
本文是对《iOS Programming The Big Nerd Ranch Guide 4th Edition》第十二章的总结。