跳转至

响应者链

监听View的触摸事件

覆写View类的touchBegin、touchMove、touchEnd系列方法监听视图的触摸。

响应者对象UIResponder

只有继承UIResponder的的类,才能处理事件。

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIApplication : UIResponder

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>

@interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>

UIApplication,UIView,UIViewController都是继承自UIResponder类,可以响应和处理事件。

CALayer不是UIResponder的子类,无法处理事件。

事件传递链

AppDelegate根 —>UIApplication单例 —> UIWindow—>FirstViewController对象—>view —>yellowView—>greenView—>blueView

一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:

先摸到硬件(屏幕),屏幕表面的事件会被IOKit先包装成UIEvent,通过mach_Port传给正在活跃的APP,mach_port和source1是一一对应的(key和value),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。

  1. 当触摸一个视图,iOS发生触摸事件后,首先系统会捕捉此事件,并为此事件创建一个UIEvent对象。
  2. 将此UIEvent对象加入当前应用程序的事件队列中。
  3. UIApplication对象从队列中一个一个取出来进行分发
  4. 首先分发给UIWindow对象。
  5. UIWindow再将事件逐步往下层找,即UIView。
  6. UIView首先看自己能否处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图。
  7. 遍历子控件,重复以上两步。直到找到最深层的子视图。
  8. 如果没有找到,那么自己就是事件处理者。也就是第一响应者对象first responder。
  9. 如果自己不能处理,那么不做任何处理。

UIView不接受事件处理的情况主要有以下三种:

  1. alpha < 0.01;
  2. userInteractionEnabled = NO;
  3. hidden = YES;

从父控件到子控件寻找处理事件最合适的view的过程,如果父视图不接受处理(上面三种情况),则子视图也不能接收事件。

事件只要触摸了就会产生,关键在于是否有最合适的view来处理和接受不了事件,如果遍历到最后都没有最合适的view来接收事件,则该事件被废弃。

寻找最合适的view

用到的判断方法是pointInside:withEventhitTest:withEvent

// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判断触摸点是否落在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

事件传递给窗口或控件后,就调用hitTest:withEvent方法寻找更合适的view,如果子控件是合适的view,则在子控件再调用hitTest:withEvent查看子控件是不是合适的view,一直遍历,直到找到最合适的view,或者废弃事件。

// 因为所有的视图类都是继承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判断当前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2. 判断点在不在当前控件
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.从后往前遍历自己的子控件
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        // 把当前控件上的坐标系转换成子控件上的坐标系
        CGPoint childP = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childP withEvent:event];
        if (fitView) { // 寻找到最合适的view
            return fitView;
        }
    }
    // 循环结束,表示没有比自己更合适的view
    return self;
}

hitTest:withEvent应用

1、判断是否回收键盘,取消第一响应者。

tableView的cell上有一个textfiled或者textview,点击背部空白关闭键盘。

利用响应链机制来解决,事件响应链机制主要就是查找事件源(调用hitTest...查找)和反馈给系统处理(调用.nextResponse),所以现在只需在点击tableview的事件反馈给appdelegate处理之前关闭键盘就可以了,这样既关闭了键盘,又不会由于覆盖tableview封装的手势而导致cell的手势操作异常问题。方法步骤如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    id view = [super hitTest:point withEvent:event];
    if (!([view isKindOfClass:[UITextView class]] || [view isKindOfClass:[UITextField class]])) {
        [self endEditing:YES];
    }
    return view;
}

2、轮播图拖动的时候 防止整个页面的滚动

在含有轮播图和tableView的整个的view上添加方法

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (point.y <= kCycleScrollViewH) {//触摸的点在轮播图上
        NSInteger count = self.cycleScrollView.subviews.count;
        for (NSInteger i = count - 1; i >= 0; i--) {
            if ([self.cycleScrollView.subviews[i] isKindOfClass:[UICollectionView class]]) {
              //设置禁止整个view滑动 scrollEnabled = NO;
                [[NSNotificationCenter defaultCenter] postNotificationName:@"cycle" object:nil userInfo:@{@"cycle":@(1)}];
                                break;
            }
        }
    } else {
        //设置允许整个view滑动 scrollEnabled = YES;
        [[NSNotificationCenter defaultCenter] postNotificationName:@"cycle" object:nil userInfo:@{@"cycle":@(0)}];
    }
    return [super hitTest:point withEvent:event];
}

pointInside:withEvent应用

扩大按钮的点击范围

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
    CGRect bounds = self.bounds;
     bounds = CGRectInset(bounds, -10, -10);
   // CGRectContainsPoint  判断点是否在矩形内
    return CGRectContainsPoint(bounds, point);
}

事件响应链(响应者链)

事件处理。和事件传递过程大致的相反的。

事件被交由第一响应者对象处理,如果第一响应者不处理,事件被沿着响应者链向上传递,交给下一个响应者(next responder)。一般来说第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,事件就会被传递给它的视图控制器对象(如果存在),然后是它的父视图(superview)对象(如果存在),以此类推,直到顶层视图。接下来会沿着顶层视图 (top view)到窗口(UIWindow对象)再到程序(UIApplication对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。

一般情况下,在响应者链中只要有对象处理事件,事件就停止传递。但有时候可以在视图的响应方法中根据一些条件判断来决定是否需要继续传递事件。


响应链是从最合适的view开始传递,处理事件传递给下一个响应者,响应者链的传递方法是事件传递的反方法,顺序倒着来,如果所有的响应者都不处理事件,则事件被丢弃。

我们通常用响应者链来获取上几级响应者,方法是UIRespondernextResponder方法。

通过UIResponder来查找控件的父视图控件。

1、通过遍历button上的响应链来查找cell

UIResponder *responder = button.nextResponder;
    while (responder) {
        if ([responder isKindOfClass:[SWSwimCircleItemTableViewCell class]]) {
            SWSwimCircleItemTableViewCell *cell = (SWSwimCircleItemTableViewCell *)responder;
            break;
        }
        responder = responder.nextResponder;
    }

2、查找view的controller

- (UIViewController *)viewController {
    UIResponder *responder = self.nextResponder;
    do {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)responder;
        }
        responder = responder.nextResponder;
    } while (responder);
    return nil;
}

3、获取当前视图控制器

- (UIViewController *)getCurrentViewController {
    UIViewController *result = nil;
    UIWindow *window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal) {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for (UIWindow *tempWindow in windows) {
            if (tempWindow.windowLevel == UIWindowLevelNormal) {
                window = tempWindow;
                break;
            }
        }
    }

    UIView *frontView = [[window subviews] objectAtIndex:0];
    id nextResponder = [frontView nextResponder];

    if ([nextResponder isKindOfClass:[UIViewController class]]) {
        result = nextResponder;
    } else {
        result = window.rootViewController;
    }

    return result;
}