开篇叨叨

在iOS 事件模型中,由iOS事件源产生事件,而后顺着布局树进行Hit-Testing测试,判断哪些View可以响应这个事件,这些View组成一个事件响应链,产生的事件将沿着响应链一级一级传递,最终传递到最终的事件响应者中,由最终事件响应者提供的响应方法处理当前的事件。

这篇博客将主要针对如下问题进行展开:

  • iOS中有哪些事件源类型
  • 哪些对象会消费这些事件(哪些对象可以组成事件响应链上的节点)
  • 事件是怎么传递的
  • 如何判断谁是事件的最佳响应者
  • 事件的处理
事件源

为满足用户需求,iOS 提供了例如点击、长按、摇晃、3D Touch 等多种事件,这些事件大体可以分成触摸事件,运动事件,远程控制事件,按压事件四类。

  • 触摸事件:

    长按手势 (UILongPressGestureRecognizer)
    拖动手势 (UIPanGestureRecognizer)
    捏合手势 (UIPinchGestureRecognizer)
    响应屏幕边缘手势 (UIScreenEdgePanGestureRecognizer)
    轻扫手势 (UISwipeGestureRecognizer)
    旋转手势 (UIRotationGestureRecognizer)
    点击手势 (UITapGestureRecognizer)
  • 运动事件
    iPhone 内置陀螺仪、加速器和磁力仪,可以感知手机的运动情况。iOS 提供了 Core Motion 框架来处理这些运动事件。
    其中陀螺仪主要用于测量设备绕 X-Y-Z 轴的自转速率,倾斜角度等,加速器主要用于测量设备在 X-Y-Z 轴速度的改变,磁力仪可以测量当前设备的磁极、方向、经纬度等数据。

  • 远程控制事件
    远程控制事件指通过耳机去控制手机上的一些操作,比如上一曲/下一曲/播放/停止等。

  • 按压事件
    iOS 9 提供了 3D Touch 事件,可以通过压力的不同来区分不同的操作。

事件响应者

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,下面是iOS中UIResponder的子类,也就是说只有这些类以及这些类的子类才能响应并处理事件,UIResponder对象之间的联系靠nextResponder指针,组成一个响应链:

UIApplication
UIViewController
UIView

UIResponder 中定义了一系列的触摸事件响应函数,我们可以通过覆写这些方法来提供自定义的响应:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

我们以最常见的触摸事件进行介绍:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;//一根或者多根手指开始触摸view
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;//一根或者多根手指在view上移动
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;//一根或者多根手指离开view
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;//触摸结束前,某个系统事件(例如电话呼入)打断触摸过程时候调用

这里还需注意的是:

  • 不论多少根手指同时触摸View,都只会调用一次touchesBegan,但是touches里面会包裹着多个UITouch,每个手指对应一个UITouch对象。
  • 如果多个手指一前一后触摸同一个View,那么会调用多次touchesBegan,每次只包含一个UITouch对象。
  • 如果是处理UIView触摸事件,需要在其子View的中覆写对应的touch方法。如果是处理UIViewController的触摸事件,可以直接在UIViewController的文件中覆写对应的touch方法。
  • 当我们手指按下后在屏幕上移动的时候会不断触发touchesMoved,一旦抬起来就会触发touchesEnded。
事件对象

了解了事件源,事件响应对象,我们还需要了解下事件对象,事件对象会携带者一系列的事件信息到事件响应对象,一个触摸事件可能是由多个手指同时触摸产生的。触摸对象集合通过 allTouches 属性获取。

  • UIEvent
@interface UIEvent : NSObject

@property(nonatomic,readonly) UIEventType type; //事件类型
@property(nonatomic,readonly) UIEventSubtype subtype; //事件子类型
@property(nonatomic,readonly) NSTimeInterval timestamp; //事件产生的时间戳
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; //一个事件包含的所有触摸事件
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture;
@end

这里的UIEventType是事件大的类别:

typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses API_AVAILABLE(ios(9.0)),
};

UIEventSubtype 为事件小的类别,比如远程控制事件会使用这个字段来区分具体是哪个子事件:

typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,

// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,

// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

里面包含了一系列的UITouch,可以通过touchesForWindowtouchesForViewtouchesForGestureRecognizer来判断属于某个window,view,以及手势下的UITouch.

  • UITouch
@interface UITouch : NSObject

//时间戳: 记录了触摸事件产生或变化时的时间。单位是秒。
@property(nonatomic,readonly) NSTimeInterval      timestamp;

//触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。通过phase可以查看当前触摸事件在一个周期中所处的状态。
@property(nonatomic,readonly) UITouchPhase        phase;

//轻击(Tap)操作和鼠标的单击操作类似,tapCount表示短时间内轻击屏幕的次数。因此可以根据tapCount判断单击、双击或更多的轻击。
@property(nonatomic,readonly) NSUInteger          tapCount;

//触摸的类型
@property(nonatomic,readonly) UITouchType         type;

//触摸的半径
@property(nonatomic,readonly) CGFloat majorRadius;

//触摸的力度
@property(nonatomic,readonly) CGFloat force;

//触摸产生时所处的窗口。由于窗口可能发生变化,当前所在的窗口不一定是最开始的窗口。
@property(nullable,nonatomic,readonly,strong) UIWindow *window;

//触摸产生时所处的视图。由于视图可能发生变化,当前视图也不一定是最初的视图。
@property(nullable,nonatomic,readonly,strong) UIView *view;

//现在触摸的坐标
/函数返回一个CGPoint类型的值,表示触摸在view这个视图上的位置,这里返回的位置是针对view的坐标系的。调用时传入的view参数为空的话,返回的时触摸点在整个窗口的位置。
-(CGPoint)locationInView:(nullable UIView *)view;

//上一次触摸的坐标
//该方法记录了前一个坐标值,函数返回也是一个CGPoint类型的值,表示触摸在view这个视图上的位置,这里返回的位置是针对view的坐标系的。调用时传入的view参数为空的话,返回的时触摸点在整个窗口的位置。
-(CGPoint)previousLocationInView:(nullable UIView *)view;

UITouchPhase 用于表示某个触摸是处于哪个阶段

typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
事件的传递流程
阶段1 将事件传递到UIWindow

当一个事件产生的时候,事件会在底层由IOKit.framework 封装成IOHIDEvent对象。然后系统通过mach port将IOHIDEvent对象转发给SpringBoard.app。 SpringBoard.app 它有点像Android中的lancher,只接收按钮,触摸,加速等事件,SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件.可能在事件产生的时候你在桌面翻页并没有应用在前台运行,这时候触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗,如果有应用在前台运行那么会通过mach port 将IOHIDEvent 转发给对应的App.App主线程的RunLoop收到SpringBoard转发的消息后,触发Source1回调__IOHIDEventSystemClientQueueCallback。在这个方法中会触发Source0回调__UIApplicationHandleEventQueue,将IOHIDEvent转换为UIEvent.并通过UIApplication的sendEvent:方法将UIEvent传递给UIWindow.

阶段2 Hit-Testing

上一阶段UIWindow已经拿到了事件,但是面对着整个复杂的视图层级树,要先判断最先将事件最先传给谁(也就是判断first responder,当然first responder也可以我们直接指定),这就需要靠Hit-Testing来完成了,注意这里并未涉及到事件的处理,只是确定由哪个视图来首先处理 UITouch 事件。

上图中左边是界面结构,右边是对应的视图层级树,其中View B的子View,View B.1遮住了View A的子View,View A2.我们假设点击了View B.1区域。
下面是整个Hit-Testing 的流程。

