likes
comments
collection
share

iOS在触摸屏幕后发生了什么?

作者站长头像
站长
· 阅读数 25

iOS触摸事件的一系列机制,涉及的主要问题包括: 1、触摸事件处理过程中涉及的对象; 2、触摸事件由触屏生成后如何传递到当前应用; 3、应用接收触摸事件后如何寻找最佳响应者; 4、触摸事件如何沿着响应链流动; 5、UIResponder(事件响应者)、UIGestureRecognizer(手势识别器)、UIControl之间对于触摸事件的响应顺序;

一、触摸(UITouch)、事件(UIEvent)和事件响应者(UIResponder)

(一)UITouch

1、UITouch是触摸对象,一个手指一次触摸屏幕就生成一个UITouch对象,多个手指同时触摸,生成多个UITouch对象; 2、多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击手势),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的tapCount属性值由1变成2);如果两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者没有联系。 3、每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图和窗口等信息。 4、手指离开屏幕一段时间后,确定该UITouch对象不会再被更新,UITouch将被释放。

(二)UIEvent

(1)UIEvent代表iOS系统同外界互动时所发生的事件,总共有三种事件:触摸事件、运动事件(加速计事件)和远程控制事件(蓝牙等)。对于触摸事件,UIEvent中保存着同当前触摸序列相关的UITouch对象; (2)UIEvent对象中包含了触发该事件的触摸对象的集合,因为一个触摸事件可能是由多个手指同时触摸产生的,触摸对象集合通过allTouches属性获取。

(三)UIResponder

每个响应者都是一个UIResponder对象,即所有派生自UIResponder的对象,本身都具备响应事件的能力,多个响应对象组成响应链。 以下类的实例都是响应者:

  • UIView,包括UIView的子类UIControl和UIWindow
  • UIViewController
  • UIApplication
  • UIAppDelegate 响应者能响应事件,因为提供了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)touchesCancel:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

二、事件的生命周期

手指触摸屏幕的一刻,系统会生成一个触摸事件。经过IPC进程间通信,事件最终被传递给了合适的应用。

(一)系统响应阶段

屏幕感应到触碰后,将事件交给IOKit处理,IOKit是监测硬件的框架。IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoard进程。

mach port是进程端口,各个进程之间通过它进行通信; SpringBoard.app是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件;

SpringBoard.app进程收到触摸事件,触发主线程RunLoop的source1事件源的回调。 SpringBoard.app会根据当前桌面的状态,判断应该由谁响应此次触摸事件。 如果没有APP在运行,则由SpringBoard处理该事件; 如果有APP在运行,则由APP处理该事件;

(二)APP响应阶段

1、APP进程的mach port接收到SpringBoard进程传递来的触摸事件,主线程的RunLoop被唤醒,触发source1回调; 2、source1回调触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象; 3、source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队列后,UIApplication开始寻找一个最佳响应者的过程,这个过程又称为hit-testing,具体细节在第二个主题寻找最佳响应者中阐述; 4、找到最佳响应者后,事件就在响应链中传递和响应,这里涉及到“事件的响应和响应链中的传递”; 5、经过上述流程,触摸事件要么被某个响应对象捕获后释放,要么没有找到能响应的对象被释放;

总结:触摸事件从触屏产生后,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台APP处理。

三、寻找事件最佳响应者(Hit-Testing)

这里涉及到两个问题: 1、应用接收到事件后,如何寻找最佳响应者,底层如何实现; 2、寻找最佳响应者过程中事件的拦截。

(一)寻找最佳响应者的过程

应用接收到事件后先将其放入事件队列中等待处理。出队列后,UIApplication首先将事件传递给当前应用最后显示的窗口(UIWindow)询问其能否响应事件,若窗口能响应事件,则传递给子视图询问是否能响应,子视图继续询问子视图。子视图询问的顺序是优先询问后添加的子视图。 事件传递顺序如下: UIApplication -> UIWindow -> 子视图 -> ... -> 子视图

(二)Hit-Testing过程中的事件拦截(自定义事件流向)

