iOS點(diǎn)擊事件和手勢沖突

0、緣起

之所以要寫這篇文章,是因?yàn)榘l(fā)現(xiàn)在實(shí)際編程處理點(diǎn)擊事件的過程中,知道響應(yīng)鏈和探測鏈根本沒有一點(diǎn)用處。

即使對于響應(yīng)鏈的流程了然于胸,依然還是無法使點(diǎn)擊事件達(dá)到實(shí)際預(yù)期效果。

所以,仔細(xì)探索了一下,響應(yīng)鏈和手勢識別的關(guān)系。希望,對于網(wǎng)上眾多只寫響應(yīng)鏈的文章,做一個補(bǔ)充。

1、問題場景

1、單擊事件響應(yīng)出現(xiàn)問題

父視圖上添加了一個UITabelView和一個UIButton。

在parentView上添加了UITapGestureRecognizer之后,subview中的UITableView實(shí)例無法正常響應(yīng)點(diǎn)擊事件了,但UIButton實(shí)例仍可以正常工作。

這是怎么回事呢?

解決鏈接:iOS觸摸

2、使用TableView寫了一個登陸界面,帳號和密碼兩個Cell中加入了TextField。

由于想在TableView的空白處,點(diǎn)擊時收起鍵盤,所以給self.view 添加一個UITapGestureRecognizer來識別手勢。

然后發(fā)生了一個奇怪的現(xiàn)象,點(diǎn)擊cell 無法選中,也就是tableView 的 didSelectRowAtIndexPath沒有反應(yīng)了!

解決鏈接:didSelectRowAtIndexPath失效

以上兩個場景問題。都已經(jīng)有解決方案了。但是,底層到底是為什么會有點(diǎn)擊事件的響應(yīng)沖突呢?

這一切的原因都是因?yàn)椋?strong>對于UITapGestureRecognizer認(rèn)識不夠深刻。

先寫下幾個結(jié)論,后面慢慢解釋:(此處只討論單擊tap事件

1、手勢響應(yīng)是大哥,點(diǎn)擊事件響應(yīng)鏈?zhǔn)切〉堋?/strong>單擊手勢優(yōu)先于UIView的事件響應(yīng)。大部分沖突,都是因?yàn)?strong>優(yōu)先級沒有搞清楚。

2、單擊事件優(yōu)先傳遞給手勢響應(yīng)大哥,如果大哥識別成功,就會直接取消事件的響應(yīng)鏈傳遞。

識別成功時候,手勢響應(yīng)大哥擁有壟斷權(quán)力。(在斗地主里面叫做:吃肉淘湯。)

如果大哥識別失敗了,觸摸事件會繼續(xù)走傳遞鏈,傳遞給響應(yīng)鏈小弟處理。

3、手勢識別是需要時間的。

手勢識別有一個狀態(tài)機(jī)的變化。在possible狀態(tài)的時候,單擊事件也可能已經(jīng)傳遞給響應(yīng)鏈小弟了。

2、關(guān)于事件的幾個概念

在具體講解上面三個結(jié)論之前。先簡要的介紹一下 iOS 里面與事件相關(guān)的幾個概念 。為了方便理解,我用了比喻的方法。

1、 UITouch —— 一指禪

當(dāng)你用一根手指觸摸屏幕時, 會創(chuàng)建一個與之關(guān)聯(lián)的UITouch對象,

一個手指第一次點(diǎn)擊屏幕,就會生成一個UITouch對象,到手指離開時銷毀。

**一個UITouch對象對應(yīng)一根手指. **。所以可以直接,想象成是神功——一指禪。

2、UIEvent —— 如來神掌 (多個手指)

一個UIEvent 事件定義為:第一個手指開始觸摸屏幕到最后一個手指離開屏幕。

一個UIEvent對象實(shí)際上對應(yīng)多個UITouch對象。所以,一個UIEvent事件,可以簡單的想象成是神功:如來神掌。(只是形象表示多個手指而已,不必要5個UITouch事件組合。)

3、 UIResponder — 響應(yīng)對象

在iOS中不是任何對象都能處理事件, 只有繼承了UIResponder的對象才能接收并處理事件,我們稱為響應(yīng)者對象。

UIApplication,UIViewController,UIView都繼承自UIResponder,因此他們都是響應(yīng)者對象, 都能夠接收并處理事件。

也就是說iOS中 所有的UIView一旦成為響應(yīng)者對象,都是可以響應(yīng)單擊的觸摸事件的。

本文不詳細(xì)介紹響應(yīng)鏈傳遞及響應(yīng)的知識了。

重點(diǎn)放在——單擊事件,手勢識別和響應(yīng)鏈之間的糾纏。

4、手勢識別 UIGestureRecognizer

手勢是Apple提供的更高級的事件處理技術(shù),可以完成更多更復(fù)雜的觸摸事件,比如旋轉(zhuǎn)、滑動、長按等?;愂荱IGestureRecognizer。

UIGestureRecognizer同UIResponder一樣也有四個方法

