首页 > 代码库 > iOS 触摸事件与UIResponder(内容根据iOS编程编写)
iOS 触摸事件与UIResponder(内容根据iOS编程编写)
- 触摸事件
因为 UIView 是 UIResponder 的子类,所以覆盖以下四个方法就可以处理四种不同的触摸事件:
1. 一根手指或多根手指触摸屏幕
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
2. 一根手指或多根手指在屏幕上移动(随着手指的移动,相关的对象会持续发送该消息)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
3. 一根手指或者多根手指离开屏幕
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
4. 在触摸操作正常结束前,某个系统事件(例如电话进来)打断了触摸过程
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
当系统检测到手指触摸屏幕的事件后,就会创建 UITouch 对象(一根手指的触摸事件对应一个 UITouch 对象)。发生触摸事件的 UIView 对象会收到 touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event 消息,系统传入的第一个实参 touches (NSSet 对象)会包含所有相关的 UITouch 对象。
当手指在屏幕上移动的时候,系统会更新相应的 UITouch 对象,为其重新设置对应的手指在屏幕上的位置。最初发生触摸事件的那个 UIView 对象会收到 touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event 消息,系统传入的第一个实参 touches (NSSet 对象)会包含所有相关的 UITouch 对象,而且这些 UITouch 对象都是最初发生触摸事件时创造的。
当手指离开屏幕的时候,系统会最后一个更新相应的 UITouch 对象,为其重新设置对应的手指在屏幕上的位置。接着,最初发生该触摸事件的视图会收到 touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event 消息。当收到该消息并执行完成之后,系统就会释放和当前事件有关的 UITouch 对象。
下面对 UITouch 对象和事件响应方法的工作机制做一个归纳。
1. 一个 UITouch 对象对应屏幕上的一根手指。只要手指没有离开屏幕,相应的 UITouch 对象就会一直存在。这些 UITouch 兑现都会保存对应的手指在屏幕上当前的位置。
2. 在触摸事件的持续过程中,无论发生什么,最初发生触摸事件的那个视图都会在各个阶段收到相应的触摸事件消息。即使手指在移动时离开了这个视图的frame区域,系统还是会向该视图发送 touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event 和 touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event 消息。也就是说,当某个视图发生触摸事件之后,该视图将永远“拥有”当时创建的所有 UITouch 对象。
3. 我们自己编写的代码不需要保存任何 UITouch 对象。当某个 UITouch 对象的状态发生变化时,系统会向指定的对象发送特定的时间消息,并传入发生变化的 UITouch 对象。
当应用发生某个触摸事件后(例如触摸开始、手指一动、触摸结束),系统都会将该事件添加至一个由 UIApplication 单例管理的事件队列。通常情况下,很少会出现满队列的情况,所以 UIApplication 会立刻分发队列中的事件。分发某个触摸事件时,UIApplication 会向 “拥有” 该事件的视图发送特定的 UIResponder 消息。
当多根手指在同一个视图、同一个时刻执行相同的触摸动作时,UIApplication 会用单个消、一次分发所有的 UITouch 对象。UIApplication 在发送特定的UIResponder 消息时,会传入一个 NSSet 对象,该对象将包含所有相关的 UITouch 对象(一个 UITouch 对象对应一根手指)。但是,因为 UIApplication 对 “同一时刻”的判断很严格,所以通常情况下,哪怕是一组事件都是在很短的一段时间内发生的,UIApplication 也会发送多个 UIResponder 消息,分批发送 UITouch 对象。
- 创建 JXTouchTracker 应用
首先,JXTouchTracker 需要一个能够描述线条的模型对象。创建一个新的 JXLine 子类。声明两个 CGPoint 属性。
#import <UIKit/UIKit.h>@interface JXLine : NSObject/** 开始位置 */@property (nonatomic,assign) CGPoint begin;/** 结束位置 */@property (nonatomic,assign) CGPoint end;@end
#import "JXLine.h"@implementation JXLine@end
接着,创建一个新的自定义类。
#import <UIKit/UIKit.h>@interface JXDrawView : UIView@end
#import "JXDrawView.h"@implementation JXDrawView@end
下面创建一个 UIViewController 子类,用于管理 JXDrawView 对象。
#import <UIKit/UIKit.h>@interface JXDrawViewController : UIViewController@end
#import "JXDrawViewController.h"@interface JXDrawViewController ()@end@implementation JXDrawViewController- (void)viewDidLoad { [super viewDidLoad]; }@end
接下来
#import "AppDelegate.h"#import "JXDrawViewController.h"@interface AppDelegate ()@end@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; JXDrawViewController * drawController = [[JXDrawViewController alloc] init]; self.window.rootViewController = drawController; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES;}
在 JXDrawViewController 中覆盖 loadView 方法,创建一个 JXDrawView 对象并将其赋值给 JXDrawViewController 对象的 view 属性
#import "JXDrawViewController.h"#import "JXDrawView.h"@interface JXDrawViewController ()@end@implementation JXDrawViewController- (void)loadView { self.view = [[JXDrawView alloc] initWithFrame:CGRectZero];}- (void)viewDidLoad { [super viewDidLoad]; }@end
- 实现 JXDrawView 完成绘图功能
JXDrawView 对象需要管理正在绘制的线条和绘制完成的线条。
#import "JXDrawView.h"#import "JXLine.h"@interface JXDrawView ()/** 保存当前正在绘制线条 */@property (nonatomic,strong) JXLine * currentLine;/** 保存已经绘制完成的线条 */@property (nonatomic,strong) NSMutableArray * finishedLines;@end@implementation JXDrawView- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.finishedLines = [NSMutableArray array]; self.backgroundColor = [UIColor grayColor]; } return self;}@end
接下来需要编写绘制线条的代码。
#import "JXDrawView.h"#import "JXLine.h"@interface JXDrawView ()/** 保存当前正在绘制线条 */@property (nonatomic,strong) JXLine * currentLine;/** 保存已经绘制完成的线条 */@property (nonatomic,strong) NSMutableArray * finishedLines;@end@implementation JXDrawView- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.finishedLines = [NSMutableArray array]; self.backgroundColor = [UIColor grayColor]; } return self;}- (void)strokeLine:(JXLine *)line { UIBezierPath * bp = [UIBezierPath bezierPath]; bp.lineWidth = 3; bp.lineCapStyle = kCGLineCapRound; [bp moveToPoint:line.begin]; [bp addLineToPoint:line.end]; [bp stroke];}- (void)drawRect:(CGRect)rect { // 用黑色表示已经绘制完成的线条 [[UIColor blackColor] set]; for (JXLine * line in self.finishedLines) { [self strokeLine:line]; } if (self.currentLine) { // 用红色表示当前正在绘制的线条 [[UIColor redColor] set]; [self strokeLine:self.currentLine]; }}@end
- 处理触摸事件并创建线条对象
这里我们只创建直线,所以我们需要用 JXLine 的 begin 和 end 属性来保存这两个点。当触摸事件开始时,JXDrawView 对象需要创建一个 JXLine 对象,并将其两个属性都设置为触摸发生时的位置。当触摸事件继续时,JXDrawView 对象要将 end 属性设置为手指当前位置。当触摸结束时,这个 JXLine 对象就能代表完成后的线条。
#import "JXDrawView.h"#import "JXLine.h"@interface JXDrawView ()/** 保存当前正在绘制线条 */@property (nonatomic,strong) JXLine * currentLine;/** 保存已经绘制完成的线条 */@property (nonatomic,strong) NSMutableArray * finishedLines;@end@implementation JXDrawView- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.finishedLines = [NSMutableArray array]; self.backgroundColor = [UIColor grayColor]; } return self;}- (void)strokeLine:(JXLine *)line { UIBezierPath * bp = [UIBezierPath bezierPath]; bp.lineWidth = 3; bp.lineCapStyle = kCGLineCapRound; [bp moveToPoint:line.begin]; [bp addLineToPoint:line.end]; [bp stroke];}- (void)drawRect:(CGRect)rect { // 用黑色表示已经绘制完成的线条 [[UIColor blackColor] set]; for (JXLine * line in self.finishedLines) { [self strokeLine:line]; } if (self.currentLine) { // 用红色表示当前正在绘制的线条 [[UIColor redColor] set]; [self strokeLine:self.currentLine]; }}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch * t = [touches anyObject]; // 根据触摸位置创建 JXLine 对象 CGPoint location = [t locationInView:self]; self.currentLine = [[JXLine alloc] init]; self.currentLine.begin = location; self.currentLine.end = location; [self setNeedsDisplay];}- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch * t = [touches anyObject]; CGPoint location = [t locationInView:self]; self.currentLine.end = location; [self setNeedsDisplay];}- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.finishedLines addObject:self.currentLine]; self.currentLine = nil; [self setNeedsDisplay];}@end
多点触摸
默认情况下,视图在同一时刻只能接收一个触摸事件。如果一个手指已经触发了 touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 方法,那么在手指离开前,其他触摸事件都会被忽略。
为了使 JXDrawView 同时接受多个触摸事件,我们需要额外的处理。
#import "JXDrawView.h"#import "JXLine.h"@interface JXDrawView ()/** 保存当前正在绘制线条 */@property (nonatomic,strong) JXLine * currentLine;/** 保存已经绘制完成的线条 */@property (nonatomic,strong) NSMutableArray * finishedLines;@end@implementation JXDrawView- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.finishedLines = [NSMutableArray array]; self.backgroundColor = [UIColor grayColor]; // 支持多点触摸 self.multipleTouchEnabled = YES; } return self;}- (void)strokeLine:(JXLine *)line { UIBezierPath * bp = [UIBezierPath bezierPath]; bp.lineWidth = 3; bp.lineCapStyle = kCGLineCapRound; [bp moveToPoint:line.begin]; [bp addLineToPoint:line.end]; [bp stroke];}- (void)drawRect:(CGRect)rect { // 用黑色表示已经绘制完成的线条 [[UIColor blackColor] set]; for (JXLine * line in self.finishedLines) { [self strokeLine:line]; } if (self.currentLine) { // 用红色表示当前正在绘制的线条 [[UIColor redColor] set]; [self strokeLine:self.currentLine]; }}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch * t = [touches anyObject]; // 根据触摸位置创建 JXLine 对象 CGPoint location = [t locationInView:self]; self.currentLine = [[JXLine alloc] init]; self.currentLine.begin = location; self.currentLine.end = location; [self setNeedsDisplay];}- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch * t = [touches anyObject]; CGPoint location = [t locationInView:self]; self.currentLine.end = location; [self setNeedsDisplay];}- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.finishedLines addObject:self.currentLine]; self.currentLine = nil; [self setNeedsDisplay];}@end
现在当多根手指在屏幕上触摸、移动、离开时, JXDrawView 都将会收到相应的 UIResponder 消息。但是现有代码并不能正确处理这些消息:现在我们目前的代码只能处理一个触摸消息。
之前实现的触摸方法中,代码向 NSSet 类型的一个 touches 发送了 anyObject 消息-在只能接收单点触摸的视图中, touches 在同一时刻只会包含一个触摸事件,因此 anyObject 可以正确返回唯一的触摸事件。但是在可以接收多点触摸的视图中, touches 在同一时刻可能包含一个或者多个触摸事件。
目前为止,我们的代码中只有一个 currentLine 属性用于保存正在绘制的直线。当有多个触摸事件的时候,我们可能会想多用多个属性来保存正在绘制的直线,但是这样做是绝对不可取的,因为加入我们只移动一根手指的时候,那么我们应该用哪个属性来接收呢?
所以更好的解决办法就是使用 NSMtableDictionary 对象来保存正在绘制的直线:之前触摸事件时,JXDrawView 可以根据传入的 UITouch 对象创建 JXLine 并将两者关联存储到字典中。
#import "JXDrawView.h"#import "JXLine.h"@interface JXDrawView ()/** 保存当前正在绘制线条 */@property (nonatomic,strong) JXLine * currentLine;/** 保存已经绘制完成的线条 */@property (nonatomic,strong) NSMutableArray * finishedLines;/** 保存正在绘制的多条直线 */@property (nonatomic,strong) NSMutableDictionary * linesInProgress;@end@implementation JXDrawView- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.linesInProgress = [NSMutableDictionary dictionary]; self.finishedLines = [NSMutableArray array]; self.backgroundColor = [UIColor grayColor]; // 支持多点触摸 self.multipleTouchEnabled = YES; } return self;}- (void)strokeLine:(JXLine *)line { UIBezierPath * bp = [UIBezierPath bezierPath]; bp.lineWidth = 3; bp.lineCapStyle = kCGLineCapRound; [bp moveToPoint:line.begin]; [bp addLineToPoint:line.end]; [bp stroke];}- (void)drawRect:(CGRect)rect { // 用黑色表示已经绘制完成的线条 [[UIColor blackColor] set]; for (JXLine * 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]; }}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch * t = [touches anyObject]; for (UITouch * t in touches) { CGPoint location = [t locationInView:self]; JXLine * line = [[JXLine alloc] init]; line.begin = location; line.end = location; NSValue * key = [NSValue valueWithNonretainedObject:t]; self.linesInProgress[key] = line; } // 根据触摸位置创建 JXLine 对象 CGPoint location = [t locationInView:self]; self.currentLine = [[JXLine alloc] init]; self.currentLine.begin = location; self.currentLine.end = location; [self setNeedsDisplay];}- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (UITouch * t in touches) { NSValue * key = [NSValue valueWithNonretainedObject:t]; JXLine * 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<UITouch *> *)touches withEvent:(UIEvent *)event { for (UITouch *t in touches) { NSValue * key = [NSValue valueWithNonretainedObject:t]; JXLine * line = self.linesInProgress[key]; [self.finishedLines addObject:line]; [self.linesInProgress removeObjectForKey:key]; } [self.finishedLines addObject:self.currentLine]; self.currentLine = nil; [self setNeedsDisplay];}@end
最后还需要处理触摸取消事件。如果系统中断了应用,触摸事件将会被取消。这时应该将应用恢复到触摸事件发生前的状态。对于我们的应用来说是需要将正在绘制的线条删除。
#import "JXDrawView.h"#import "JXLine.h"@interface JXDrawView ()/** 保存当前正在绘制线条 */@property (nonatomic,strong) JXLine * currentLine;/** 保存已经绘制完成的线条 */@property (nonatomic,strong) NSMutableArray * finishedLines;/** 保存正在绘制的多条直线 */@property (nonatomic,strong) NSMutableDictionary * linesInProgress;@end@implementation JXDrawView- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.linesInProgress = [NSMutableDictionary dictionary]; self.finishedLines = [NSMutableArray array]; self.backgroundColor = [UIColor grayColor]; // 支持多点触摸 self.multipleTouchEnabled = YES; } return self;}- (void)strokeLine:(JXLine *)line { UIBezierPath * bp = [UIBezierPath bezierPath]; bp.lineWidth = 3; bp.lineCapStyle = kCGLineCapRound; [bp moveToPoint:line.begin]; [bp addLineToPoint:line.end]; [bp stroke];}- (void)drawRect:(CGRect)rect { // 用黑色表示已经绘制完成的线条 [[UIColor blackColor] set]; for (JXLine * line in self.finishedLines) { [self strokeLine:line]; } // 用红色绘制正在画的线条 [[UIColor redColor] set]; for (NSValue * key in self.linesInProgress) { [self strokeLine:self.linesInProgress[key]]; } }- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (UITouch * t in touches) { CGPoint location = [t locationInView:self]; JXLine * line = [[JXLine alloc] init]; line.begin = location; line.end = location; NSValue * key = [NSValue valueWithNonretainedObject:t]; self.linesInProgress[key] = line; } [self setNeedsDisplay];}- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (UITouch * t in touches) { NSValue * key = [NSValue valueWithNonretainedObject:t]; JXLine * line = self.linesInProgress[key]; line.end = [t locationInView:self]; } [self setNeedsDisplay];}- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (UITouch *t in touches) { NSValue * key = [NSValue valueWithNonretainedObject:t]; JXLine * line = self.linesInProgress[key]; [self.finishedLines addObject:line]; [self.linesInProgress removeObjectForKey:key]; } [self setNeedsDisplay];}- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { for (UITouch *t in touches) { NSValue * key = [NSValue valueWithNonretainedObject:t]; [self.linesInProgress removeObjectForKey:key]; } [self setNeedsDisplay];}@end
- 响应链
UIResponder 对象可以接收触摸事件,而 UIView 是典型的 UIResponder 子类。除了 UIView ,还有很多其他的 UIResponder 子类,其中包括 UIViewController 、 UIApplication 、 UIWindow 。UIViewController 不是视图对象,也就是无法显示触摸,无法显示,为什么也是 UIResponder 子类呢?因为虽然不能向其直接发送触摸事件,但是该对象能够通过响应链来接收事件。
UIResponder 对象拥有一个名为 nextResponder 的指针,相关的 UIResponder 对象可以通过该指针组成一个响应链。当 UIView 对象属于某个 UIViewController 对象时,其 nextResponder 指针就会指向包含该视图的 UIViewController 对象。当 UIView 对象不属于任何 UIViewController 对象时,其 nextResponder 指针就会指向该视图的父视图。UIViewController 对象的 nextResponder 通常会指向其视图的父视图。最顶层的父视图是 UIWindow 对象,而 UIWindow 对象的 nextResponder 指向的是 UIApplication 单例。
如果 UIResponder 对爱国没有处理传给他的事件,会发生什么?该对象会将未处理的消息转发给自己的 nextResponder 。这也是 touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 这类方法的默认实现。因此,如果没有为某个 UIResponder 对象覆盖特定的事件处理方法,那么该对象的 nextResponder 会尝试处理相应的触摸事件。最终,该事件会传递给 UIApplication ,如果它也无法处理,那么系统就会丢掉该事件。
这里有一个操作技巧,获取 UIView 自定义文件的控制器
-(UIViewController *)viewcontroller{ UIResponder *next = [self nextResponder]; while (next) { if ([next isKindOfClass:[UIViewController class]]) { return (UIViewController *)next; } next = [next nextResponder]; } return nil; }
iOS 触摸事件与UIResponder(内容根据iOS编程编写)