实际开发中可能会遇到一些特殊的交互需求,需要定制视图对于事件的响应。 例如:TabBar的控件高于Tabbar时,点击高于TabBar的区域(红框区域),该控件不会得到响应。

iOS在触摸屏幕后发生了什么?

分析事件传递的过程: 点击红色区域后,生成的触摸事件首先传到UIWindow,然后传到控制器的根视图RootView,RootView经过判断可以响应戳事件,将该事件传递给了子空间TabBar,因为触摸点不在TabBar的坐标范围内,因此,TabBar无法响应该触摸事件。而后RootView询问TableView是否能够响应,TableView能够响应,因此事件被TableView消耗。 产生不能响应的问题原因是事件传递到TabBar时,没能继续往CircleButton传递,因为点击区域坐标不在TabBar的坐标范围内,所以我们可以修改事件Hit-Testing的过程,当点击红色方框区域时让事件流向圆形按钮。 事件传递到TabBar时,TabBar的hitTest:withEvent:被调用,但是pointInside:withEvent:会返回NO,因此hitTest:withEvent:返回了nil。 重写TabBar的pointInside:withEvent:判断当前触摸坐标是否在子视图CircleButton的坐标范围内,若在,则返回YES,反之,返回NO。

// TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 将触摸点坐标转换到在CirCleButton上的坐标
    CGPoint pointTemp = [self convertPoint:point toView:_circleButton];
    // 若触摸点在CircleButton上则返回YES
    if ([_circleButton pointInside:pointTemp withEvent:evnet]) {
        return YES;
    }
    // 否则返回默认的操作
    return [super pointInside:point withEvent:event];
}

四、事件的响应和在响应链中的传递

经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事就是: 1、将事件传递给最佳响应者响应; 2、事件沿着响应链传递;

(一)事件响应的前奏

最佳响应者有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。 UIApplication将事件通过sendEvent:传递给事件所属的UIWindow,UIWindow同样通过sendEvent:将事件传递给最佳响应者。

(二)事件的响应

每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,默认不对事件做任何处理,只是将事件沿着响应链传递。 若要截获事件进行自定义的响应操作,就要重写相关的方法。例如:重写touchesMoved:withEvent:方法实现简单的视图拖动。

(三)事件的传递(响应链中传递)

最佳响应者具备响应事件的最高优先权,最佳响应者接收到了事件,就拥有了事件的绝对控制权,它可以选择独吞这个事件,也可以将这个事件往下传递给其它响应者,这个响应者构成的视图链称之为响应链。

寻找最佳响应者的过程也提到了事件的传递,与此处说的事件传递有本质区别。寻找最佳响应者过程的事件传递是为了寻找事件的最佳响应者,是自下而上的传递;这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下。 前者为了”寻找“,后者为了”响应“。

3.1 响应者对于事件的操作方式

响应者对于事件的拦截和传递都是通过touchesBegan:withEvent:方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。 响应者接收到事件后有3种操作: 1、不拦截,向下分发事件(默认操作) 事件会自动沿着默认的响应链向下传递,系统默认实现会调用[super touchesBegan:touches withEvent:event],写测试代码时注意。 2、拦截,不再向下分发事件 实现方式是重写touchesBegan:withEvent:进行事件处理,不调用[super touchesBegan:touches withEvent:event]。 3、拦截,继续向下分发事件 实现方式是重写touchesBegan:withEvent:进行事件处理,同时调用super touchesBegan:touches withEvent:event]将事件往下传递。

3.2 响应链中的事件传递规则

每一个响应者对象(UIResponder对象)都有一个nextResponder方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。 对于响应者对象,默认的nextResponder实现如下: UIView,若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder是其父视图。 UIViewController,若控制器的视图是UIWindow的根视图,则其nextResponder为UIWindow对象,若控制器是从别的控制器present出来的,则nextResponder为presentint view controller。 UIWindow,nextResponder是UIApplication对象。 UIApplication,若当前应用的AppDelegate是一个UIResponder对象,且不是UIView、UIViewController或APP本身,则UIApplication的nextResponder是AppDelegate。

