最好的輸入方式:iOS中的觸摸事件

喬幫主在發布會上提到,用戶的手才是最好的輸入設備,的確,iPhone之后,非觸屏手機再已難覓。觸摸是最基本的用戶輸入事件,理解iOS特有的觸摸事件響應機制,能夠良好管理程序中觸摸響應方法,避免沖突的發生。

iOS中的事件

iOS中的事件主要分為三類:

  1. UIControl Actions: 使用target/action注冊的SEL。
  2. User Events: 用戶與應用之間的交互:觸摸,輸入文字,搖晃,遠程控制等。
  3. System Events: 應用啟動,切前后臺,低內存等。
    cocoa和cocoa touch的程序啟動后,,會首先初始化一些基本資源:在主線程創建一個main event loop;初始化主UIWindow
    應用啟動過程-w500

    main event loop本質上是一個NSRunLoop,與其他輔助線程的run loop不同,其是自創建后自動開始運行的。主消息循環最大的特點是:它在創建時就與負責捕獲用戶事件的系統底層建立了連接,所以它的input source可以收到系統傳遞過來的用戶事件。UIApplication對象會將當前要處理的用戶事件封裝成UIEvent,發送給UIWindow,在由UIWindow轉發給對應的響應者。
    iOS響應用戶事件
    iOS響應用戶事件

    UIEvent表示用戶與iOS產生交互的事件,UIWindow將觸摸事件發送給hitTest View,其他事件發送給first responder,若它們不能處理該事件,事件在響應鏈向上傳遞,找到最終的響應者或丟棄。
    本文主要介紹觸摸事件的響應機制。

iOS中能夠捕獲觸摸事件的類

iOS程序中,有三種類可以接受用戶的觸摸事件并響應,分別是:UIControl, UIReponder, UIGestureRecognizer,這三個類在參與觸摸響應機制的時機不同,在實際使用時要加以注意。

iOS中的觸摸事件

iOS中使用UItouch來表示用戶的一根手指在屏幕上的觸摸行為。當用戶觸摸屏幕時,硬件會捕捉到觸摸行為,將觸摸點的半徑、力度和坐標等發送給iOS,經過UIKit封裝后,得到UITouch對象。通過UITouch對象,我們可以獲得其關聯的視圖(hitTest View),在視圖中的坐標,生命周期的當前階段,點擊數等信息。。一次用戶點擊多次的事件,其只包含一個UITouch
觸摸類型的UIEvent包含至少一個UITouch,也就是用戶在屏幕上的一次手勢操作的手指運動,其會持有此次事件相關聯的UITouches序列。,即在一次手勢操作中,其中一個手指中途離開屏幕,它所對應的UITouch依然存在于該事件中。響應者會在touchesBegan:withEvent:等方法中獲取UITouch對應的UIEvent
UITouches序列在用戶第一根手指觸摸屏幕時開始,最后一根手指離開時結束,當手指狀態變化時,iOS會將序列中的UITouch對象發送給UIEvent對象。

一個UItouches序列和UItouch的不同生命周期
一個UItouches序列和UItouch的不同生命周期

iOS的觸摸事件響應機制

當用戶觸摸屏幕時,對應的觸摸事件會加入到UIApplication事件隊列中,當下一個RunLoop來臨時,UIApplication會將出列最前端的事件,發送給當前的UIWindow(key window)。
UIWindow會調用hitTest:withEvent:方法,開始hit-testing流程尋找包含觸摸點的視圖。該流程會返回包含觸摸點的層級最低的視圖。
每當用戶觸摸屏幕時,UIKit都會執行hit-testing,之后再從hitTest視圖開始尋找事件的響應者。當hitTest視圖決定后,它就關聯了對應的觸摸事件,會持續收到觸摸事件生命周期的方法,(touchBegan, touchMove, touchCancel/touchEnd),即使是觸摸點已經在touchMove階段移出了hitTest視圖,它依然能夠收到后續的消息。

Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.

iOS的觸摸事件響應機制

hit-testing流程

iOS中hit-testing使用逆前序的深度遍歷算法來確定用戶點按的最低層級(最靠近用戶)的視圖,該hitTest視圖是觸摸事件的響應鏈頭結點。
逆前序的深度遍歷算法:根節點-->右子樹-->左子樹。
當收到觸摸事件后,UIApplication在當前視圖層級中,從key window開始(最頂級),從上往下遍歷子視圖調用hitTest:withEvent:,若找到hitTest視圖則停止遍歷并返回。
當視圖收到hitTest:withEvent:方法后,通過下列條件判斷是否在該視圖執行hit-testing。

  1. pointInside:withEvent:方法返回YES。pointInside:withEvent:方法用來判斷觸摸點是否在當前視圖內。
  2. hidden == NO。
  3. userInteractionEnabled == YES。
  4. alpha >= 0.01。若view的content繪制為透明的,則不受影響。

