一、UITouch詳解
1、一個手指第一次點擊屏,會形成一個UITouch對象,直到離開銷毀。
2、UITouch對象包括當前手指觸碰的屏幕位置等信息。
3、UITouch對象的TouchPhase保存當前狀態,包括開始觸碰、移動、保持、離開、被取消。
@interface UITouch : NSObject @property(nonatomic,readonly) NSTimeInterval timestamp; @property(nonatomic,readonly) UITouchPhase phase; @property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time @property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0); @property(ullable,nonatomic,readonly,strong) UIWindow *window; @property(nullable,nonatomic,readonly,strong) UIView *view; @property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2); //省略部分代碼 @end //Touch 狀態枚舉 typedef NS_ENUM(NSInteger, UITouchPhase) { UITouchPhaseBegan, // whenever a finger touches the surface. UITouchPhaseMoved, // whenever a finger moves on the surface. UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event. UITouchPhaseEnded, // whenever a finger leaves the surface. UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face) };
二、UIEvent詳解
1、觸摸事件:第一個手指開始觸摸屏幕到最后一個手指離開屏幕定義為一個觸摸事件。
2、UIEvent實際包括了多個UITouch對象。有幾個手指觸碰,就會有幾個UITouch對象。
@interface UIEvent : NSObject @property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0); @property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0); @property(nonatomic,readonly) NSTimeInterval timestamp; #if UIKIT_DEFINE_AS_PROPERTIES @property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; //省略部分代碼 @end
typedef NS_ENUM(NSInteger, UIEventType) { UIEventTypeTouches, // 觸摸事件 UIEventTypeMotion, // 加速計事件 UIEventTypeRemoteControl, // 遠程事件 UIEventTypePresses , // 物理按壓事件 }; typedef NS_ENUM(NSInteger, UIEventSubtype) { // available in iPhone OS 3.0 UIEventSubtypeNone = 0, // for UIEventTypeMotion, available in iPhone OS 3.0 UIEventSubtypeMotionShake = 1, // for UIEventTypeRemoteControl, available in iOS 4.0 UIEventSubtypeRemoteControlPlay = 100, UIEventSubtypeRemoteControlPause = 101, UIEventSubtypeRemoteControlStop = 102, UIEventSubtypeRemoteControlTogglePlayPause = 103, UIEventSubtypeRemoteControlNextTrack = 104, UIEventSubtypeRemoteControlPreviousTrack = 105, UIEventSubtypeRemoteControlBeginSeekingBackward = 106, UIEventSubtypeRemoteControlEndSeekingBackward = 107, UIEventSubtypeRemoteControlBeginSeekingForward = 108, UIEventSubtypeRemoteControlEndSeekingForward = 109, };
UIEvent中包含若干UITouch,當某個UITouch對象的phase狀態發生變化,系統會產生一條TouchMessage,繼而傳遞和派發Touch message。
也就是說每次用戶手指的移動和變化,UITouch都會形成狀態改變,系統便會產生TouchMessage。一次觸摸事件是由一組UITouch對象狀態變化引起的一組Touch message的傳遞和轉發。
三、UIResponder詳解
1、UIResponder類可以接收並處理事件。
2、UIApplication、AppDelegate、UIViewController、UIView均繼承UIResponder類。
3、UIResponder類的nextResponser。
Return value:UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t);UIViewController implements the method by returning its view’s superview; UIWindow returns the application object, and UIApplication returns nil.
1)UIView的nextResponder,如果存在管理的UIViewController對象,返回UIViewController對象,如果沒有,返回父視圖;
2)UIViewController的nextResponder是self.view;
3)UIWindow的nextResponder是application對象;
4)Application的nextResponder是nil;
@interface UIResponder : NSObject <UIResponderStandardEditActions> //觸摸事件 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1); //物理按鈕,遙控器上面的按鈕在按壓狀態等狀態下的回調 - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); - (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); - (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); //設備的陀螺儀和加速傳感器使用 - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); // 省略部分代碼 @end
四、手勢UIGestureRecognizer
為沒有繼承UIControl的視圖對象添加響應事件
1、UIGestureRecognizer類包含UIResponder類中的以下方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
2、手勢狀態
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) { //未知狀態 UIGestureRecognizerStatePossible, // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state //首次識別狀態,對於連續手勢,例如長按,有這種狀態 UIGestureRecognizerStateBegan, // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop //再次識別,當手連續手勢識別之后,再次受到touch事件 UIGestureRecognizerStateChanged, // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop //識別完成,受到touchend 消息之后 UIGestureRecognizerStateEnded, // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible //取消識別 UIGestureRecognizerStateCancelled, // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible //識別失敗 UIGestureRecognizerStateFailed, // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible // Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled //識別狀態,與識別結束一個意思 UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible };
結合圖我們來看手勢的整個遷移過程,先明確幾個信息
1)手勢的狀態遷移,前提是收到Touch message,才能做狀態變化處理代碼。
2)手勢分為連續狀態手勢、不連續狀態手勢。連續手勢有長按,慢滑等。不連續手勢有單擊,雙擊等等。
3)當用戶沒有點擊屏幕,所有手勢都處於Possiable初始狀態。
當用戶點擊屏幕,手勢會收到Touch Began Message, 手勢的touchBegan方法會被調用,手勢開始記錄點擊位置和時間,仍處於Possiable狀態。
如果用戶按住不放,間隔超過一定時間,單擊手勢會變化為Failed狀態,並在下個一runloop變為possiable。
如果時間大於長按手勢設定時間,長按手勢就會變化為Began狀態,當用戶移動手指,長按手勢的touch move方法被調用,長按手勢將自己狀態設置為Changed狀態,並且也會回調處理方法。最后手指離開,系統調用長按手勢touchEnd方法,手勢狀態設置為Recognized狀態。
3、混合手勢處理
1)當給UIView添加多個UIGestureRecognizer對象時,默認只有1個生效。如果想全部都生效,讓協議中的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法返回YES。
2)同時添加單擊和雙擊,也均允許生效。問題來了,那雙擊屏幕時,默認觸發1次單擊事件和1次雙擊事件。但這不是想要的效果,如何實現雙擊時,只觸發雙擊手勢呢,單擊時只觸發單擊手勢呢?解決方案是讓協議中的gestureRecognizer:shouldRequireFailureOfGestureRecognizer:方法、
gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:方法都返回YES。
4、UIGestureRecognizerDelegate協議
@protocol UIGestureRecognizerDelegate <NSObject> @optional // 手勢狀態是否允許更改,默認為YES。 // 如果實現中返回NO,那么手勢最后都為失敗狀態。 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer; // 允許多個手勢生效,默認為NO。 // 如果實現中返回YES,同時添加單擊和雙擊手勢,雙擊屏幕時,同時產生1次單擊事件和1次雙擊事件 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer; // 以下2個方法,為手勢之間添加依賴,默認NO。 // 比如單擊和雙擊,如果雙擊手勢識別失敗,轉換為識別單擊手勢 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0); - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0); // 手勢是否關注UITouch、UIPress對象狀態變化,和gestureRecognizerShouldBegin:效果類似,默認為YES。 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch; - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press; @end
4、蘋果封裝以下幾個手勢
UITapGestureRecognizer
UIPinchGestureRecognizer
UIRotationGestureRecognizer
UISwipeGestureRecognizer
UIPanGestureRecognizer
UIScreenEdgePanGestureRecognizer
UILongPressGestureRecognizer
五、響應鏈
響應鏈由UIResponder對象為node,形成的一個鏈表狀結構,通過UIResonder的nextResonder鏈接。
下圖中節點關系是箭頭方向朝上,也就是subView指向superView。
六、iOS觸摸事件詳解
事件傳遞是在響應鏈中查找hitTestView的過程,從父View到子View。事件轉發是響應消息中觸發的,從子View通過nextResponder在響應鏈中回溯。
1、事件傳遞
1)找到設備中的Application。
觸摸屏幕時,由iOS系統的硬件進程獲取,簡單封裝事件后暫存在系統中,利用端口實現與Application進程完成通信,將事件傳遞給Application進程。
當應用程序啟動時,主線程的RunLoop會注冊一個基於端口的source,當接收到相關事件時,主線程會被喚醒執行觸摸事件。
2)通過響應鏈找到最終處理事件的hitTestView。
當Application接收到新的事件時,開始尋找響應鏈中的hitTestView。
將所有的顯示在屏幕上的 "UIWindow對象",按照層級結構從上到下排列成一個數組。從第一個UIWindow對象開始,先判斷UIWindow是否不隱藏且可見度大於0.01且可交互,再判斷點擊位置在不在這個UIWindow內。
如果不在 ,返回nil, 就換下一個UIWindow;
如果在的話,並且UIWindow沒有subView就返回自己,但如果UIWindow有subViews,就遞歸遍歷整個subViews,直到找到hitTestView。
如果沒有找到到就不做傳遞。
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event { //判斷該視圖是否滿足響應事件的條件 if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) { //判斷點擊位置是否在自己區域內部 if ([self pointInside: point withEvent:event]) { UIView *attachedView; // 遍歷子視圖 for (int i = self.subviews.count - 1; i >= 0; i--) { UIView *view = self.subviews[i]; // 對子view遞歸調用本方法 attachedView = [view hitTest:point withEvent:event]; if (attachedView) break; } if (attachedView) { return attachedView; } else { return self; } } } return nil; }
2、事件轉發
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { //do someThiing [self.nextResponser touchesBegan: touches withEvent:event]; }
3、事件響應
當鎖定hitTestView后,當觸摸狀態發生變化,會不停的收到UITouch Message消息,調用hitTestView從UIResponder類繼承的方法。
// 點擊剛開始,回調這個方法 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 點擊之后移動,回調這個方法 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 手指移開、點擊結束,回調這個方法 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 事件被手勢識別,回調這個方法 - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
七、Touch Event、UIGestureRecognizer、響應鏈之間的關系
1、關聯詳解:
第一步:系統會將所有的Touch message優先發送給關聯在響應鏈上的全部手勢。手勢根據Touch序列消息和手勢基本規則更改自己的狀態(有的可能失敗,有的可能識別等等)。如果某個手勢對Touch message成功攔截(被攔截時,系統不會將Touch message 發送給響應鏈頂部響應者),頂部視圖控件調用touchesCancelled:withEvent方法,否則系統會進入第二步。
第二步:系統將Touch message發送給響應鏈頂部的視圖控件,頂部視圖控件這個時候就會調用Touch相關的四個方法中的某一個。
2、攔截,
舉例說明:
- (void)viewDidLoad { [super viewDidLoad]; UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleClick)]; gesture.numberOfTapsRequired = 2; self.view.userInteractionEnabled = YES; [self.view addGestureRecognizer:gesture]; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"touchesBegan"); } -(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"touchesEnded"); } -(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"touchesMoved"); } -(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"touchesCancelled"); } -(void)doubleClick { NSLog(@"雙擊拉"); }
結果分析:
1)如果單擊屏幕(攔截失敗),打印
2017-07-08 23:19:12.025 TestForMoreGesture[26097:18114462] touchesBegan
2017-07-08 23:19:12.378 TestForMoreGesture[26097:18114462] touchesEnded
2)如果雙擊屏幕(攔截成功),打印
2017-07-08 23:22:31.944 TestForMoreGesture[26097:18114462] touchesBegan
2017-07-08 23:22:32.122 TestForMoreGesture[26097:18114462] 雙擊拉
2017-07-08 23:22:32.124 TestForMoreGesture[26097:18114462] touchesCancelled
手勢是否攔截該Touch Message,主要由UIGestureRecognizer類的三個屬性控制。
// 默認為YES,表明當手勢成功識別事件后,系統會將Touch cancel消息發送給hitTestView ,並調用hitTestView的TouchCancel。設置為NO,不會再收到TouchCancel @property(nonatomic) BOOL cancelsTouchesInView; // 默認為YES, 表明無論什么情況下,不會攔截Touch began消息。如果設置為NO,只要有一個手勢不識別失敗,都不會發送Touch began到響應鏈的第一響應者。 @property(nonatomic) BOOL delaysTouchesBegan; // 默認為NO, 和delaysTouchesBegan類似,不過它是用來控制TouchEnd message的攔截 @property(nonatomic) BOOL delaysTouchesEnded;
總結
iOS整個事件處理的過程就是這樣,系統為完成整個交互做了很多東西,核心點如下:
1、事件分發過程分為:1.事件消息傳遞;2.事件消息分發。
2、響應網是事件響應的基礎,響應鏈是事件響應的具體路徑。
3、事件消息分發優先發送給手勢集合,手勢內部會做沖突處理,過濾消息。不被過濾的消息會傳遞給響應鏈對象。