iOS在触摸屏幕后发生了什么? 如果有需要,完全可以重写响应者的nextResponder方法来自定义响应链。

五、事件处理的三个对象UIResponder、UIGestureRecognizer、UIControl

iOS中,除了UIResponder(响应者)能够响应事件,UIGestureRecognizer(手势识别器)和UIControl(继承自UIView)同样具备对事件的处理能力。 当这三个对象同时存在于某一个场景下时,点击事件会被如何处理呢?

(一)UIGestureRecognizer(手势识别器)

本章节要探讨UIGestureRecognizer(手势识别器)和UIResponder(响应者)的联系。 手势分为离散型手势(discrete gesture)和持续性手势(continuous gesture)。 系统提供的离散型手势包括点按手势(UITapGestureRecognizer)和轻扫手(UISwipeGestureRecognizer),其余均为持续性手势。 两者的区别在于状态变化过程: 离散型: 识别成功:Possible->Recognized 识别失败:Possible->Failed 持续型: 完整识别:Possible->Began->[Changed]->Ended 不完整识别:Possible->Began->[Changed]->Cancel

1.1 离散型手势测试

iOS在触摸屏幕后发生了什么?

在控制器的视图上添加了一个yellowView,并对yellowView添加一个单击手势识别器。

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *yellowView = [[UIView alloc] init];
    yellowView.frame = CGRectMake(0, 0, 300, 300);
    yellowView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:yellowView];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:**self** action: @selector(actionTap)];
    [yellowView addGestureRecognizer:tap];
}

- (void)actionTap {
    NSLog(@"yellowView Taped");
}

单击YellowView,日志打印如下:

yellowView touchesBegan
yellowView Taped
yellow touchesCancelled

从日志上看出来yellowView最后Cancel了对触摸事件的响应,而正常应当是触摸结束后,调用yellowView的touchesEnded: withEvent:方法,另外这里还调用了UIGestureRecognizer绑定的action。 官方文档的解释:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.

大致的意思是,Window在将事件传递给hit-tested view之前,会先将事件传递给相关的UIGestureRecognizer(手势识别器)并由UIGestureRecognizer(手势识别器)优先识别。 若UIGestureRecognizer(手势识别器)成功识别了事件,就会取消hit-tested view对事件的响应;若手势识别器没能识别事件,hit-tested view才会完全接手事件的响应。 一句话概括:UIGestureRecognizer比UIResponder具有更高的事件响应优先级! UIEvent所绑定的Touch对象上维护了一个UIGestureRecognizer(手势识别器)数组,Window先将事件传递给这些UIGestureRecognizer(手势识别器),再传给hit-tested view。 一旦有UIGestureRecognizer(手势识别器)识别了手势,Application就会取消hit-tested view对事件的响应。

1.2 持续型手势测试

将上面Demo中视图绑定的手势替换成UIPanGestureRecognizer(滑动手势识别器)。

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *yellowView = [[UIView alloc] init];
    yellowView.frame = CGRectMake(0, 0, 300, 300);
    yellowView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:yellowView];
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(actionPan)];
    [yellowView addGestureRecognizer:pan];
}

- (void)actionPan {
    NSLog(@"yellowView Panned");
}

在YellowView上进行滑动,日志打印如下:

[yellowView touchesBegan:withEvent:]
[yellowView touchesMoved:withEvent:]
[yellowView touchesMoved:withEvent:]
[yellowView touchesMoved:withEvent:]
yellowView Panned
[yellowView touchesCancelled:withEvent:]
yellowView Panned
yellowView Panned
yellowView Panned

