iOS屏幕接触事情的处理关于APP来说是很重要的,假如咱们只了解监听UIControl类的点击事情或者手势事情的话, 咱们只能做简单的点击呼应处理, 关于用户体验有较高的要求时就不好解决,比方饼状图点击区域、扩巨细按钮的呼应区域、UIScrollView与右滑回来手势冲突的问题.

一、View上的接触事情

UIView承继于UIResponder, 关于每个视图都能经过链式调用nextResponder找到一条呼应者链,这个是视图增加到操控器窗口时就现已存在了,如下是一个典型的呼应者链。

iOS 触摸事件的传递和手势识别探索

接触事情分为查找最佳呼应用户接触点视图的进程跟呼应事情沿呼应者链传递的进程。

1. 查找最佳呼应用户接触点视图的进程

体系根据Port的进程间通信交给当时的Application -> 从可见的最顶层的UIWindow开端遍历内部window -> window内部UIView经过- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;这个办法来回来最佳呼应者。

1.1 hitTest办法的默许实现

//作用: 去寻找最适合的View
//什么时分调用: 当一个事情传递给当时View,就会调用.
//回来值: 回来的是谁,谁便是最适合的View(就会调用最适合的View的touchBegin系列办法)
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
   //1.判别自己能否接收事情
    if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
        return nil;
    }
    //2.判别当时点在不在当时View.
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    //3.从后往前遍历自己的子控件.让子控件重复前两步操作,(把事情传递给,让子控件调用hitTest)
    int count = (int)self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
        //取出每一个子控件
        UIView *chileV =  self.subviews[i];
        //把当时的点转换成子控件坐标系上的点.
        CGPoint childP = [self convertPoint:point toView:chileV];
        UIView *fitView = [chileV hitTest:childP withEvent:event];
        //判别有没有找到最适合的View
        if(fitView){
            return fitView;
        }
    }
    //4.没有找到比它自己更适合的View.那么它自己便是最适合的View
    return self;
}

1.2. 扩展呼应区域的问题

比方当咱们列表中有个小的保藏按钮,要扩展呼应区域,咱们根据上面的了解,咱们能够这样做,在保藏按钮的父视图重写-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event, 假如点击的point在point外加扩展的区域内,则将保藏按钮上的center赋值给point,再调用[super hitTest:point.....]即可.

1.3 写demo调试检查寻找最佳呼应者的进程

在操控器的view中增加一个自定义view,在自定义view的-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event办法中打断点。检查函数调用状况如下:

iOS 触摸事件的传递和手势识别探索
能看出的是:首要点击传递到主线程时是一个souce0事情,接下来会找到最适合window(查找的细节暂时不知道,体系调用的是私有办法_),接下来window让内部的view调用-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event.

1.4 hitTest:办法会调用两遍是为什么?

Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.

翻译为:

是的,这很正常。体系可能会在两次呼叫之间调整被命中测验的点。由于hitTest应该是一个没有副作用的纯函数,所以这应该很好。

2. 沿呼应者链传递的进程

在找到了最合适的view之后,咱们经过在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event中打断点检查函数调用状况:

iOS 触摸事件的传递和手势识别探索

能够看到是: [UIApplication sendEvent:] -> [UIWindow sendEvent:] -> [UIWindow _sendTouchesForEvent:] -> [ZLView touchesBegan:withEvent:] -> 假如ZLView的touchesBegan:withEvent:内有调用super -> ZLView的nextResponder调用touchesBegan:withEvent:] -> nextResponder...依次类推

假如咱们在自定义view中的touchesBan办法不调用super,这样就拦截掉了之后的事情传递(不会拦截掉手势的),即他的nextResponder之后都收不到下面4个办法的调用了:

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

二、假如是有手势增加在view上

1. 接触事情在手势辨认上是怎样发生的?

咱们新建一个UITapGestureRecognizer子类ZLTestGesure,重写里边的touchesBegan的那四个办法。 接下来创立demo:操控器view增加赤色view,赤色view增加绿色view,赤色view增加手势.

iOS 触摸事件的传递和手势识别探索

点击绿色view,检查打印状况:

// 由上往下看打印台
-[ZLView hitTest:withEvent:]
-[ZLGreenView hitTest:withEvent:]
-[ZLView hitTest:withEvent:]
-[ZLGreenView hitTest:withEvent:]
-[ZLTestGesure touchesBegan:withEvent:]
-[ZLGreenView touchesBegan:withEvent:]
-[ZLView touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ZLTestGesure touchesEnded:withEvent:]
-[ZLView tap:]
-[ZLGreenView touchesCancelled:withEvent:]
-[ZLView touchesCancelled:withEvent:]
-[ViewController touchesCancelled:withEvent:]

能够看到:

1.手势的touchesBegan:withEvent:办法是优先于一切view调用的, (UIControl的优先级会更高,这儿说的是UIView),咱们点击point在绿色view,而绿色view是赤色view的subview,手势是增加在赤色view上的, 咱们的打印结果却是手势的touchesBegan优先于子View上的。

2.手势辨认到之后view上接触会取消。

接下来,咱们在绿色view也增加手势的tap:办法打断点:

iOS 触摸事件的传递和手势识别探索
能够看到接触事情发生时,体系会调用sendEvent:办法,event传递给体系手势辨认器后辨认为了tap手势,然后调用该手势的target-action。

2. 手势发生时,手势怎样调用到target-action这一步