//UIGestureRecognizer
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent: (nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

需要注意的是UIGestureRecognizer,是有狀態(tài)的變化的。同一個手勢是有具有多個狀態(tài)的變化的,會形成一個有限狀態(tài)機(jī)。

如下圖:

手勢狀態(tài)機(jī).png

左側(cè)是非連續(xù)手勢(比如單擊)的狀態(tài)機(jī),右側(cè)是連續(xù)手勢(比如滑動)的狀態(tài)機(jī)。

所有的手勢的開始狀態(tài)都是UIGestureRecognizerStatePossible。

非連續(xù)的手勢要么識別成功(UIGestureRecognizerStateRecognized),要么識別失敗(UIGestureRecognizerStateFailed)。

連續(xù)的手勢識別到第一個手勢時,變成UIGestureRecognizerStateBegan,然后變成UIGestureRecognizerStateChanged,并且不斷地在這個狀態(tài)下循環(huán),當(dāng)用戶最后一個手指離開view時,變成UIGestureRecognizerStateEnded,

當(dāng)然如果手勢不再符合它的模式的時候,狀態(tài)也可能變成UIGestureRecognizerStateCancelled。

3、手勢識別與事件響應(yīng)混用

重點(diǎn)來了。

iOS處理觸屏事件,分為兩種方式:

  1. 高級事件處理:利用UIKit提供的各種用戶控件或者手勢識別器來處理事件。

  2. 低級事件處理:在UIView的子類中重寫觸屏回調(diào)方法,直接處理觸屏事件。

這兩種方式會在,單擊觸摸事件的時候得到使用。

觸摸事件可以通過響應(yīng)鏈來傳遞與處理,也可以被綁定在view上的手勢識別和處理。那么這兩個一起用會出現(xiàn)什么問題?

如果回答了這個問題,就可以說清楚,開篇的兩個問題了。

此處的DEMO例子參考。iOS點(diǎn)擊事件和手勢沖突

下面開始例子的講解。Demo鏈接:testTap
圖片:

demo.png

圖中baseView 有兩個subView,分別是testView和testBtn。我們在baseView和testView都重載touchsBegan:withEvent、touchsEnded:withEvent、
touchsMoved:withEvent、touchsCancelled:withEvent方法,并且在baseView上添加單擊手勢,action名為tapAction,給testBtn綁定action名為testBtnClicked。
主要代碼如下:

//baseView
- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
    [self.view addGestureRecognizer:tap];
    ...
    [_testBtn addTarget:self action:@selector(testBtnClicked) forControlEvents:UIControlEventTouchUpInside];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"=========> base view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     NSLog(@"=========> base view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     NSLog(@"=========> base view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     NSLog(@"=========> base view touchs Cancelled");
}
- (void)tapAction {
     NSLog(@"=========> single Tapped");
}
- (void)testBtnClicked {
     NSLog(@"=========> click testbtn");
}
//test view
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"=========> test view touchs Began");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"=========> test view touchs Moved");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"=========> test view touchs Ended");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"=========> test view touchs Cancelled");
}

情景A :單擊baseView,輸出結(jié)果為:

=========> base view touchs Began
=========> single Tapped
=========> base view touchs Cancelled

情景B :單擊testView,輸出結(jié)果為:

=========> test view touchs Began
=========> single Tapped
=========> test view touchs Cancelled

情景C :單擊testBtn, 輸出結(jié)果為:

=========> click testbtn

情景D :按住testView,過5秒后或更久釋放,輸出結(jié)果為:

=========> test view touchs Began
=========> test view touchs Ended

1、情景A和B

情景A和B,都是在單擊之后,既響應(yīng)了手勢的tap 事件,也讓響應(yīng)鏈方法執(zhí)行了。為什么兩個響應(yīng)都執(zhí)行了呢?

看看開發(fā)文檔,就應(yīng)該可以理解了。

Gesture Recognizers Get the First Opportunity to Recognize a Touch.

A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.

Google翻譯:

手勢識別器獲得識別觸摸的第一個機(jī)會。

一個窗口延遲將觸摸對象傳遞到視圖,使得手勢識別器可以首先分析觸摸。 在延遲期間,如果手勢識別器識別出觸摸手勢,則窗口不會將觸摸對象傳遞到視圖,并且還將先前發(fā)送到作為識別的序列的一部分的視圖的任何觸摸對象取消。

圖片:


手勢圖片.png

觸摸事件首先傳遞到手勢上,如果手勢識別成功,就會取消事件的繼續(xù)傳遞,否則,事件還是會被響應(yīng)鏈處理。具體地,系統(tǒng)維持了與響應(yīng)鏈關(guān)聯(lián)的所有手勢,事件首先發(fā)給這些手勢,然后再發(fā)給響應(yīng)鏈。

這樣可以解釋情景A和B了。

首先,我們的單擊事件,是有有手勢識別這個大哥來優(yōu)先獲取。只不過,手勢識別是需要一點(diǎn)時間的。在手勢還是Possible 狀態(tài)的時候,事件傳遞給了響應(yīng)鏈的第一個響應(yīng)對象(baseView 或者 testView)。

