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

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

iOS中的事件

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

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

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

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

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

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

iOS中的觸摸事件

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

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

iOS的觸摸事件響應(yīng)機(jī)制

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

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

iOS的觸摸事件響應(yīng)機(jī)制

hit-testing流程

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

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

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

hit-testing

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

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

- (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對象組成的鏈形結(jié)構(gòu),它以first responder為頭結(jié)點(diǎn),UIApplication對象為尾節(jié)點(diǎn),事件從頭開始在響應(yīng)鏈中向上傳遞。
UIResponder用來設(shè)計處理事件,UIApplication, UIViewController, UIView都是其子類,只要它們實現(xiàn)了UIResponder中的鉤子方法,就可以響應(yīng)對應(yīng)的事件。

UIResponder的繼承關(guān)系
UIResponder的繼承關(guān)系

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

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

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

尋找響應(yīng)對象

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

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視圖獲得了最先接受觸摸對象的機(jī)會,但如果它不能處理對應(yīng)的觸摸事件,那么UIKit會沿著以hit-test開頭的響應(yīng)鏈尋找能夠最終的響應(yīng)者。


The responder chain on iOS
The responder chain on iOS

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

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

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

響應(yīng)觸摸事件

當(dāng)確定了響應(yīng)鏈后,UIWindow會向hitTest View發(fā)送以下方法:

- (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用于響應(yīng)觸摸事件的方法,這些鉤子方法的默認(rèn)實現(xiàn)是向nextResponder轉(zhuǎn)發(fā)方法。
當(dāng)觸摸事件在響應(yīng)鏈上傳遞時,判斷當(dāng)前UIResponder能否響應(yīng)的條件是:其是否實現(xiàn)了touchesBegan方法。
在這些UITouches序列的生命周期方法中,我們可以獲取對應(yīng)UIEventUITouch,利用它們所提供的信息,進(jìn)一步?jīng)Q定如何響應(yīng)用戶的觸摸事件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容