iOS 觸摸事件的處理層次及原理

iOS 事件包括:運動事件、遠程控制事件、觸摸事件。

其中觸摸事件的響應(yīng)流程是:當手指觸摸屏幕時,會產(chǎn)生一個事件,事件發(fā)生會被系統(tǒng)封裝為一個 IOHIDEvent對象,傳遞給 SpringBoard(桌面),然后將這個桌面進程傳遞給當前 app。app 的主線程 Runloop 會被觸發(fā),因此會觸發(fā) Source1回調(diào),Source1回調(diào)內(nèi)部觸發(fā) Source0 回調(diào),然后傳遞給 UIWindow,app 內(nèi)部開始傳遞和響應(yīng)。

事件傳遞與響應(yīng)

事件傳遞流程:通過hitTestpointInside來反序遍歷找到正確的view。
從UIWindow開始,反向遍歷 UIWindow 的 subviews。從最后一個 subview 開始,判斷點擊區(qū)域是不是在當前 view 的 bounds 范圍內(nèi)。如果是(pointInside 返回 YES),反向遍歷當前 view 的 subviews,如果沒有 subviews,則當前 view 是需要的 view;如果不是(pointInside 返回 YES), 查找當前 view 的兄弟 view。這是一個遞歸查找過程。

pointInside:

  • 如果返回 NO,則不會遍歷該 view 的 subviews;
  • 觸摸的點不在范圍內(nèi),找兄弟級別,在范圍內(nèi),找兒子級別。

hitTest:

  • 會調(diào)用pointInside
  • 根據(jù)pointInside的值做不同的處理:如果pointInside為 YES,則反序遍歷 subviews;如果不是,返回 nil。
  • 如果點擊區(qū)域在自己這里,而不是在子 view 里面,返回自己;
  • 如果點擊區(qū)域在子 view 里,返回子 view。

注意:如果 view 的alpha 小于等于0.01,或者 userInteractionEnabled 屬性為 NO,或者 hidden 為 YES 時,view 不響應(yīng)事件。

事件響應(yīng):

每個view都有一個nextResponder對象,這樣就逐級串聯(lián)起一個響應(yīng)鏈。

有了響應(yīng)鏈,會進行touch四個方法的傳遞響應(yīng);可以通過不寫 super touchBegin/moved/cancel/end 來阻止傳遞流程。
scrollView的 touch 事件傳遞不了,因為系統(tǒng)會重寫 scrollView 的 super touch:,導(dǎo)致無法向上傳遞。

手勢

手勢和 pointInside 及 hitTest 的關(guān)系:

  • 必須通過 pointInside 和 hitTest 找到 view,才能響應(yīng) view 的手勢事件;

  • 通過 pointInside 、 hitTest 找到的 view,其自身和它的 superview 的手勢都能響應(yīng),但不會響應(yīng)其子 view 的手勢;

  • 手勢的種類根據(jù)收拾自己的 touchs 的四個方向辨別;

  • 默認情況下,如果手勢識別出來了,會 cancel 掉 view 的 touch 事件。

  • 修改 delaysContentTouches canCancelContentTouches 的值可使手勢和 touch 共存。
    scrollView.delaysContentTouches = NO; // 如果為 YES,阻止 touch 方法的識別
    scrollView.canCancelContentTouches = NO;
    這兩個屬性可實現(xiàn)scrollView上的其他view滑動時,scrollView不滑動

button 事件

  • button 和 pointInside 及 hitTest 的關(guān)系:hitTest 必須有返回的 button,才能響應(yīng)事件。
  • button 不同事件的識別也是通過四個 touch 方法分辨的。
  • button 的 sendActionForControlEvents: 方法可以識別事件。

如果想讓 button 的響應(yīng)區(qū)域變大,只需要在想要的區(qū)域里,pointInside 返回為 YES 就可以。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    
    CGRect rect = self.bounds;
    
    if (rect.size.width < 50.f) {
        rect.origin.x -= (50.f - rect.size.width) / 2;
    }
    if (rect.size.height < 50.f) {
        rect.origin.y -= (50.f - rect.size.height) / 2;
    }
    
    rect.size.width = 50.f;
    rect.size.height = 50.f;
    
    if (CGRectContainsPoint(rect, point)) {
        return YES;
    }
    
    return [super pointInside:point withEvent:event];
}

如果一個 button 超出了其父控件的范圍,要讓超出范圍的區(qū)域響應(yīng) button 的點擊,則需要在其父類的pointInside方法里判斷。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    
    UIView *subview = self.subviews[0];
    CGPoint convertedPoint = [self convertPoint:point toView:subview];
    if ([subview pointInside:convertedPoint withEvent:event]) {
        return YES;
    }
    
    return [super pointInside:point withEvent:event];
}

手勢的互斥與共存

  1. 通過 UIGestureRecognizerDelegate 的代理方法操作
/**
 手勢已經(jīng)識別,通過返回值確定是否響應(yīng)
 */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    return YES;
}

/**
 兩個手勢是否共存,只要其中一個手勢的代理d方法返回 YES,則兩個手勢共存
 */
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return NO;
}

/**
 gestureRecognizer 要響應(yīng),必須 otherGestureRecognizer 響應(yīng)失敗,otherGestureRecognizer 優(yōu)先級高
 */
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}
/**
 otherGestureRecognizer 要響應(yīng),必須 gestureRecognizer 響應(yīng)失敗,gestureRecognizer 優(yōu)先級高
 */
//- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
//    return YES;
//}

/**
 手勢是否支持 touch
 */
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    return YES;
}
  1. 讓其中一個手勢的優(yōu)先級變高A requireGestureRecognizerToFail:B(讓 B 優(yōu)先級更高)。

UITableView 的 didselect 方法是通過 touch 起作用,如果給 tableview 的父控件添加手勢,則 tableview 的 didselect 方法不會實現(xiàn)。原因是 tableview 的 touch 事件和父控件的手勢沖突,根本原因是手勢的 cancelsTouchesInView 的值為 YES。

cancelsTouchesInView 的作用是當手勢識別出來時,會取消touch事件。

解決以上實例的途徑是將手勢的 cancelsTouchesInView 的值設(shè)為 NO。此時手勢和 tableview 的 touch 都會響應(yīng)。

要 didselect 方法起作用而手勢不響應(yīng),應(yīng)該在手勢的代理方法 shouldReceiveTouch 中判斷 touch 的 view 的種類。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    UIView *view = touch.view;
    return ![view isKindOfClass:NSClassFromString(@"UITableViewCellContentView")];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容