首先UIWindow 会顺着层级树,到RootView,然后RootView有三个子View .由于三个子View的添加顺序为: A –> B –> C, 所以先从C开始,也就是从subViews的最后一个开始,从后往前进行遍历,为什么从后往前而不是从前往后是因为,在数组中所处的位置越后,在界面上所处的位置越上层,这是为了考虑视图遮挡的情况下先判断最上层的。View C调用hit test 判断不在它上面,所以转向B,hit-test测试在它上面,就继续遍历它的子view,子view的遍历过程也是从子view数组的最后一项,往前测试。在测试到View B.1的时候发现触点在它上面,并且它没有子View所以,到此位置终止测试。View B.1 作为Hit-testing最终的结果。

上面只是展示了正常的Hit-Testing 的流程,实际上,View的一些属性还会影响到Hit-Testing的结果,比如如下的情况:

视图的hidden等于 YES
视图的alpha小于等于 0.01
视图的userInteractionEnabled为 NO

也就是当前待测试的视图不可见或者不处理交互,这些视图将会被忽略。因此整个hitTest的代码如下所示。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//如果当前视图不可见或者不接受事件,将传递给上层
if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
return nil;
}
//判断触点是否在当前视图内部
BOOL inside = [self pointInside:point withEvent:event];
if (inside) {
//如果在的话判断子view
NSArray *subViews = self.subviews;
for (NSInteger i = subViews.count - 1; i >= 0; i--) {
UIView *subView = subViews[i];
//将point转换到subView视图上的坐标递归调用子view的hitTest
CGPoint insidePoint = [self convertPoint:point toView:subView];
UIView *hitView = [subView hitTest:insidePoint withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
return nil;
}

下面需要重点看下这个阶段的两个重要方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest 返回的是包含触点的最适合的子view,如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是该控件的父控件。

想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!因为有可能在还没遍历到返回self的那个view的时候就已经拿到触点的的view。

pointInside很好理解就是判断点是否在当前view上面,如果是返回YES,否则返回NO.这里需要注意的是,point 必须先转换为相对当前view的坐标系坐标。

  • Hit-Testing 的应用
  1. 增加视图的touch区域

这里可以通过两种方式都可以实现:

一种是通过重写hitTest,一种是通过重写pointInside

重写hitTest

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;

CGFloat inset = 45.0f - 78.0f;
CGRect touchRect = CGRectInset(self.bounds, inset, inset);

if (CGRectContainsPoint(touchRect, point)) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}

重写pointInside

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
CGRect bounds = self.bounds;
CGFloat widthDelta = MAX(self.focusSize - bounds.size.width, 0);
CGFloat heightDelta = MAX(self.focusSize - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
  1. 透传事件

当时想在当前 view 处理事件,不想在对 subview 进行遍历,可以直接重写 [hitTest:withEvent:] 方法并 return self 即可。

  1. 指定某个视图处理事件

重写父视图的[hitTest:withEvent:],指定响应 View。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView) {
hitTestView = self.scrollView;
}
return hitTestView;
}
阶段3 事件的响应

Touch 事件处理的传递过程与 Hit-Testing 过程正好相反。Hit-Tesing 过程是从父视图到子视图遍历;Touch 事件处理传递是从子视图到父视图传递。
首先Touch事件会被发送到first responder,first responder便拥有了对事件的绝对控制权:它可以选择独吞这个事件,也可以将这个事件往下传递给其他响应者.

整个过程如下所示:

* 如果当前view是控制器的RootView,那么控制器就是nextResponder,事件就传递给控制器.
* 如果当前view不是控制器的RootView,那么父视图就是当前view的nextResponder,事件就传递给它的父视图.
* 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理.
* 如果window对象也不处理,则其将事件或消息传递给UIApplication对象.
* 如果UIApplication也不能处理该事件或消息,则将其丢弃

响应者对于接收到的事件有3种操作:

  • 不拦截 :事件会自动沿着默认的响应链往下传递
  • 拦截,不再往下分发事件 :重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  • 拦截,继续往下分发事件 :重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递

