您当前的位置: 首页 >  ios

white camel

暂无认证

  • 2浏览

    0关注

    442博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

iOS 一一 触摸事件和手势

white camel 发布时间:2017-10-14 14:51:56 ,浏览量:2

iOS触摸事件和手势
文章出处: http://www.jianshu.com/p/cb0314b72883
在iOS中,触摸表示用户手指触击屏幕及在屏幕上移动时,系统不断发送给应用程序对象,一个UITouch对象表示一个触摸,一个UIEvent表示一个事件
手势是指从一个或多个手指接触屏幕开始,直到手指离开屏幕为止发生的所有事情。手势是触摸事件的集合
简介

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  *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  *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的转发和派送。那么事件派发的原则是什么?

响应链

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

  • 每条链是一个 链表状结构,整个是一棵树
  • 链表的每一个node是一个 UIResponser对象

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

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

//触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches NS_AVAILABLE_IOS(9_1);

//物理按钮,遥控器上面的按钮在按压状态等状态下的回调
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet *)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. 程序启动

    • UIApplication会生成一个单例,并会关联一个APPDelegate。APPDelegate作为整个响应链的根建立起来,而UIApplication会将自己与这个单例链接,即UIApplication的nextResponser(下一个事件处理者)为APPDelegate。
  2. 创建UIWindow

    • 程序启动后,任何的UIWindow被创建时,UIWindow内部都会把nextResponser设置为UIApplication单例。
    • UIWindow初始化rootViewController, rootViewController的nextResponser会设置为UIWindow
  3. UIViewController初始化

    • loadView, VC的view的nextResponser会被设置为VC.
  4. addSubView

    • addSubView操作过程中,如果子subView不是VC的View,那么subView的nextResponser会被设置为superView。如果是VC的View,那就是 subView -> subView.VC ->superView

      如果在中途,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,但是我们可以重载上面两个方法做如下事情:

  • 将控件外部点规整到控件内部。 例如控件较小,点击位置在控件边缘外部,可以重载- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; 将外部的点也判断为内部点,这样hitTest就会遍历自己。
  • 重载HitTest更改默认行为。 有时候点击subView的某些特殊位置需要superView处理,我们可以在superView的hitTest,返回superView。这样superView变成首部响应者

    hitTest的逻辑代码中会把隐藏,透明(alpha

关注
打赏
1661428283
查看更多评论
立即登录/注册

微信扫码登录

0.1588s