iOS點擊事件和手勢沖突

0、緣起

之所以要寫這篇文章,是因為發現在實際編程處理點擊事件的過程中,知道響應鏈和探測鏈根本沒有一點用處。

即使對于響應鏈的流程了然于胸,依然還是無法使點擊事件達到實際預期效果。

所以,仔細探索了一下,響應鏈和手勢識別的關系。希望,對于網上眾多只寫響應鏈的文章,做一個補充。

1、問題場景

1、單擊事件響應出現問題

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

在parentView上添加了UITapGestureRecognizer之后,subview中的UITableView實例無法正常響應點擊事件了,但UIButton實例仍可以正常工作。

這是怎么回事呢?

解決鏈接:iOS觸摸

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

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

然后發生了一個奇怪的現象,點擊cell 無法選中,也就是tableView 的 didSelectRowAtIndexPath沒有反應了!

解決鏈接:didSelectRowAtIndexPath失效

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

這一切的原因都是因為:對于UITapGestureRecognizer認識不夠深刻

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

1、手勢響應是大哥,點擊事件響應鏈是小弟。單擊手勢優先于UIView的事件響應。大部分沖突,都是因為優先級沒有搞清楚。

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

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

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

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

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

2、關于事件的幾個概念

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

1、 UITouch —— 一指禪

當你用一根手指觸摸屏幕時, 會創建一個與之關聯的UITouch對象,

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

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

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

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

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

3、 UIResponder — 響應對象

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

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

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

本文不詳細介紹響應鏈傳遞及響應的知識了。

重點放在——單擊事件,手勢識別和響應鏈之間的糾纏

4、手勢識別 UIGestureRecognizer

手勢是Apple提供的更高級的事件處理技術,可以完成更多更復雜的觸摸事件,比如旋轉、滑動、長按等。基類是UIGestureRecognizer。

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,是有狀態的變化的。同一個手勢是有具有多個狀態的變化的,會形成一個有限狀態機

如下圖:

手勢狀態機.png

左側是非連續手勢(比如單擊)的狀態機,右側是連續手勢(比如滑動)的狀態機。

所有的手勢的開始狀態都是UIGestureRecognizerStatePossible。

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

連續的手勢識別到第一個手勢時,變成UIGestureRecognizerStateBegan,然后變成UIGestureRecognizerStateChanged,并且不斷地在這個狀態下循環,當用戶最后一個手指離開view時,變成UIGestureRecognizerStateEnded,

當然如果手勢不再符合它的模式的時候,狀態也可能變成UIGestureRecognizerStateCancelled。

3、手勢識別與事件響應混用

重點來了。

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

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

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

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

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

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

此處的DEMO例子參考。iOS點擊事件和手勢沖突

下面開始例子的講解。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,輸出結果為:

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

情景B :單擊testView,輸出結果為:

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

情景C :單擊testBtn, 輸出結果為:

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

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

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

1、情景A和B

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

看看開發文檔,就應該可以理解了。

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翻譯:

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

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

圖片:


手勢圖片.png

觸摸事件首先傳遞到手勢上,如果手勢識別成功,就會取消事件的繼續傳遞,否則,事件還是會被響應鏈處理。具體地,系統維持了與響應鏈關聯的所有手勢,事件首先發給這些手勢,然后再發給響應鏈。

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

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

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

2、情境C

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

這里可以說是事件響應的一個特例

iOS 開發文檔里這樣說:

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及更高版本中,默認控制操作可防止重疊的手勢識別器行為。 例如,按鈕的默認操作是單擊。 如果您有一個單擊手勢識別器附加到按鈕的父視圖,并且用戶點擊按鈕,則按鈕的動作方法接收觸摸事件而不是手勢識別器。 這僅適用于與控件的默認操作重疊的手勢識別,其中包括:

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

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

所以呢,在情境C,里面testBtn的 默認action,獲取了事件響應,不會把事件傳遞給父視圖baseView,自然就不會觸發,baseView的tap 事件了。

3、情境D

在情景D中,由于長按住testView不釋放,tap手勢就會識別失敗,因為長按就已經不是單擊事件了。手勢識別失敗之后,就可以繼續正常傳遞給testView處理。

所以,只有響應鏈的方法觸發了。

4、實際開發遇到的問題解決

基本的開發目標,不讓父視圖的手勢識別干擾子視圖UIView的點擊事件響應或者說響應鏈的正常傳遞。

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

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

1、iOS開發中讓子視圖不響應父視圖的手勢識別器

2、iOS單擊響應,UIControl

3、didSelectRowAtIndexPath失效

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

小結

復習一下結論:

(此處只討論單擊tap事件

1、手勢響應是大哥,點擊事件響應鏈是小弟。單擊手勢優先于UIView的事件響應。大部分沖突,都是因為優先級沒有搞清楚。

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

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

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

3、手勢識別是需要時間的。
手勢識別有一個狀態機的變化。在possible狀態的時候,單擊事件也可能已經傳遞給響應鏈小弟了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容