由於 UIView 是 UIResponder 的子類,因此覆蓋如下四個方法就能夠處理四種不一樣的觸摸事件:git
1. 一根手指或多根手指觸摸屏幕github
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
2. 一根手指或多根手指在屏幕上移動(隨着手指的移動,相關的對象會持續發送該消息)app
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
3. 一根手指或者多根手指離開屏幕atom
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
4. 在觸摸操做正常結束前,某個系統事件(例如電話進來)打斷了觸摸過程spa
- (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 對象都是最初發生觸摸事件時創造的。code
當手指離開屏幕的時候,系統會最後一個更新相應的 UITouch 對象,爲其從新設置對應的手指在屏幕上的位置。接着,最初發生該觸摸事件的視圖會收到 touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event 消息。當收到該消息並執行完成以後,系統就會釋放和當前事件有關的 UITouch 對象。對象
下面對 UITouch 對象和事件響應方法的工做機制作一個概括。blog
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 須要一個可以描述線條的模型對象。建立一個新的 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 對象須要管理正在繪製的線條和繪製完成的線條。
#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; }