这里还需要注意的一点是如果我们的View有关联的手势识别器,那么在将touches发送给发生触摸的视图本身前,会先将touches发送给发生触摸的视图所关联的手势识别器,这部分内容将在下面讲手势的时候介绍。

触摸事件经历上面各个环节后要么被某个响应对象捕获后释放,要么没能找到能够响应的对象被丢弃。整个触摸事件就结束了。Runloop若没有其他事件需要处理,也将重新进入休眠,等待新的事件到来后唤醒。

手势

上面介绍了iOS的事件体系,紧接着讲下和事件相关的手势交互,iOS中的手势都是继承自UIGestureRecognizer,系统为了方便大家使用也内置了一系列的手势,下面是目前支持的几种手势,如果不够使用还可以通过继承UIGestureRecognizer来自定义手势。

手势 说明
UITapGestureRecognizer 轻拍手势
UISwipeGestureRecognizer 轻扫手势
UILongPressGestureRecognizer 长按手势
UIPanGestureRecognizer 平移手势
UIPinchGestureRecognizer 捏合(缩放)手势
UIRotationGestureRecognizer 旋转手势
UIScreenEdgePanGestureRecognizer 屏幕边缘平移
常用属性和方法
//设置代理,具体的协议后面会说
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate;
//设置手势是否有效
@property(nonatomic, getter=isEnabled) BOOL enabled;
//获取手势所在的view
@property(nullable, nonatomic,readonly) UIView *view;
//获取触发触摸的点
- (CGPoint)locationInView:(nullable UIView*)view;
//设置触摸点数
- (NSUInteger)numberOfTouches;
//获取某一个触摸点的触摸位置
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view;
手势的初始化

各种手势都是通过initWithTarget:action进行初始化的

- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action;

还可以通过removeTarget:action将一个selector从手势对象上移除

- (void)removeTarget:(nullable id)target action:(nullable SEL)action;

iOS系统允许一个手势对象可以添加多个selector触发方法,并且触发的时候,所有添加的selector都会被执行

- (void)addTarget:(id)target action:(SEL)action;

这里需要注意的是UIGestureRecognizerSubclass.h头文件中定义了一个UIGestureRecognizer分类UIGestureRecognizerProtected,它里面也定义了一系列和UIResponder一样的方法,
可以供我们覆写以实现自定义效果。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
手势代理

手势有其对应的代理UIGestureRecognizerDelegate 通过它可以指定很多特性,下面将对这些特性进行一一介绍:

@protocol UIGestureRecognizerDelegate <NSObject>
@optional
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;
@end
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

开始进行手势识别时调用的方法,当手势识别器识别到手势,准备从UIGestureRecognizerStatePossible状态开始转换时.调用此代理,如果返回YES,那么就继续识别,如果返回NO,那么手势识别器将会将状态置为UIGestureRecognizerStateFailed.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch

手指触摸屏幕后回调的方法,返回NO则不再进行手势识别,方法触发此方法在window对象在有触摸事件发生时,调用gesture recognizer的touchesBegan:withEvent:方法之前调用,如果返回NO,则gesture recognizer不会看到此触摸事件。(默认情况下为YES)

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

手指按压屏幕后回调的方法,返回NO则不再进行手势识别,方法触发等

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

这个方法返回YES,第一个手势和第二个互斥时,第一个会失效

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

这个方法返回YES,第一个和第二个互斥时,第二个会失效

手势状态

和其他事件一样手势也是有个状态机,它用一个state属性描述。

@property(nonatomic,readonly) UIGestureRecognizerState state;

它的描述值如下:

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 默认的状态,这个时候的手势并没有具体的情形状态
UIGestureRecognizerStateBegan, // 手势开始被识别的状态
UIGestureRecognizerStateChanged, // 手势识别发生改变的状态
UIGestureRecognizerStateEnded, // 手势识别结束,将会执行触发的方法
UIGestureRecognizerStateCancelled, // 手势识别取消
UIGestureRecognizerStateFailed, // 识别失败,方法将不会被调用
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