這樣自然就去調(diào)用了,響應(yīng)鏈UIResponder的touchsBegan:withEvent方法,之后手勢識別成功了,就會去cancel之前傳遞到的所有響應(yīng)對象,于是就會調(diào)用它們的touchsCancelled:withEvent:方法。

2、情境C

好了,情景A和B都可以解釋明白了。但是,請注意,按這樣的解釋為什么情景C沒有觸發(fā)響應(yīng)鏈的方法呢?

這里可以說是事件響應(yīng)的一個特例。

iOS 開發(fā)文檔里這樣說:

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:

A single finger single tap on a UIButton, UISwitch, UISegmentedControl, UIStepper,and UIPageControl.A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

Google 翻譯為:

在iOS 6.0及更高版本中,默認(rèn)控制操作可防止重疊的手勢識別器行為。 例如,按鈕的默認(rèn)操作是單擊。 如果您有一個單擊手勢識別器附加到按鈕的父視圖,并且用戶點(diǎn)擊按鈕,則按鈕的動作方法接收觸摸事件而不是手勢識別器。 這僅適用于與控件的默認(rèn)操作重疊的手勢識別,其中包括:

單個手指單擊UIButton,UISwitch,UISegmentedControl,UIStepper和UIPageControl.

單個手指在UISlider的旋鈕上滑動,在平行于滑塊的方向上。在UISwitch的旋鈕上的單個手指平移手勢 與開關(guān)平行的方向。

所以呢,在情境C,里面testBtn的 默認(rèn)action,獲取了事件響應(yīng),不會把事件傳遞給父視圖baseView,自然就不會觸發(fā),baseView的tap 事件了。

3、情境D

在情景D中,由于長按住testView不釋放,tap手勢就會識別失敗,因?yàn)殚L按就已經(jīng)不是單擊事件了。手勢識別失敗之后,就可以繼續(xù)正常傳遞給testView處理。

所以,只有響應(yīng)鏈的方法觸發(fā)了。

4、實(shí)際開發(fā)遇到的問題解決

基本的開發(fā)目標(biāo),不讓父視圖的手勢識別干擾子視圖UIView的點(diǎn)擊事件響應(yīng)或者說響應(yīng)鏈的正常傳遞。

一般都會是重寫UIGestureRecognizerDelegate中的- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch方法。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
{  
     // 若為UITableViewCellContentView(即點(diǎn)擊了tableViewCell),
    if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) {  
    // cell 不需要響應(yīng) 父視圖的手勢,保證didselect 可以正常
        return NO;  
    }  
    //默認(rèn)都需要響應(yīng)
    return  YES;  
}

1、iOS開發(fā)中讓子視圖不響應(yīng)父視圖的手勢識別器

2、iOS單擊響應(yīng),UIControl

3、didSelectRowAtIndexPath失效

4、以上例子的Demo鏈接:testTap;

小結(jié)

復(fù)習(xí)一下結(jié)論:

(此處只討論單擊tap事件

1、手勢響應(yīng)是大哥,點(diǎn)擊事件響應(yīng)鏈?zhǔn)切〉堋?/strong>單擊手勢優(yōu)先于UIView的事件響應(yīng)。大部分沖突,都是因?yàn)?strong>優(yōu)先級沒有搞清楚。

2、單擊事件優(yōu)先傳遞給手勢響應(yīng)大哥,如果大哥識別成功,就會直接取消事件的響應(yīng)鏈傳遞。

識別成功時候,手勢響應(yīng)大哥擁有壟斷權(quán)力。(在斗地主里面叫做:吃肉淘湯。)

如果大哥識別失敗了,觸摸事件會繼續(xù)走傳遞鏈,傳遞給響應(yīng)鏈小弟處理。

3、手勢識別是需要時間的。
手勢識別有一個狀態(tài)機(jī)的變化。在possible狀態(tài)的時候,單擊事件也可能已經(jīng)傳遞給響應(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)容

  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點(diǎn)擊了?糾結(jié)于如何實(shí)現(xiàn)這個奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 59,435評論 51 604
  • 在iOS開發(fā)中經(jīng)常會涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,237評論 4 26
  • -- iOS事件全面解析 概覽 iPhone的成功很大一部分得益于它多點(diǎn)觸摸的強(qiáng)大功能,喬布斯讓人們認(rèn)識到手機(jī)其實(shí)...
    翹楚iOS9閱讀 3,201評論 0 13
  • 本文主要想講的是觸摸事件和手勢混合使用的一個問題,但作為知識儲備,還是把兩者再單獨(dú)介紹一下。兩者的基本知識點(diǎn)都是i...
    foolishBoy閱讀 10,332評論 9 36
  • Android MVP View是一個接口,負(fù)責(zé)被動的把處理好的數(shù)據(jù)顯示出來 Model也是一個接口,負(fù)責(zé)獲取數(shù)據(jù)...
    我就是我不一樣的水果閱讀 609評論 0 2

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