事件傳遞和響應者鏈條
事件處理
- iOS中的
事件
可以分為3大型別
-
觸控
事件(MultiTouch events) -
加速計
事件(Motion events) -
遠端控制
事件(Remote Control events)
-
- 響應者物件(
UIResponder
)- 在iOS中不是任何物件都能處理事件,只有繼承了UIResponder的物件才能接收並處理事件,稱之為
響應者物件
- UIApplication、UIViewController、UIView都繼承自
UIResponder
,因此它們都是響應者物件,都能夠接收並處理事件
- 在iOS中不是任何物件都能處理事件,只有繼承了UIResponder的物件才能接收並處理事件,稱之為
-
UIResponder
內部提供了以下方法來處理事件,重點掌握觸控
事件
// 觸控事件(重點)
// 一根或者多根手指**開始**觸控view,系統會自動呼叫view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指在view上**移動**,系統會自動呼叫view的下面方法(隨著手指的移動,會持續呼叫該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指**離開**view,系統會自動呼叫view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 觸控結束前,某個系統事件(例如:電話呼入)會打斷觸控過程,系統會自動呼叫view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
// 加速計事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
// 遠端控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
- UITouch
- 當使用者用一根手指觸控螢幕時,會建立一個與手指相關聯的UITouch物件
- 一根手指對應一個UITouch物件
- UITouch的作用
- 儲存著跟手指相關的資訊,比如觸控的位置、時間、階段
- 當手指移動時,系統會更新
同一個
UITouch物件,使之能夠一直儲存該手指的觸控位置 - 當手指離開螢幕時,系統會銷燬相應的UITouch物件
- UITouch常用屬性及方法
// *************************常用屬性*************************** // 觸控產生時所處的視窗 @property(nonatomic,readonly,retain) UIWindow *window; // 觸控產生時所處的檢視 @property(nonatomic,readonly,retain) UIView *view; // 短時間內點按螢幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點選 @property(nonatomic,readonly) NSUInteger tapCount; // 記錄了觸控事件產生或變化時的時間,單位是秒 @property(nonatomic,readonly) NSTimeInterval timestamp; // 當前觸控事件所處的狀態 @property(nonatomic,readonly) UITouchPhase phase; /* UITouchPhase是一個列舉型別,包含: UITouchPhaseBegan(觸控開始) UITouchPhaseMoved(接觸點移動) UITouchPhaseStationary(接觸點無移動) UITouchPhaseEnded(觸控結束) UITouchPhaseCancelled(觸控取消) */ // *************************常用方法*************************** /** * 返回值表示觸控在view上的位置 * 這裡返回的位置是針對view的座標系的(以view的左上角為原點(0, 0)) * 呼叫時傳入的view引數為nil的話,返回的是觸控點在UIWindow中的位置 **/ -(CGPoint)locationInView:(UIView *)view; // 該方法記錄了前一個觸控點的位置 -(CGPoint)previousLocationInView:(UIView *)view;
- UIEvent(事件物件)
- 每產生一個事件,就會產生一個UIEvent物件
- 作用:記錄事件產生的
時刻和型別
// *************************常用屬性***************************
// 事件型別
@property(nonatomic,readonly) UIEventType type;
// 子事件型別
@property(nonatomic,readonly) UIEventSubtype subtype;
/*
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,// 觸控事件
UIEventTypeMotion, // 加速計事件
UIEventTypeRemoteControl,// 遠端控制事件
};
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,
};
*/
// 事件產生的時間
@property(nonatomic,readonly) NSTimeInterval timestamp;
// UIEvent還提供了相應的方法可以獲得在某個view上面的觸控物件(UITouch)
- (NSSet *)allTouches;
- (NSSet *)touchesForWindow:(UIWindow *)window;
- (NSSet *)touchesForView:(UIView *)view;
- (NSSet *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture
- touches和event引數
- 一次完整的觸控過程,會經歷
3個狀態
:- 觸控開始
- (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
- 觸控開始
- 4個觸控事件處理方法中,都有
NSSet *touches
和UIEvent *event
兩個引數
一次完整的觸控過程中,只會產生一個事件物件
,4個觸控方法都是同一個event引數 - 如果兩根手指
同時
觸控一個view,那麼view只會呼叫一次
touchesBegan:withEvent:方法,touches引數中裝著2個
UITouch物件 - 如果這兩根手指
一前一後
分開觸控同一個view,那麼view會分別呼叫2次
touchesBegan:withEvent:方法,並且每次呼叫時的touches引數中只包含一個
UITouch物件 - 根據touches中UITouch的個數可以判斷出是
單點觸控還是多點觸控
- 一次完整的觸控過程,會經歷
- 注意點:
- 提示:iPhone開發中,要
避免
使用雙擊事件! - 預設情況
不支援
多點觸控,需要勾選Multiple Touch
選項
- 提示:iPhone開發中,要
觸控事件處理
- 觸控
事件
處理的完整過程:- 發生觸控事件後,系統會將該事件加入到一個由UIApplication管理的
事件佇列
中 - UIApplication會從事件佇列中取出
最前面
的事件,並將事件分發下去以便處理,先傳送事件給應用程式的主視窗 - 主視窗會在檢視層次結構中找到一個
最合適
處理事件的控制元件,這也是整個事件處理過程的第一步
- 找到
最合適
處理事件的控制元件後,呼叫控制元件的touches...
方法,touches...
方法的預設做法
是將事件順著響應者鏈條
向上傳遞,將事件交給上一個響應者進行處理,直至事件處理完畢
- 發生觸控事件後,系統會將該事件加入到一個由UIApplication管理的
- 觸控事件傳遞
- 觸控
事件
的傳遞是從父控制元件傳遞到子控制元件
- 如果
父控制元件不能
接收觸控事件,那麼子控制元件就不可能
接收到觸控事件 - 不能接受觸控事件的
四種
情況- 不接收使用者互動,即:
userInteractionEnabled = NO
- 隱藏,即:
hidden = YES
- 透明,即:
alpha <= 0.01
- 未啟用,即:
enabled = NO
- 不接收使用者互動,即:
-
提示:
UIImageView的userInteractionEnabled預設就是NO
,因此UIImageView以及它的子控制元件預設是不能接收觸控事件的 - 如何找到
最合適
處理事件的控制元件:- 首先,判斷
自己能否
接收觸控事件- 可以通過重寫
hitTest:withEvent:
方法驗證
- 可以通過重寫
- 其次,判斷
觸控點
是否在自己身上
- 對應方法
pointInside:withEvent:
- 對應方法
- 尋找檢視是從後往前遍歷子控制元件
(後新增的子控制元件先遍歷)
,重複前面的兩個步驟 - 如果沒有符合條件的子控制元件,那麼就自己處理
- 首先,判斷
- 觸控
- 響應者鏈條
- 響應者鏈條:是由多個響應者物件連線起來的鏈條
- 作用:能很清楚的看見每個響應者之間的聯絡,並且可以讓一個事件可以由多個物件處理
- 響應者物件:能處理事件的物件
- 如何判斷上一個響應者
- 如果當前這個view是
控制器的view
,那麼控制器
就是上一個響應者 - 如果當前這個view
不是
控制器的view,那麼父控制元件
就是上一個響應者 -
注意:
繼承於UIControl的控制元件有些特殊
,下面用程式碼解釋
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 這個事件是不會傳遞給UIButton父控制元件的,因為UIButton本身有處理事件的能力 // 切記重寫此方法【必須】呼叫父類方法,這樣addTarget設定的監聽的方法【才會被執行】 [super touchesBegan:touches withEvent:event]; // 如果真想把事件傳遞給父控制元件,可以這樣做 // [self.nextResponder touchesBegan:touches withEvent:event]; }
- 如果當前這個view是
-
響應者鏈
的事件傳遞過程- 如果view的控制器存在,就傳遞給控制器;
- 如果控制器不存在,則將其傳遞給它的父控制元件
- 在檢視層次結構的最頂層檢視,如果也不能處理收到的事件或訊息,則將事件或訊息傳遞給
window物件
進行處理 - 如果window物件也不處理,則將事件或訊息傳遞給
UIApplication物件
- 如果UIApplication也不能處理該事件或訊息,則將其
丟棄
-
驗證事件傳遞和響應者鏈條的預設做法
- 模仿系統做法找到
最合適
的控制元件的做法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@---%s",NSStringFromClass([self class]),__func__);
// 系統預設做法
// return [super hitTest:point withEvent:event];
// 模仿系統做法找到最合適的控制元件的做法
return [self findTheRightView:point withEvent:event];
}
/**
* 模仿系統做法找到最合適的控制元件的做法
*/
- (UIView *)findTheRightView:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷當前控制元件能否接收事件
if (!self.userInteractionEnabled
|| self.hidden
|| self.alpha <= 0.01) return nil;
// 2. 判斷點在不在當前控制元件
if (![self pointInside:point withEvent:event]) return nil;
// 3.從後往前遍歷自己的子控制元件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--)
{
UIView *childView = self.subviews[i];
// 把當前控制元件上的座標系轉換成子控制元件上的座標系
CGPoint childP = [self convertPoint:point toView:childView];
// 遞迴找到最合適處理事件的控制元件
UIView *rightView = [childView findTheRightView:childP withEvent:event];
if (rightView) return rightView;
}
// 迴圈結束,表示沒有比自己更合適的處理事件的view
return self;
}
-
hitTest:withEvent:
方法- 可以重寫這個方法來改變系統預設尋找最合適處理事件的view的處理邏輯(比如可以控制一個按鈕始終是最合適處理事件的控制元件,不管他上面是否有其他覆蓋控制元件)
-
pointInside:withEvent:
方法- 可以重寫這個方法來改變系統預設判斷點是否在自己身上的邏輯(比如可以通過這個方法來控制手勢解鎖的有效範圍)
-
pointInside:withEvent:
方法與CGRectContainsPoint
的區別:-
CGRectContainsPoint
用於判斷一個點是否在指定區域,左上角參考點(0,0)
必須一致才有可比性 -
pointInside:withEvent:
方法用於判斷一個點(相對指定控制元件
的左上角的(0,0)
點確定的座標)是否在指定控制元件內
-
/**
* 控制返回最合適處理事件的控制元件
*/
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 將當前座標點轉化為相對按鈕左上角(0,0)座標點
CGPoint btnPoint = [self convertPoint:point toView:self.btn];
// CGRectContainsPoint用於判斷一個點是否在指定區域,左上角參考點(0,0)必須一致才有可比性
// 由於btnPoint已經被轉化為相對按鈕的左上角的座標點了,所以不能用此函式判斷點是否在指定區域內
// BOOL isContain = CGRectContainsPoint(self.btn.frame, btnPoint);
// NSLog(@"self.btn.frame-->%@",NSStringFromCGRect(self.btn.frame));
// NSLog(@"btnPoint-->%@",NSStringFromCGPoint(btnPoint));
// 只要點在按鈕上就交由按鈕處理對應的事件
if([self pointInside:btnPoint withEvent:event])
{
return self.btn;
}
return [super hitTest:point withEvent:event];
}
手勢識別
- 監聽觸控事件的做法
- 如果想監聽一個view上面的觸控事件,
之前的做法是
- 自定義一個view
- 實現view的touches方法,在方法內部實現具體處理程式碼
- 通過touches方法監聽view觸控事件,有很明顯的幾個
缺點
-
必須
得自定義view - 由於是在view內部的touches方法中監聽觸控事件,因此預設情況下,無法讓其他外界物件監聽view的觸控事件
- 不容易區分使用者的具體手勢行為
-
-
iOS 3.2
之後,蘋果推出了手勢識別功能(Gesture Recognizer
),在觸控事件處理方面,大大簡化了開發者的開發難度
- 如果想監聽一個view上面的觸控事件,
-
UIGestureRecognizer
是一個抽象類,定義了所有手勢的基本行為,使用它的子類才能處理具體的手勢 - 常見手勢
- UI
Tap
GestureRecognizer(敲擊
) - UI
Pinch
GestureRecognizer(捏合,用於縮放
) - UI
Pan
GestureRecognizer(拖拽
) - UI
Swipe
GestureRecognizer(輕掃
) - UI
Rotation
GestureRecognizer(旋轉
) - UI
Long
PressGestureRecognizer(長按
)
- UI
- 手勢使用演示
// 建立手勢識別器物件
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
// 連續敲擊2次
tap.numberOfTapsRequired = 2;
// 需要2根手指一起敲擊
tap.numberOfTouchesRequired = 2;
// 新增手勢識別器到對應的view上,那麼就可以在此view應用對應的手勢
[self.iconView addGestureRecognizer:tap];
// 監聽手勢
[tap addTarget:self action:@selector(tapIconView:)];
- 手勢識別的狀態
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
// 沒有觸控事件發生,所有手勢識別的預設狀態
UIGestureRecognizerStatePossible,
// 一個手勢已經開始但尚未改變或者完成時
UIGestureRecognizerStateBegan,
// 手勢狀態改變
UIGestureRecognizerStateChanged,
// 手勢完成
UIGestureRecognizerStateEnded,
// 手勢取消,恢復至Possible狀態
UIGestureRecognizerStateCancelled,
// 手勢失敗,恢復至Possible狀態
UIGestureRecognizerStateFailed,
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
- 同時支援多個手勢是通過代理完成
- 設定每個手勢的代理
- 實現UIGestureRecognizerDelegate協議
// 每個手勢設定代理
// 是否允許同時支援多個手勢,預設是不支援多個手勢
// 返回yes表示支援多個手勢
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- 注意點:
- 如果以後想要一個控制元件支援多個方向的輕掃,必須建立多個輕掃手勢,一個輕掃手勢只支援一個方向(但是同時設定左右方向是能同時支援,同時設定上下方向也是支援的,但是最好是建立多個清掃手勢來支援)
- 旋轉手勢,記得復位
- 縮放手勢,記得復位
- 拖拽手勢,記得復位
#pragma mark - 旋轉手勢
- (void)setUpRotation
{
UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotation:)];
rotation.delegate = self;
[self.imageView addGestureRecognizer:rotation];
}
// 預設傳遞的旋轉的角度都是相對於最開始的位置
- (void)rotation:(UIRotationGestureRecognizer *)rotation
{
self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, rotation.rotation);
// 復位,這個很重要!!!
rotation.rotation = 0;
}
#pragma mark - 捏合
- (void)setUpPinch
{
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
pinch.delegate = self;
[self.imageView addGestureRecognizer:pinch];
}
- (void)pinch:(UIPinchGestureRecognizer *)pinch
{
self.imageView.transform = CGAffineTransformScale(self.imageView.transform, pinch.scale, pinch.scale);
// 復位,這個很重要!!!
pinch.scale = 1;
}
#pragma mark - 拖拽
- (void)setUpPan
{
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
pan.delegate = self;
[self.imageView addGestureRecognizer:pan];
}
- (void)pan:(UIPanGestureRecognizer *)pan
{
// 移動檢視
// 獲取手勢的移動,也是相對於最開始的位置
CGPoint transP = [pan translationInView:self.imageView];
self.imageView.transform = CGAffineTransformTranslate(self.imageView.transform, transP.x, transP.y);
// 復位,這個很重要!!!
[pan setTranslation:CGPointZero inView:self.imageView];
}
相關文章
- 事件傳遞和響應鏈事件
- 理解響應者和響應鏈如何處理事件事件
- 記錄下:iOS事件的事件的傳遞和響應iOS事件
- 淺談 iOS 事件的傳遞和響應過程iOS事件
- iOS 中的事件傳遞和響應機制 - 原理篇iOS事件
- iOS 中的事件傳遞和響應機制 - 實踐篇iOS事件
- iOS 中的事件傳遞和響應機制 – 實踐篇iOS事件
- iOS探索:UI檢視之事件傳遞&檢視響應iOSUI事件
- iOS響應者鏈iOS
- iOS 響應者鏈iOS
- UIResponder事件響應鏈學習筆記UI事件筆記
- [譯] iOS 響應者鏈 UIResponder、UIEvent 和 UIControl 的使用iOSUI
- Netty使用及事件傳遞Netty事件
- React事件傳遞引數React事件
- Flutter 使用者互動事件的響應Flutter事件
- 值傳遞和引用傳遞
- Java的值傳遞和引用傳遞Java
- JavaScript的值傳遞和引用傳遞JavaScript
- Android觸控事件傳遞機制Android事件
- Day30--值傳遞和引用傳遞
- go 值傳遞和地址傳遞的例子Go
- cc.Node事件響應事件
- Qt 事件傳遞流程-事件處理器|事件分發器|事件過濾器QT事件過濾器
- 網路安全事件應急響應事件
- 資料洩露後,攻擊者是如何應對事件響應的?事件
- 呼叫鏈系列四:呼叫鏈上下文傳遞
- Cyber Triage 3.12 for Windows - 數字取證和事件響應Windows事件
- 關於值傳遞和引用傳遞的解釋
- Android中觸控事件的傳遞機制Android事件
- Vue自定義元件事件傳遞:EventBus部分Vue元件事件
- Java進階09 事件響應Java事件
- Flutter事件響應原始碼分析Flutter事件原始碼
- Flutter:如何響應觸控事件Flutter事件
- Flutter:如何響應互動事件?Flutter事件
- UIDatePicker事件不響應問題UI事件
- 前後端分離應用——使用者資訊傳遞後端
- Java 從陣列來看值傳遞和引用傳遞Java陣列
- element-UI change 事件傳遞自定義引數UI事件