-
觸摸對象的產(chǎn)生晦譬。
當iOS系統(tǒng)檢測到手指觸摸屏幕時,會將其打包成一個UIEvent對象互广,該對象包含一些處理事件所需要的信息敛腌。然后將其放入事件隊列(Event Queue)。程序的單例UIApplication對象會從隊列頭部取出一個事件對象惫皱,將其分發(fā)出去像樊。通常首先是將事件分發(fā)給程序的主窗口(keyWindow)對象,如下圖:
- IOKit.framework 為系統(tǒng)內核的庫
- SpringBoard.app 相當于手機的桌面
- Source1 主要接收系統(tǒng)的消息
- Source0 - UIApplication - UIWindow
UIEvent
An object that describes a single user interaction with your app.
描述單次用戶與應用的交互行為旅敷。包括觸摸事件生棍、運動事件、遠程控制事件和按壓事件四種類型媳谁,一般討論的是觸摸事件涂滴。
觸摸事件對象包含與事件相關的觸摸(手指接觸屏幕)友酱。觸摸事件對象可以包含一個或多個觸摸,并且每個觸摸都由UITouch對象表示氢妈。
多點觸控時粹污,UIKit會重用相同的UIEvent對象,因此不建議在app中強引用UIEvent或UITouch首量,而是將相關數(shù)據(jù)(比如坐標)從其中復制出來壮吩。
下面是UIEvent的屬性和方法:
/// 事件類型
@property(nonatomic,readonly) UIEventType type;
/// 事件子類型,一般用于遠程控制事件
@property(nonatomic,readonly) UIEventSubtype subtype;
/// 事件觸發(fā)時間
@property(nonatomic,readonly) NSTimeInterval timestamp;
/// 事件包含的touch對象加缘,不同touch可能來源于不同view或window
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
/// 返回來源于指定window的touch對象
- (nullable NSSet *)touchesForWindow:(UIWindow*)window;
/// 返回來源于指定view的touch對象
- (nullable NSSet *)touchesForView:(UIView*)view;
/// 返回來源于指定手勢的touch對象
- (nullable NSSet *)touchesForGestureRecognizer:(UIGestureRecognizer*)gesture;
/// 回與指定主要觸摸相關聯(lián)的由于在屏幕上滑動太快而丟失的需要合并的所有觸摸鸭叙。因為有些屏幕幀速可能會比較低,當你使用一款繪圖軟件時快速畫一個圓拣宏,那么呈現(xiàn)出來的可能會是一個看起來像是不規(guī)則多邊形的東西沈贝,這就是因為丟失了一些輔助觸摸事件的原因,這一屬性可以提高觸摸精度勋乾。
- (nullable NSArray *)coalescedTouchesForTouch:(UITouch*)touchAPI_AVAILABLE(ios(9.0));
/// 返回的觸摸表示系統(tǒng)根據(jù)用戶過去的輸入估計用戶觸摸輸入的位置宋下。處理用戶的觸摸輸入并將該信息轉換為繪圖命令需要時間,而將這些繪圖命令轉換為渲染內容則需要額外的時間辑莫。此方法的預測觸摸最小化感知的延遲学歧,常用會繪圖等應用程序。
- (nullable NSArray *)predictedTouchesForTouch:(UITouch*)touchAPI_AVAILABLE(ios(9.0));
UITouch
An object representing the location, size, movement, and force of a touch occurring on the screen.
表示在屏幕上發(fā)生的觸摸的位置各吨、大小枝笨、移動和力度的對象。
當手指移動時揭蜒,系統(tǒng)會更新同一個UITouch對象横浑,使之能夠一直保存該手指在的觸摸位置。當手指離開屏幕時屉更,系統(tǒng)會銷毀相應的UITouch對象徙融。
下面是UITouch的屬性和方法:
/// 觸發(fā)時間
@property(nonatomic,readonly) NSTimeInterval timestamp;
/// 狀態(tài),包括手指開始瑰谜,手指移動张咳,手指靜止,手指離開(結束)似舵,意外中斷(取消)
@property(nonatomic,readonly) UITouchPhase phase;
/// 短時間內單擊的次數(shù)
@property(nonatomic,readonly) NSUInteger tapCount;
/// 觸摸類型,包括手指直接接觸葱峡,間接接觸觸摸砚哗,觸筆觸摸
@property(nonatomic,readonly) UITouchType type;
/// 半徑
@property(nonatomic,readonly) CGFloat majorRadius;
/// 半徑誤差
@property(nonatomic,readonly) CGFloat majorRadiusTolerance;
/// 所在window
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
/// 所在view
@property(nullable,nonatomic,readonly,strong) UIView *view;
/// 接收到觸摸的手勢
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers ;
/// 當前位置在指定view坐標系的坐標
- (CGPoint)locationInView:(nullable UIView *)view;
/// 前一個位置在指定view坐標系的坐標
- (CGPoint)previousLocationInView:(nullable UIView *)view;
/// 返回精確的當前位置,不要在hit test中使用
- (CGPoint)preciseLocationInView:(nullable UIView *)view;
/// 返回精確的前一個位置砰奕,不要在hit test中使用
- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view;
/// 按壓力度
@property(nonatomic,readonly) CGFloat force;
/// 最大可能的力度
@property(nonatomic,readonly) CGFloat maximumPossibleForce;
/// 方位角度只用于觸控筆
- (CGFloat)azimuthAngleInView:(nullable UIView *)view;
/// 指向方位角方向的單位矢量蛛芥。僅對觸控筆類型有效提鸟。
- (CGVector)azimuthUnitVectorInView:(nullable UIView *)view;
/// 高度角。僅對觸控筆類型有效仅淑。
@property(nonatomic,readonly) CGFloat altitudeAngle;
/// 此屬性包含當前觸摸數(shù)據(jù)的唯一標記称勋,當每個觸摸對象的觸摸特性發(fā)生變化時,該值將會單獨增加,返回值是NSNumber 索引號涯竟,關聯(lián)更新的觸摸與原始觸摸赡鲜。
@property(nonatomic,readonly) NSNumber * _Nullable estimationUpdateIndex;
/// 當前觸摸對象估計的觸摸屬性,包括力度庐船,方位银酬,高度,位置
@property(nonatomic,readonly) UITouchProperties estimatedProperties;
/// 一組期望將來有傳入更新的屬性筐钟。如果估計屬性沒有更新揩瞪,則當前值是我們的最終估計值
@property(nonatomic,readonly) UITouchProperties estimatedPropertiesExpectingUpdates;
- 事件傳遞鏈,確定事件的第一響應者(hit-testing)篓冲。
對于觸摸事件來講李破,window對象會首先嘗試將事件分發(fā)給觸摸事件發(fā)生的那個視圖上。這一視圖通常被稱為hit-test視圖壹将,而查找這一視圖的過程就叫做hit-testing嗤攻。
UIKit使用基于視圖的hit-testing來確定Touch事件在哪里產(chǎn)生。UIKit將Touch位置與視圖層級中的視圖對象的邊界進行了比較瞭恰。UIView的hitTest:withEvent:方法在視圖層級中執(zhí)行屯曹,尋找最深的包含指定Touch的子視圖,這個視圖將成為Touch事件的第一響應者惊畏。
UIKit不變的分配每一個Touch給包含它的視圖恶耽。UIKit創(chuàng)建UITouch對象當touch第一次產(chǎn)生時,釋放這個UITouch對象在touch結束時颜启。當touch位置或者其他參數(shù)改變時偷俭,UIKit更新UITouch對象新的信息。只有包含它的視圖這個屬性不會改變缰盏。甚至這個touch位置移動到初始視圖的外面涌萤,這個屬性也不會改變。
這個方法忽略以下情況:
視圖是隱藏的 hidden = YES
用戶交互關閉的 userInteractionEnabled = NO
透明度小于0.01的 alpha < 0.01
點在接收者的范圍之外不會被命中口猜,即使它們實際上處于接收者的子視圖之內负溪。如果當前視圖的cilpsToBounds屬性被設置為NO,影響了子視圖超過當前視圖會產(chǎn)生這種情況。
怎么尋找最合適的view济炎?
這里要用到兩個方法川抡。
/// 此方法返回的View是本次點擊事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
/// 判斷一個點是否落在范圍內
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent:工作流程:
首先調用當前視圖的pointInside:withEvent:方法判斷觸摸點是否在當前視圖內。
若不在须尚,說明觸摸點不在當前視圖內崖堤,則hitTest:withEvent:返回nil侍咱。
若在,說明觸摸點在當前視圖內密幔,則遍歷當前視圖的所有子視圖(subviews)楔脯,調用子視圖的hitTest:withEvent:方法重復前面的步驟,子視圖的遍歷順序是從top到bottom胯甩,即從subviews數(shù)組的末尾向前遍歷昧廷,直到有子視圖的hitTest:withEvent:方法返回非空對象或者全部子視圖遍歷完畢,返回非空時會終止子視圖的遍歷蜡豹。直到找到最終的視圖沒有subviews麸粮,這就是第一響應者視圖。
系統(tǒng)默認的hitTest和pointInside處理類似如下代碼:
// 因為所有的視圖類都是繼承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判斷當前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點在不在當前控件
if ([self pointInside:point withEvent:event] == NO) 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 *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結束,表示沒有比自己更合適的view
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
CGRect bounds = self.bounds;
// CGRectContainsPoint 判斷點是否在矩形內
return CGRectContainsPoint(bounds, point);
}
備注:可以通過重寫hitTest:withEvent: 常用于改變傳遞鏈镜廉,也可以通過重寫pointInside:withEvent:常用于改變視圖的觸摸熱區(qū)弄诲。
- 響應鏈。
經(jīng)過以上的事件的傳遞過程娇唯,事件已經(jīng)傳遞給系統(tǒng)認為最適合的View了齐遵。接下來就是處理這個事件。
處理事件方法:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
}
響應鏈是通過 nextResponder 屬性組成的一個鏈表塔插,如下圖:
默認UIView會將事件上拋梗摇,其它大部分響應者不會上拋。
UIResponder
響應和處理事件的抽象類想许。
包括觸摸伶授,運動,遠程控制流纹,按壓事件糜烹。可以通過重寫特定的方法處理相應事件漱凝。例如疮蹦,要處理觸摸事件,響應程序實現(xiàn)touchesStarted:withEvent:茸炒、touchesMoved:withEvent:愕乎、touchesend:withEvent:、touchesCancelled:withEvent:方法壁公。
可以通過inputView接受自定義輸入感论。系統(tǒng)鍵盤是輸入視圖最明顯的例子。當用戶在屏幕上點擊UITextField和UITextView對象時紊册,該視圖將成為第一個響應程序并顯示其輸入視圖比肄,即系統(tǒng)鍵盤。類似地,您可以創(chuàng)建自定義輸入視圖薪前,并在其他響應程序激活時顯示它們。若要將自定義輸入視圖與響應程序關聯(lián)关斜,請將該視圖分配給響應程序的inputView屬性示括。
// 當對象成為第一個響應者時調用并顯示。上升到響應鏈痢畜。
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView;
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView;
問題:
當手指觸摸從某個視圖A中拖動到視圖A外側垛膝,為什么A仍然能接收到touchesMoved消息?
解答:
當手指觸摸屏幕時丁稀,產(chǎn)生event X吼拥,并會以觸摸開始點進行hit-testing判斷,找到第一響應者线衫。單個手指的觸摸只會產(chǎn)生一個touch對象凿可,此后,滑動手指都是在更新event X中touch的坐標授账。當滑動離開視圖A時枯跑,event X的第一響應者仍然是View A。