左侧是非连续手势(比如单击)的状态机,右侧是连续手势(比如滑动)的状态机。所有的手势的开始状态都是UIGestureRecognizerStatePossible。
非连续的手势要么识别成功(UIGestureRecognizerStateRecognized),要么识别失败(UIGestureRecognizerStateFailed)。
连续的手势识别到第一个手势时,变成UIGestureRecognizerStateBegan,然后变成UIGestureRecognizerStateChanged,并且不断地在这个状态下循环,当用户最后一个手指离开view时,变成UIGestureRecognizerStateEnded,当然如果手势不再符合它的模式的时候,状态也可能变成UIGestureRecognizerStateCancelled。

手势与事件处理

我们前面讲到了当一个view有关联手势的时候,手势的优先级会比事件来的高,因此在我们触摸带手势的view的时候,如果先被手势识别器识别了,事件就不会传递给view 的UIResponder进行处理,但是我们知道手势是有状态的,也就是说手势识别是有一个过程的,因此在手势未被完全识别之前,事件是会被同时发送到UIResponder,以及UIGestureRecognizer,一旦手势识别器识别了某个手势之后,UIResponder就会的touchesCancelled:withEvent就会被调用,此后的事件都被UIGestureRecognizer独占,也就是只发往UIGestureRecognizer。如果识别失败手势状态将会被识别为UIGestureRecognizerStateFailed,这时候事件会继续发送给UIResponder,直到结束。

简单得说就是,在手势识别器未判定手势识别成功之前,事件会发给手势识别器和UIResponder,一旦手势识别器判定为识别成功就拦截了整个事件,UIResponder会收到cancel的信号,后续就不会继续收到对应的事件了,所有的后续事件都交给手势识别器进行处理。
同样在手势识别器未判定手势识别成功之前,事件会发给手势识别器和UIResponder,如果手势识别器识别失败,那么就会被标记为UIGestureRecognizerStateFailed,然后后续的事件都交给UIResponder进行处理。

  • cancelsTouchesInView

默认为YES。表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给UIResponder。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给UIResponder。

手势其他用法

我们添加的两个手势都是单击手势,会产生冲突,触发是很随机的,如果我们想设置一下当手势互斥时要优先触发的手势,可以使用如下的方法:

- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
比如我们在秀场直播间,双击屏幕会产生关注,如果单击屏幕会有点赞效果,就可以通过这种方式解决。

// 单击的 Recognizer UITapGestureRecognizer* singleRecognizer; singleRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:selfaction:@selector(handleSingleTapFrom)]; singleTapRecognizer.numberOfTapsRequired = 1; // 单击 [self.view addGestureRecognizer:singleRecognizer]; // 双击的 Recognizer UITapGestureRecognizer* double; doubleRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:selfaction:@selector(handleDoubleTapFrom)]; doubleTapRecognizer.numberOfTapsRequired = 2; // 双击 [self.view addGestureRecognizer:doubleRecognizer]; // 如果双击确定检测失败才会触发单击 [singleRecognizer requireGestureRecognizerToFail:doubleRecognizer]; [singleRecognizer release]; [doubleRecognizer release]; ``` 老规矩上图 ![](./iOS-事件及手势处理流程/000007.png)
Contents
  1. 1. 开篇叨叨
  2. 2. 事件源
  3. 3. 事件响应者
  4. 4. 事件对象
  5. 5. 事件的传递流程
    1. 5.1. 阶段1 将事件传递到UIWindow
    2. 5.2. 阶段2 Hit-Testing
    3. 5.3. 阶段3 事件的响应
  6. 6. 手势
    1. 6.1. 常用属性和方法
    2. 6.2. 手势的初始化
    3. 6.3. 手势代理
    4. 6.4. 手势状态
    5. 6.5. 手势与事件处理
    6. 6.6. 手势其他用法