OtheriOS开发技术iOS-OC-基础

iOS触摸事件处理详解

2016-10-14  本文已影响7395人  AKsoftware

简介

iOS 事件分为三大类

以下我们讲解触摸事件
触摸事件是我们平时遇到最多的事件,例如单击、长按、滑动等等。当用户点击按钮,到按钮处理回调。整个过程是如何发生,需要什么样的原则,这些都是问题。为了使系统能更加鲜明符合用户的操作逻辑,iOS系统将事件相应过程拆分成两部分:1.寻找响应链;2.事件响应。先将事件通过某种规则来分发,找到处理事件的控件。其次是将事件传递分发,响应。

触摸事件

UIEvent
iOS将触摸事件定义为第一个手指开始触摸屏幕到最后一个手指离开屏幕定义为一个触摸事件。用类UIEvent表示。

UITouch
一个手指第一次点击屏,会形成一个UITouch对象,直到离开销毁。表示触碰。UITouch对象能表明了当前手指触碰的屏幕位置,状态。状态分为开始触碰、移动、离开。

根据定义,UIEvent实际包括了多个UITouch对象。有几个手指触碰,就会有几个UITouch对象。
代码定义如下

@interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval  timestamp;
#if UIKIT_DEFINE_AS_PROPERTIES
//UITouch SET
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
//省略部分代码
@end

UIEventType表明了事件类型,UIEvent表示了三大事件。
allTouches是该事件的所有UITouch对象的集合。

//UITouch
@interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval      timestamp;
@property(nonatomic,readonly) UITouchPhase        phase;
@property(nonatomic,readonly) NSUInteger          tapCount;   // touch down within a certain point within a certain amount of time
@property(nonatomic,readonly) UITouchType         type NS_AVAILABLE_IOS(9_0);

@property(ullable,nonatomic,readonly,strong) UIWindow                        *window;
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers 
NS_AVAILABLE_IOS(3_2);
//省略部分代码
@end
//Touch 状态枚举
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)
};

UITouch中phase表明了手指移动的状态,包括1.开始点击;2.移动;3.保持; 4.离开;5.被取消(手指没有离开屏幕,但是系统不再跟踪它了)

综上,UIEvent就是一组UITouch。每当该组中任何一个UITouch对象的phase发生变化,系统都会产生一条TouchMessage。也就是说每次用户手指的移动和变化,UITouch都会形成状态改变,系统变回会形成Touch message进行传递和派发。那么 一次触摸事件是由一组UITouch对象状态变化引起的一组Touch message的转发和派送。那么事件派发的原则是什么?

响应链

响应链是“事件派发”的原则和规定,那么响应链是什么?顾名思义事件链是一个链条,详细的定义如下:

先看下UIResponser,UIResponser是用来做什么的?

UIResponser就是用来接收和处理事件的类,先抛开iOS中的具体传递细节,系统发送UIEvent的Touch message给UIResponser类。UIResponser提供了一下几个函数来做事件处理

