Swift开发

iOS 触摸事件的探索

2021-05-21  本文已影响0人  Sweet丶

iOS屏幕触摸事件的处理对于APP来说是很重要的,如果我们只了解监听UIControl类的点击事件或者手势事件的话, 我们只能做简单的点击响应处理, 对于用户体验有较高的要求时就解决不了,比如饼状图点击区域、扩大小按钮的响应区域、UIScrollView与右滑返回手势冲突的问题.

一、View上的触摸事件

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

响应者链
触摸事件分为查找最佳响应用户触摸点视图的过程响应事件沿响应者链传递的过程
1. 查找最佳响应用户触摸点视图的过程

系统基于Port的进程间通信交给当前的Application -> 从可见的最顶层的UIWindow开始遍历内部window -> window内部UIView通过- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;这个方法来返回最佳响应者。

1.1 hitTest方法的默认实现
//作用:去寻找最适合的View
//什么时候调用:当一个事件传递给当前View,就会调用.
//返回值:返回的是谁,谁就是最适合的View(就会调用最适合的View的touch方法)
-(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方法中打断点。查看函数调用情况如下:

hitTest方法调用前函数调用情况.png
能看出的是:首先点击传递到主线程时是一个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中打断点查看函数调用情况:

找到最合适响应view之后的sendEvent.png

可以看到是:
[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上
2.0 触摸事件在手势识别上是怎么发生的?

我们新建一个UITapGestureRecognizer子类ZLTestGesure,重写里面的touchesBegan的那四个方法。
接下来创建demo:控制器view添加红色view,红色view添加绿色view,红色view添加手势.

demo的情况.png
点击绿色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:方法打断点:

系统识别到了手势事件.png
2.1 手势发生时,手势怎么调用到target-action这一步

手势初始化都有对应的target-action,在添加在view上后,触摸事件的查找最合适响应view的过程不受影响,而是在识别到一个手势时:
1.找到手势初始化对应的target-action执行。
2.让最合适响应view调用touchesCancelled:withEvent: ,view不再处理这个触摸事件了。
3.如果view上的子view上有添加相同的手势,则子view上的手势会优先识别出;如果是不同的手势,那就看这次触摸优先被哪个手势识别到,与父视图子视图层级无关,手势识别出来后,对于这次的触摸系统不会再去识别手势(默认情况)

2.2 如何控制多个手势识别之间的关系

可以给手势设置代理,通过代理方法来控制:

// 是否允许多个手势同时进行识别,返回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;
2.3 UIScrollView上的手势

UIScrollView内部默认封装了两个手势 pan和pinch和两个重要的属性delaysContentTouchescanCancelContentTouches
delaysContentTouches:默认值为YES;如果设置为NO,则无论手指移动的多么快,始终都会将触摸事件传递给内部控件;设置为NO可能会影响到UIScrollView的滚动功能。
canCancelContentTouches:如果属性值为YES并且跟踪到手指正触摸到一个内容控件,这时如果用户拖动手指的距离足够产生滚动,那么内容控件将收到一个touchesCancelled:withEvent:消息,而scrollview将这次触摸作为滚动来处理。如果值为NO,一旦contentview开始跟踪(tracking==YES),则无论手指是否移动,scrollView都不会滚动。

2.4 我们可以创建自定义手势

继承自UIGestureRecognizer,详情查看API之UIGestureRecognizer及自定义手势

三、如果有UIControl情况下,事件的传递会有什么不同?
3.1 UIControl的作用

UIControl是继承于UIView的,相比UIView,UIControl能识别特定的触摸事件,使我们能对特定的事件比如UIControlEventTouchUpInside添加Target-action; 首先我们知道,如果手势添加在UIControl上,那么手势会优先识别出,识别后会打断UIControl的事件传递,下面我们来看手势是添加在父视图上的情况。
测试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事件");
    }
}
UIContol的事件识别测试.png

点击蓝色的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的事件传递的。

3.2 UIControl识别出了UIControlEventTouch事件后的方法调用
UIControl事件识别后的发送.png

-> 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对象收到方法调用。
从这里可以知道:手势识别到之后不会发送给UIApplication, 而是直接让target调用对应的action,这是与UIControl不同的地方.
所以我们如果是埋点事件是在自定义UIApplication类里面做的话,我们是收集不到手势点击事件的。

上一篇 下一篇

猜你喜欢

热点阅读