需要注意的是,當clipsToBounds == NO時,視圖的子視圖可能會超出其bounds,這種情況如果觸摸點在子視圖超出父視圖的范圍,那么hit-tesing不會再此視圖樹上執行。

hit-testing

如圖,當用戶觸摸viewB.1時,UIApplication對象收到觸摸事件,從key window開始執行hit-testing,首先訪問viewC,由于pointInside:withEvent:方法返回NO,取消執行并訪問viewB,滿足執行,則從右往左開始訪問其子視圖(視圖層級從下往上),找到viewB.1,它沒有子視圖,則返回自己。最終UIWindow對象將viewB.1作為hitTest視圖返回給UIApplication對象。
hit-testing流程圖

可以看到,當某一視圖收到hitTest:withEvent:方法后,它會向所有子視圖發送hitTest:withEvent:方法,若它的沒有子視圖或所有子視圖返回nil,那么就返回自己,所有hit-testing流程最終一定會找到一個對象UIView/UIWindow去接收觸摸事件。
以下是hitTest:withEvent:可能的實現。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

responder chain

responder chain是UIResponder對象組成的鏈形結構,它以first responder為頭結點,UIApplication對象為尾節點,事件從頭開始在響應鏈中向上傳遞。
UIResponder用來設計處理事件,UIApplication, UIViewController, UIView都是其子類,只要它們實現了UIResponder中的鉤子方法,就可以響應對應的事件。

UIResponder的繼承關系
UIResponder的繼承關系

其中first responder用來第一個接觸事件,可以使用becomeFirstResponder來設置它,主要要在視圖層級已經完全建立之后再設置。

If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO

默認情況下,fist responder是當前UIWindow中最有可能響應事件的UIView,這由UIkit決定。
iOS中大部分的事件都依賴響應鏈來找到最終的響應者,在UIResponder的頭文件中可以看到,Touch events,Motion events,Remote events,UIControl Action,Text editing,press events等事件都可以在響應鏈中傳遞。

尋找響應對象

UIApplication在處理的事件時,觸摸事件會交給hitTest view開始的響應鏈處理,其他的動作事件,遠程事件,系統事件等,會交給first responder開始的響應鏈處理。
UIKit會將用戶事件發送給理論上最合適的對象。所以當程序中的響應者要經過很長的查找路徑時,這時就要考慮是否實現是否設計合理了。

UIKit first sends the event to the object that is best suited to handle the event. For touch events, that object is the hit-test view, and for other events, that object is the first responder

對于觸摸事件,hit-test視圖獲得了最先接受觸摸對象的機會,但如果它不能處理對應的觸摸事件,那么UIKit會沿著以hit-test開頭的響應鏈尋找能夠最終的響應者。


The responder chain on iOS
The responder chain on iOS

當找到響應者或已經到鏈尾(UIApplication)仍不能處理,UIKit會停止查找,對于后者,對應的事件會被丟棄。

除了UIResponder對象,UIGestureRecognizerUIControl也可以響應觸摸事件,但它們參與觸摸事件響應的方式不同。

  1. UIGestureRecognizer在響應鏈中的位置取決于依附的視圖。
  2. UIControl參與響應的方式決定于其關聯的target。
    UIGestureRecognizer要先于視圖收到觸摸事件,但需要注意的是,若該視圖也可以響應觸摸事件(實現了UITouch生命周期函數),那么手勢對象并不會阻礙視圖的響應,雙方是同時響應的,只不過存在先后順序。
    UIGestureRecognizer與UIView的接觸事件的次序
    UIGestureRecognizer與UIView的接觸事件的次序

響應觸摸事件

當確定了響應鏈后,UIWindow會向hitTest View發送以下方法:

- (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;

這是UIResponder用于響應觸摸事件的方法,這些鉤子方法的默認實現是向nextResponder轉發方法。
當觸摸事件在響應鏈上傳遞時,判斷當前UIResponder能否響應的條件是:其是否實現了touchesBegan方法。
在這些UITouches序列的生命周期方法中,我們可以獲取對應UIEventUITouch,利用它們所提供的信息,進一步決定如何響應用戶的觸摸事件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容