手势初始化都有对应的target-action,在增加在view上后,接触事情的查找最合适呼应view的进程不受影响,而是在辨认到一个手势时:

1.找到手势初始化对应的target-action执行。

2.让最合适呼应view调用touchesCancelled:withEvent: ,view不再处理这个接触事情了。

3.假如view上的子view上有增加相同的手势,则子view上的手势会优先辨认出;假如是不同的手势,那就看这次接触优先被哪个手势辨认到,与父视图子视图层级无关,手势辨认出来后,关于这次的接触体系不会再去辨认手势 (默许状况)

3. 怎么操控多个手势辨认之间的联系

能够给手势设置署理,经过署理办法来操控:

// 是否允许多个手势一起进行辨认,回来YES时,多个手势事情的辨认互不搅扰
// 默许是NO的
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
// 当自己手势进行辨认时是否让别的手势失利,回来YES时,只辨认自己的手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
// 当有其它手势辨认时,自己的手势辨认是否要设置为失利。回来YES是当有别的手势,自己就失利
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

4. UIScrollView上的手势

UIScrollView内部默许封装了两个手势 pan和pinch和两个重要的属性delaysContentTouchescanCancelContentTouches

  • delaysContentTouches: 默许值为YES;假如设置为NO,则无论手指移动的多么快,始终都会将接触事情传递给内部控件;设置为NO可能会影响到UIScrollView的滚动功能。
  • canCancelContentTouches: 默许值为YES, 并且盯梢到手指正接触到一个内容控件,这时假如用户拖动手指的间隔足够发生滚动,那么内容控件将收到一个touchesCancelled:withEvent:消息,而scrollview将这次接触作为滚动来处理。假如值为NO,一旦contentview开端盯梢(tracking==YES),则无论手指是否移动,scrollView都不会滚动。

5. 咱们能够创立自定义手势

承继自UIGestureRecognizer,概况检查API之UIGestureRecognizer及自定义手势

三、假如有UIControl状况下,事情的传递会有什么不同?

1. UIControl的作用

UIControl是承继于UIView的,相比UIView,UIControl能辨认特定的接触事情,使咱们能对特定的事情比方UIControlEventTouchUpInside增加Target-action.

首要, 咱们知道,假如手势增加在UIControl上,那么手势会优先辨认出,辨认后会打断UIControl的事情传递.

下面, 咱们来看将手势增加在UIControl的SuperView上的状况。

测验demo:我把上述的绿色view换成一个承继于UIControl的蓝色view, 并且增加一切UIControl特定事情辨认的监听, 手势是增加在父视图上的时分。

[blueView addTarget:self action:@selector(btnClick:events:) forControlEvents:UIControlEventAllEvents];
- (void)btnClick:(UIButton *)sender events:(UIControlEvents)controlEvents{
    if (controlEvents & UIControlEventTouchDown) {
        NSLog(@"监听到了UIControlEventTouchDown事情");
    }else if (controlEvents & UIControlEventTouchDownRepeat) {
        NSLog(@"监听到了UIControlEventTouchDownRepeat事情");
    }else if (controlEvents & UIControlEventTouchDragInside) {
        NSLog(@"监听到了UIControlEventTouchDragInside事情");
    }else if (controlEvents & UIControlEventTouchDragOutside) {
        NSLog(@"监听到了UIControlEventTouchDragOutside事情");
    }else if (controlEvents & UIControlEventTouchDragEnter) {
        NSLog(@"监听到了UIControlEventTouchDragEnter事情");
    }else if (controlEvents & UIControlEventTouchDragExit) {
        NSLog(@"监听到了UIControlEventTouchDragExit事情");
    }else if (controlEvents & UIControlEventTouchUpInside) {
        NSLog(@"监听到了UIControlEventTouchUpInside事情");
    }else if (controlEvents & UIControlEventTouchUpOutside) {
        NSLog(@"监听到了UIControlEventTouchUpOutside事情");
    }else if (controlEvents & UIControlEventTouchCancel) {
        NSLog(@"监听到了UIControlEventTouchCancel事情");
    }
}

iOS 触摸事件的传递和手势识别探索

点击蓝色的UIControl后看打印作用:

-[ZLBlueView hitTest:withEvent:]
-[ZLView hitTest:withEvent:]
-[ZLBlueView hitTest:withEvent:]
-[ZLTestGesure touchesBegan:withEvent:]
-[ZLBlueView touchesBegan:withEvent:]
监听到了UIControlEventTouchUpOutside事情
-[ZLTestGesure touchesEnded:withEvent:]
-[ZLView tap:]
-[ZLBlueView touchesCancelled:withEvent:]
监听到了UIControlEventTouchUpOutside事情

由上面看出,关于UIControl辨认出了UIControlEventTouch事情并没有打断手势的辨认。手势辨认出来的话是会打断UIControl的事情传递的。

2. UIControl辨认出了UIControlEventTouch事情后的办法调用

iOS 触摸事件的传递和手势识别探索

-> UIControl辨认出一个特定的接触事情

-> UIControl调用- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event办法发送给UIApplication处理

-> UIApplication调用内部- (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event,让事情的target调用对应的action。

-> taget目标收到办法调用。

从这儿能够看出手势与UIControl不同:手势辨认到之后不会发送给UIApplication, 而是直接让target调用对应的action.

所以咱们假如是埋点事情是在自定义UIApplication类里边做的话,咱们是搜集不到手势点击事情的。

至此,探究完结,相信理解了这些后,在实践开发时会对你很有协助~