//触摸事件
- (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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

//物理按钮,遥控器上面的按钮在按压状态等状态下的回调
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

//设备的陀螺仪和加速传感器使用
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

UIResponser包括了各种Touch message 的处理,比如开始,移动,停止等等。常见的UIResponser有 UIView及子类,UIViController,APPDelegate,UIApplication等等。

回到响应链,响应链是由UIResponser组成的,那么是按照哪种规则形成的。

  1. 程序启动
  1. 创建UIWindow
  1. UIViewController初始化
  1. addSubView

如果在中途,subView.VC被释放,就会变成subView.nextResponser = superView

最终形成类似这样一张图


responser chain@2x.png

其中应该是由箭头的,箭头的方向是超上,也就是subView指向superView.

事件传递

有了响应网为基础,事件的传递就比较简单,只需要选择其中一条响应链,但是选择那一条响应链来传递呢?为了弄清真个过程,我们先来查看一下从触摸硬件事件转化为UIEvent消息。

  1. 首先用户触摸屏幕,系统的硬件进程会获取到这个点击事件,将事件简单处理封装后存到系统中,由于硬件检测进程和当前运行的APP是两个进程,所以进程两者之间传递事件用的是端口通信。硬件检测进程会将事件放入到APP检测的那个端口。
  2. 其次,APP启动主线程RunLoop会注册一个端口事件,来检测触摸事件的发生。当时事件到达,系统会唤起当前APP主线程的Runloop。唤起原因就是端口触摸事件,主线程会分析这个事件。
  3. 最后,系统判断该次触摸是否导致了一个新的事件, 也就是说是否是第一个手指开始触碰,如果是,系统会先从响应网中 寻找响应链。如果不是,说明该事件是当前正在进行中的事件产生的一个Touch message, 也就是说已经有保存好的响应链。

如果是新事件,系统会寻找响应链,为了符合用户的操作习惯,系统会根据用户的点击位置,在当前的整个APP的显示层级中寻找。过程如下:

  1. 将所有的显示在屏幕上的 "合格的"UIWindow对象 按照层级结构从上到下排列成一个数组。
  2. 从第一个UIWindow对象开始,先判断UIWindow是否合格,其次判断 点击位置在不在这个Window内,如果不在 ,返回nil, 就换下一个UIWindow;如果在的话,并且UIWindow没有subView就返回自己,整个过程结束。如果UIWindow有subViews,就从后往前遍历整个subViews,做和UIWindow类似的事情,直到找到一个View。如果没有找到到就不做传递。

合格的UIWindow,UIView。意思是控件被允许接受事件。符合三个条件:1.不能被隐藏;2.alpha值大于0.01(不是backgroundColor为clearColor);3.isUserInteractionEnabled为YES,打开状态。一般UILabel,UIImageView纯显示的控件默认是关闭状态,也就是不处理事件。

显示控件有了两个方法来做上面这件事,就是常说的hitTest

 // 先判断点是否在View内部,然后遍历subViews
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
//判断点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

整个过程的系统实现大致如下

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
  //判断是否合格
    if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
        //判断点击位置是否在自己区域内部
        if ([self pointInside: point withEvent:event]) {
            UIView *attachedView;
            for (int i = self.subviews.count - 1; i >= 0; i--) {
                UIView *view  = self.subviews[i];
                //对子view进行hitTest
                attachedView =  [view hitTest:point withEvent:event];
                if (attachedView)
                    break;
            }
            if (attachedView)  {
                return attachedView;
            } else {
                return self;
            }
        }
    }
    return nil;
}

技巧

以上可知默认情况下,用户点击哪个View,系统就会在寻找过程中返回哪个view,但是我们可以重载上面两个方法做如下事情:

hitTest的逻辑代码中会把隐藏,透明(alpha<0.01,不是backgroundColor为clearColor),不交互的view滤过,但不代表hitTest不会被调用,我们可以重载hitTest去让 已经隐藏、透明、不交互的view响应事件。不过最正规的方法是打开控件交互属性。

** 以上过程返回的View被称作hitTestView,顺着hitTestView的nextResponser,可以形成一个链,即响应链。** 最后指向appDelegate. 并且返回hitTestView之后,系统会持有hitTestView。事件不结束,这个hitTestView不会发生变化,即使用户点击之后将手指移动到其他控件上面,该点击都会绑定开始的hitTestView。当所有手指离开屏幕,事件结束。再次点击,事件重新开始。以上过程再来一次。

事件响应

形成响应链之后,UIWindow会把事件目标锁定为hitTestView(响应链头的控件),当手指状态发生变化, 会不停的发送UITouch Message 给这个hitTestView。 下面这几个方法会被调用。

然后控件的以下方法会陆续被调用

//点击刚开始,回调这个方法
- (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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

技巧

由于系统只会把事件发送给 hitTestView,如果你想让hitTestView之后的其他响应者处理该Touch Mesage ,需要自己实现以上几个方法做派发,例如

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    //do someThiing
  [self.nextResponser touchesBegan: touches withEvent:event];
}

事件转发可以做很多事情。大家可以尽可能的想象

手势处理

以上看来所有的事情都很平稳,无非就是寻找响应链,传递事件等等。但是接下来大家可能需要蒙圈。先来道题目

可能很多人会说没有任何影响,基本都会调用,答案是整个过程会调用这两个方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

touchEnd不会被调用。
为什么?因为有手势的存在,我们先看一下手势。

手势

手势是苹果为处理常用的用户交互所推出了一个优先级更高的处理技术。为了让用户完成对多种控件的基本操作,苹果实现了以下几个手势

UITapGestureRecognizer
UIPinchGestureRecognizer
UIRotationGestureRecognizer
UISwipeGestureRecognizer
UIPanGestureRecognizer
UIScreenEdgePanGestureRecognizer
UILongPressGestureRecognizer

上面包括点击,长按,旋转,滑动等等手势。这样开发者就可以随便将其关联到某个控件上完成交互。
先抛开刚才的问题,先看单纯的手势如何识别用户操作。

系统会将用户触摸屏幕的点事件 发送给手势,手势会根据具体的点击位置和序列,判断是否是某种特定行为。具体的判断方法如下

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