在一开始滑动的过程中,UIGestureRecognizer(手势识别器)处在识别手势的阶段,滑动产生的连续事件既会传递给UIGestureRecognizer(手势识别器)也会传递给YellowView,因此YellowView的touchesMoved: withEvent:在开始的一段时间内会持续调用。 当UIGestureRecognizer(手势识别器)成功识别了该滑动手势时,UIGestureRecognizer(手势识别器)的action开始调用,同时通知UIApplication取消YellowView对事件的响应。之后仅由手势识别器接收事件并响应,YellowView不再接收事件。 在滑动的过程中,若UIGestureRecognizer(手势识别器)未能识别手势,则事件在触摸滑动过程中,会传递给hit-tested view,直到触摸结束。

1.3 手势识别器的3个属性

@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

(1)cancelsTouchesInView、 默认为YES,表示当UIGestureRecognizer(手势识别器)成功识别了手势之后,会通知UIApplication取消响应链对事件的响应,并不再传递事件给hit-tested view。 若设置成NO,表示UIGestureRecognizer(手势识别器)识别成功后不取消响应链对事件的响应,事件依旧会传递给hit-tested view。 (2)delaysTouchesBegan 默认为NO,默认情况下UIGestureRecognizer(手势识别器)在识别手势期间,UIApplication会将事件传递给手势识别器和hit-tested view。 若设置成YES,则表示UIGestureRecognizer(手势识别器)在识别手势期间会截断事件,不会将事件发送给hit-tested view。 (3)delaysTouchesEnded 默认为YES,当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)再调用UIResponder(响应者)的touchesEnded: withEvent:。 若设置成NO,则手势识别失败时会立即通知UIApplication发送状态为End的touch事件给hit-tested view调用touchesEnded: withEvent:方法结束事件响应。

(二)UIControl

UIControl是系统提供的能够以Target-Action模式处理事件的控件,iOS中UIButton、UISegmentedControl、UISwich等控件都是UIControl的子类。 当UIControl跟踪到触摸事件时,会向其上添加的Target发送事件以执行Action。 UIControl是UIView的子类,因此本身具备UIResponder的身份。 关于UIControl,主要介绍两点内容: 1、Target-Action执行时机和执行过程; 2、触摸事件的优先级;

2.1 Target-Action执行时机和执行过程

UIControl也会跟踪事件发生的过程,不同于UIResponder和UIGestureRecognizer通过touches系列方法跟踪,UIControl有独有的跟踪方式。

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

UIControl只能接收单点触控,因此接收的参数是单个UITouch对象。 这几个方法的职能和UIResponder的方法一致,用于跟踪触摸的开始、滑动、结束和取消。不过,UIControl本身也是UIResponder,因此同样有touches系列的4个方法。 实际上,UIControl的Tracking系列方法是在touch系列方法内部调用的,比如:beginTrackingWithTouch是在touchesBegan方法内部调用的。 因此,虽然UIControl也是UIResponder,但touches系列方法的默认实现和UIResponder还是有区别的。 当UIControl跟踪事件的过程中,识别出事件交互符合响应条件,就会触发Target-Action进行响应,UIControl通过addTarget: action: forControlEvents:添加事件处理的Target和Action。 当事件发生时,UIControl通知Target对应的Action。 UIControl调用sendAction: to: forEvent:将Target、Action和Event对象发送给全局应用,Application对象再通过sendAction: to: from: forEvent:向Target发送Action。

2.2 触摸事件优先级

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.This applies only to gesture recognition that overlaps the default action for a control, which includes:

  • A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
  • A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
  • A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

简单理解: UIControl上添加了UIGestureRecognizer(手势识别器)和Action时,UIGestureRecognizer(手势识别器)会响应事件并取消Action和UIResponder对事件的响应。 系统提供的有默认Action操作的UIControl,如:UIButton、UISwitch等的单击事件,会阻止父视图上的UIGestureRecognizer(手势识别器)行为,即UIControl处理事件的优先级比父视图的UIGestureRecognizer高。 若UIControl上的UIGestureRecognizer(手势识别器)cancelsTouchesInView属性设置为NO,则UIGestureRecognizer(手势识别器)和Button都能响应事件。 参考资料: blog.csdn.net/u014600626/…

转载自:https://juejin.cn/post/7374968339905413130
评论
请登录