前言:蘋果的官方文檔《Event Handling Guide for iOS》對(duì)事件處理做了非常詳盡清晰的解釋,建議大家仔細(xì)研讀
關(guān)于iOS的事件響應(yīng)機(jī)制網(wǎng)上講解文章不少,有的文章內(nèi)容少?zèng)]講全面,有的說(shuō)的太多,一個(gè)概念反復(fù)說(shuō),傳遞和響應(yīng)混在一起講,不好理解,我綜合參考了幾篇文章總結(jié)了一下,覺得可以分為以下幾點(diǎn)來(lái)講
1. iOS中的事件介紹
2. 事件的產(chǎn)生和傳遞
3. 事件響應(yīng)
4. 實(shí)際項(xiàng)目中的應(yīng)用
1.iOS中的事件介紹
iOS中的事件可以分為3大類型:
觸屏事件(例如點(diǎn)擊按鈕、通過手勢(shì)縮放圖片、拖動(dòng)上下滾動(dòng)頁(yè)面等)
傳感器事件(例如搖一搖紅包、通過旋轉(zhuǎn)設(shè)備控制賽車方向、指南針等)
遠(yuǎn)程控制事件(例如耳機(jī)的線控、外接手柄、遙控器等)
著重講解一下iOS處理觸屏事件,觸屏事件分為兩種方式:
高級(jí)事件處理:利用UIKit提供的各種用戶控件或者手勢(shì)識(shí)別器來(lái)處理事件。
低級(jí)事件處理:在UIView的子類中重寫觸屏回調(diào)方法,直接處理觸屏事件。
如果想監(jiān)聽一個(gè)view上面的觸摸事件,之前的做法是:
(1)自定義一個(gè)view。
(2)實(shí)現(xiàn)view的touches方法,在方法內(nèi)部實(shí)現(xiàn)具體處理代碼。
通過touches方法監(jiān)聽view觸摸事件,有很明顯的幾個(gè)缺點(diǎn):
(1)必須得自定義view。
(2)由于是在view內(nèi)部的touches方法中監(jiān)聽觸摸事件,因此默認(rèn)情況下,無(wú)法讓其他外界對(duì)象監(jiān)聽view的觸摸事件。
(3)不容易區(qū)分用戶的具體手勢(shì)行為。
iOS 3.2之后,蘋果推出了手勢(shì)識(shí)別功能(Gesture Recognizer),在觸摸事件處理方面,大大簡(jiǎn)化了開發(fā)者的開發(fā)難度。
UIKit中我們常用的是UIControl類實(shí)例的addTarget:action:forControlEvents:方法維護(hù)控件目標(biāo)行為表,除了UIKit控件外,手勢(shì)識(shí)別器UIGestureRecognizer類的實(shí)例也是處理觸屏事件的好幫手,其內(nèi)部也使用目標(biāo)行為表。
為什么手勢(shì)和單擊事件只會(huì)響應(yīng)手勢(shì)?
UIGestureRecognizer 有個(gè)屬性cancelsTouchesInView,這個(gè)屬性默認(rèn)值是YES,即當(dāng)手勢(shì)識(shí)別成功后,會(huì)發(fā)送touchesCancelled消息給view來(lái)結(jié)束view的響應(yīng)。
如果cancelsTouchesInView為NO,那么gestureRecognizer和view都可以響應(yīng)
UIKit內(nèi)置了6種手勢(shì)識(shí)別器:
UITapGestureRecognizer:點(diǎn)擊(單擊、雙擊、三連擊等)手勢(shì)。
UIPinchGestureRecognizer:縮放手勢(shì)。
UIPanGestureRecognizer:拖拽手勢(shì)。
UISwipeGestureRecognizer:滑動(dòng)手勢(shì)。
UIRotationGestureRecognizer:旋轉(zhuǎn)手勢(shì)。
UILongPressGestureRecognizer:長(zhǎng)按手勢(shì)。
UIKit控件和手勢(shì)識(shí)別器屬于高級(jí)事件處理的范疇,這些不再多說(shuō),以下文字都是介紹的低級(jí)事件處理過程#### UITouch當(dāng)你用一根手指觸摸屏幕時(shí), 會(huì)創(chuàng)建一個(gè)與之關(guān)聯(lián)的UITouch對(duì)象, 一個(gè)UITouch對(duì)象對(duì)應(yīng)一根手指. 在事件中可以根據(jù)NSSet中UITouch對(duì)象的數(shù)量得出此次觸摸事件是單指觸摸還是雙指多指等等
觸摸產(chǎn)生時(shí)所處的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
觸摸產(chǎn)生時(shí)所處的視圖
@property(nonatomic,readonly,retain) UIView? *view;
短時(shí)間內(nèi)點(diǎn)按屏幕的次數(shù),可以根據(jù)tapCount判斷單擊、雙擊或更多的點(diǎn)擊
@property(nonatomic,readonly) NSUInteger? ? ? tapCount;
記錄了觸摸事件產(chǎn)生或變化時(shí)的時(shí)間,單位是秒
@property(nonatomic,readonly) NSTimeInterval? timestamp;
當(dāng)前觸摸事件所處的狀態(tài)
@property(nonatomic,readonly) UITouchPhase? ? phase;
#### UIEvent每產(chǎn)生一個(gè)事件, 就對(duì)應(yīng)產(chǎn)生一個(gè)UIEvent.UIEvent記錄著該事件產(chǎn)生的時(shí)間, 事件的類型等等UIEvent幾個(gè)重要的屬性 :
事件類型
@property(nonatomic,readonly) UIEventType? ? type;
@property(nonatomic,readonly) UIEventSubtype? subtype;
事件產(chǎn)生的時(shí)間
@property(nonatomic,readonly) NSTimeInterval? timestamp;
#### 響應(yīng)者對(duì)象(UIResponder)在iOS中不是任何對(duì)象都能處理事件, 只有繼承了UIResponder的對(duì)象才能接收并處理事件,我們稱為響應(yīng)者對(duì)象UIApplication,UIViewController,UIView都繼承自UIResponder,因此他們都是響應(yīng)者對(duì)象, 都能夠接收并處理事件繼承自UIResponder的類能處理事件是由于UIResponder內(nèi)部提供了以下方法
觸摸事件
(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;
傳感器事件
(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
(void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠(yuǎn)程控制事件
(void)remoteControlReceivedWithEvent:(UIEvent *)event;
兩個(gè)UIView相關(guān)屬性:* multipleTouchEnabled:是否開啟多點(diǎn)觸控* exclusiveTouch :多個(gè)控件接受事件時(shí)的排他性---------------------------------------------------------------### 2. iOS中事件的產(chǎn)生和傳遞1.發(fā)生觸摸事件后,系統(tǒng)會(huì)將該事件加入到一個(gè)由UIApplication管理的隊(duì)列事件中2.UIApplication會(huì)從事件隊(duì)列中取出最前面的事件,并將事件分發(fā)下去以便處理,通常會(huì)先發(fā)送事件給應(yīng)用程序的主窗口(keyWindow)3.主窗口會(huì)在視圖層次結(jié)構(gòu)中找到一個(gè)最合適的視圖來(lái)處理觸摸事件事件的具體傳遞過程,如圖:一般事件的傳遞是從父控件傳遞到子控件的例如:點(diǎn)擊了綠色的View,傳遞過程如下:UIApplication->Window->白色View->綠色View點(diǎn)擊藍(lán)色的View,傳遞過程如下:UIApplication->Window->白色View->橙色View->藍(lán)色View如果父控件接受不到觸摸事件,那么子控件就不可能接收到觸摸事件### ( 重難點(diǎn))如何尋找最合適的view
應(yīng)用如何找到最合適的控件來(lái)處理事件?有以下準(zhǔn)則
1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件
2.觸摸點(diǎn)是否在自己身上
3.從后往前遍歷子控件,重復(fù)前面的兩個(gè)步驟(首先查找數(shù)組中最后一個(gè)元素)
4.如果沒有符合條件的子控件,那么就認(rèn)為自己最合適處理
詳述:
1.主窗口接收到應(yīng)用程序傳遞過來(lái)的事件后,首先判斷自己能否接手觸摸事件。如果能,那么在判斷觸摸點(diǎn)在不在窗口自己身上
2.如果觸摸點(diǎn)也在窗口身上,那么窗口會(huì)從后往前遍歷自己的子控件(遍歷自己的子控件只是為了尋找出來(lái)最合適的view)
3.遍歷到每一個(gè)子控件后,又會(huì)重復(fù)上面的兩個(gè)步驟(傳遞事件給子控件,1.判斷子控件能否接受事件,2.點(diǎn)在不在子控件上)
4.如此循環(huán)遍歷子控件,直到找到最合適的view,如果沒有更合適的子控件,那么自己就成為最合適的view。
注意:之所以會(huì)采取從后往前遍歷子控件的方式尋找最合適的view只是為了做一些循環(huán)優(yōu)化。因?yàn)橄啾容^之下,后添加的view在上面,降低循環(huán)次數(shù)。
UIView不能接收觸摸事件的三種情況:* 不接受用戶交互:userInteractionEnabled =NO;* 隱藏:hidden =YES;* 透明:alpha =0.0~0.01尋找最合適的view過程,如圖:說(shuō)明一下控件的添加順序:白1->綠2->橙2->藍(lán)3->紅3->黃4這里點(diǎn)擊了橙色的那塊區(qū)域,事件傳遞判斷過程如下:1.UIApplication從事件隊(duì)列中取出事件分發(fā)給UIWindow2.UIWindow判斷自己是否能接受觸摸事件,可以3.UIWindow判斷觸摸點(diǎn)是否在自己身上,是的。4.UIWindow從后往前便利自己的子控件,取出白15.白1都滿足最上面兩個(gè)條件,遍歷子控件橙26.橙2都滿足最上面兩個(gè)條件,遍歷子控件,先取出紅37.紅3不滿足條件2,取出藍(lán)38.藍(lán)3也不滿足條件2,最后最合適的控件是橙2#### 在事件傳遞尋找最合適的View時(shí),底層到底干了哪些事?尋找合適的View用到兩個(gè)重要方法:*? hitTest:withEvent: *? pointInside####? hitTest:withEvent:方法什么時(shí)候調(diào)用?* 只要事件一傳遞給一個(gè)控件,這個(gè)控件就會(huì)調(diào)用他自己的hitTest:withEvent:方法尋找合適的View
作用
尋找并返回最合適的view(能夠響應(yīng)事件的那個(gè)最合適的view)
注 意:不管這個(gè)控件能不能處理事件,也不管觸摸點(diǎn)在不在這個(gè)控件上,
事件都會(huì)先傳遞給這個(gè)控件,隨后再調(diào)用hitTest:withEvent:方法
hitTest:withEvent:底層調(diào)用流程:
底層具體實(shí)現(xiàn)如下 :
(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷當(dāng)前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點(diǎn)在不在當(dāng)前控件
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];
// 把當(dāng)前控件上的坐標(biāo)系轉(zhuǎn)換成子控件上的坐標(biāo)系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結(jié)束,表示沒有比自己更合適的view
return self;
}
**事件傳遞給窗口或控件的后,就調(diào)用hitTest:withEvent:方法尋找更合適的view。所以是,先傳遞事件,再根據(jù)事件在自己身上找更合適的view。不管子控件是不是最合適的view,系統(tǒng)默認(rèn)都要先把事件傳遞給子控件,經(jīng)過子控件調(diào)用自己的hitTest:withEvent:方法驗(yàn)證后才知道有沒有更合適的view。即便父控件是最合適的view了,子控件的hitTest:withEvent:方法還是會(huì)調(diào)用,不然怎么知道有沒有更合適的!即,如果確定最終父控件是最合適的view,那么該父控件的子控件的hitTest:withEvent:方法也是會(huì)被調(diào)用的。** hitTest:withEvent:方法忽略隱藏(hidden=YES)的視圖,禁止用戶操作(userInteractionEnabled=YES)的視圖,以及alpha級(jí)別小于0.01(alpha<0.01)的視圖。? ? ? 如果一個(gè)子視圖的區(qū)域超過父視圖的bound區(qū)域(父視圖的clipsToBounds 屬性為NO,這樣超過父視圖bound區(qū)域的子視圖內(nèi)容也會(huì)顯示),那么正常情況下對(duì)子視圖在父視圖之外區(qū)域的觸摸操作不會(huì)被識(shí)別,因?yàn)楦敢晥D的pointInside:withEvent:方法會(huì)返回NO,這樣就不會(huì)繼續(xù)向下遍歷子視圖了。** - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event **該方法判斷觸摸點(diǎn)是否在控件身上, 是則返回YES, 否則返回NO**作用? 可以使用以上兩個(gè)方法做到:? 指鹿為馬(明明點(diǎn)擊的是B視圖, 卻由A視圖來(lái)響應(yīng)事件)? 穿透某控件點(diǎn)擊被覆蓋的下一層控件? 讓父控件frame之外的子控件響應(yīng)觸摸事件(下面實(shí)際應(yīng)用中有具體介紹)**? ? ? ---------------------------------------------------------------### 3.事件響應(yīng)上文介紹了事件的傳遞過程,找到合適的View之后就會(huì)調(diào)用該view的touches方法要進(jìn)行響應(yīng)處理具體的事件,找不到最合適的view,就不會(huì)調(diào)用touches方法進(jìn)行事件處理。這里先介紹一下響應(yīng)者鏈條:響應(yīng)者鏈條其實(shí)就是很多響應(yīng)者對(duì)象(繼承自UIResponder的對(duì)象)一起組合起來(lái)的鏈條稱之為響應(yīng)者鏈條一般默認(rèn)做法是控件將事件順著響應(yīng)者鏈條向上傳遞,將事件交給上一個(gè)響應(yīng)者進(jìn)行處理 (即調(diào)用super的touches方法)。那么如何判斷當(dāng)前響應(yīng)者的上一個(gè)響應(yīng)者是誰(shuí)呢?有以下兩個(gè)規(guī)則:1.判斷當(dāng)前是否是控制器的View,如果是控制器的View,上一個(gè)響應(yīng)者就是控制器2.如果不是控制器的View,上一個(gè)響應(yīng)者就是父控件響應(yīng)過程如下圖:touch響應(yīng):* 找到最合適的view會(huì)調(diào)用touches方法處理事件* touches默認(rèn)做法是把事件順著響應(yīng)者鏈條向上拋
//只要點(diǎn)擊控件,就會(huì)調(diào)用touchBegin,如果沒有重寫這個(gè)方法,自己處理不了觸摸事件
// 上一個(gè)響應(yīng)者可能是父控件
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默認(rèn)會(huì)把事件傳遞給上一個(gè)響應(yīng)者,上一個(gè)響應(yīng)者是父控件,交給父控件處理
[super touchesBegan:touches withEvent:event];
// 注意不是調(diào)用父控件的touches方法,而是調(diào)用父類的touches方法
// super是父類 superview是父控件
}
如果控制器也不響應(yīng)響應(yīng)touches方法,就交給UIWindow。如果UIWindow也不響應(yīng),交給UIApplication,如果都不響應(yīng)事件就作廢了。用具體的例子來(lái)看下響應(yīng)過程,例子太簡(jiǎn)單就不上demo了,看下截圖(3-2)(圖3-2)? 控件的添加順序:紅1->藍(lán)2->綠2->黃3綠色View實(shí)現(xiàn)了touch方法,如下黃色view沒有實(shí)現(xiàn)touch方法,如下當(dāng)點(diǎn)擊黃色區(qū)域時(shí),由于黃色view沒有實(shí)現(xiàn)touch方法,就順著響應(yīng)鏈找到其父view(綠色View),綠色view實(shí)現(xiàn)了touch方法,便打印了-- touchGreen最后總結(jié)來(lái)說(shuō)一次完整的觸摸事件的傳遞響應(yīng)過程為:UIApplication-->UIWindow-->遞歸找到最合適處理的控件-->控件調(diào)用touches方法-->判斷是否實(shí)現(xiàn)touches方法-->沒有實(shí)現(xiàn)默認(rèn)會(huì)將事件傳遞給上一個(gè)響應(yīng)者-->找到上一個(gè)響應(yīng)者-->找不到方法作廢一句話總結(jié)整個(gè)過程是:觸摸或者點(diǎn)擊一個(gè)控件,然后這個(gè)事件會(huì)從上向下(從父->子)找最合適的view處理,找到這個(gè)view之后看他能不能處理,能就處理,不能就按照事件響應(yīng)鏈向上(從子->父)傳遞給父控件事件的傳遞和響應(yīng)的區(qū)別:事件的傳遞是從上到下(父控件到子控件),事件的響應(yīng)是從下到上(順著響應(yīng)者鏈條向上傳遞:子控件到父控件。### 4 實(shí)際項(xiàng)目中的應(yīng)用* 情景1: 點(diǎn)擊子控件,讓父控件響應(yīng)事件;(點(diǎn)擊綠色View,紅色View響應(yīng))分析:可通過兩種方式實(shí)現(xiàn)(1)因?yàn)閔itTest:withEvent:方法的作用就是控件接收到事件后,判斷自己是否能處理事件,判斷點(diǎn)在不在自己的坐標(biāo)系上,然后返回最合適的view。所以,我們可以在hitTest:withEvent:方法里面強(qiáng)制返回父控件為最合適的view.
作者:舊飯盆mingzhi_liu
鏈接:http://www.itdecent.cn/p/f55b613b564e
來(lái)源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。