和UIResponser一样,手势也有这几个方法,点击的每个阶段手势都会响应不同的方法,手势会在以上四个方法中去对手势的State做更改,手势的State表明当前手势是识别还是失败等等。比如单击手势会在touchesBegan 时记录点击位置,然后在touchesEnded判断点击次数、时间、是否移动过,最后得出否识别该手势。这几个方法一般在自定义手势里面使用。

手势状态
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    //未知状态
    UIGestureRecognizerStatePossible,   // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
    //首次识别状态,对于连续手势,例如长按,有这种状态
    UIGestureRecognizerStateBegan,      // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
    //再次识别,当手连续手势识别之后,再次受到touch事件
    UIGestureRecognizerStateChanged,    // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
    //识别完成,受到touchend 消息之后
    UIGestureRecognizerStateEnded,      // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
    //取消识别
    UIGestureRecognizerStateCancelled,  // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
    //识别失败
    UIGestureRecognizerStateFailed,     // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
    // Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
    //识别状态,与识别结束一个意思
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};

手势的状态有以上几种,官方给手势状态图如下。


gestureStateChange.jpg

结合图我们来看手势的整个迁移过程,先明确几个信息

  1. 手势的状态迁移只有在它们收到Touch message的时候,才能做状态变化处理代码。
  2. 手势分为连续状态手势和不连续状态手势。连续手势有长按,慢滑等。不连续手势有单击,双击等等。
  3. 当用户没有点击屏幕,所有手势都处于Possiable状态。
    当用户点击屏幕,手势会收到Touch Began Message, 手势的touchBegan方法会被调用。手势开始记录点击位置和时间。仍处于Possiable状态。如果用户按住不放,间隔超过一定时间,单击手势会变化为失败状态,并在下个一runloop变为possiable。如果时间大于长按手势设定时间,长按手势就会变化为Began状态,当用户移动手指,长按手势的touch move方法被调用,长按手势将自己状态设置为Change,并且也会回调处理方法。最后手指离开,系统调用长按手势touchEnd方法,手势状态回归为Recognized状态。

手势混合处理

如果一个View上既有单击,又有双击,用户点击该view两次, 默认情况下,单击被处理,双击不管用。因为默认情况下,一旦事件被某个手势处理,第二个手势会识别失败 幸运的是苹果提供了方法让我们修改这种默认行为,具体的方法如下

@protocol UIGestureRecognizerDelegate <NSObject>
@optional
// called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

// called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
// return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
//
// note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

// called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

@end

上面是手势的代理方法,你可以实现手势的这几个代理方法,更改默认行为。

上图只表明执行顺序,但是不一定每次每一个方法都被调用,实际要根据每个方法的实现,来判断是否要执行其他方法

到此手势和view单独接受事件的情况完全介绍完。但是实际过程中二者是混合的,那么和这个时候会有什么情况发生呢?

手势与事件相应

回到我们上面问过的问题,BView只有touchBegan, touchesCancelle 的原因是什么?答案在于整个触摸事件全过程
1.系统会通过hitTest的方法寻找响应链,完成之后会形成下图模型。


event handing squence.png

图中最右边是响应链,中间是关联在相应链在视图上的手势

2.有了模型之后就会发生图上的三个步骤
第一步:系统会将所有的 Touch message 优先发送给 关联在响应链上的全部手势。手势根据Touch序列消息和手势基本规则更改自己的状态(有的可能失败,有的可能识别等等)。如果没有一个手势对Touch message 进行拦截(拦截:系统不会将Touch message 发送给响应链顶部响应者),系统会进入第二步

第二步:系统将Touch message 发送给响应链 顶部的 视图控件,顶部视图控件这个时候就会调用Touch相关的四个方法中的某一个。之后进入自定义Touch message转发

第三步:自定义Touch message转发可以继承UIResponser的四个Touch函数做转发。

解释一下第一步中说的拦截,手势会表明是否拦截该Touch Message,主要由下面三个属性控制。

再回到那道题目,如果我们想hitTestView的toucheEnd函数依然能得到调用,怎么办?其实UIGestureRecognizer有三个属性

@property(nonatomic) BOOL cancelsTouchesInView;       // default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.
@property(nonatomic) BOOL delaysTouchesBegan;         // default is NO.  causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture
@property(nonatomic) BOOL delaysTouchesEnded;         // default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized

总结

iOS整个事件处理的过程就是这样,系统为完成整个交互做了很多东西,核心点如下:

上一篇下一篇

猜你喜欢

热点阅读