iOS事件是怎么处理的

1,005 阅读5分钟
先稍微回顾一下基础
我们肯定的一点是:不是所有的类都能接受并处理事件。那么,哪些类可以呢?继承自UIResponder的类。
UIResponder作为响应者,可以响应用户的触摸操作:
touchesBegan、touchesMoved、touchesEnded、touchesCancelled
在9.0增加了按压事件的处理:
pressesBegan、pressesChanged、pressesEnded、pressesCancelled
其实还有一些用户操作,用户的摇动、远程控制等
motionBegan、motionEnded、motionCancelled
remoteControlReceivedWithEvent

UIView、UIViewController、UIAppDelegate、UIApplication都是UIResponder的子类
UIWindow、UIControl是UIView的子类
UITableView、UICollectionView是UIScrollView的子类,UIScrollView是UIView的子类

UITouch
当用户触摸屏幕时,就会创建一个与手指相关联的UITouch对象。UITouch对象保存着与手指相关联的信息,例如,触摸位置、事件、阶段。离开屏幕时,销毁UITouch对象
获取触摸位置:
locationInView、previousLocationInView
9.1的API,获取精确位置,但是,在进行hit-test时不要使用精确位置(因为触摸可能会在视图内进行测试,但有一个精确的位置就在视图外)
preciseLocationInView、precisePreviousLocationInView
触摸类型:
typedefNS_ENUM(NSInteger, UITouchType) {
UITouchTypeDirect, // A direct touch from a finger (on a screen)
UITouchTypeIndirect, // An indirect touch (not a screen)
UITouchTypePencil API_AVAILABLE(ios(9.1)), // Add pencil name variant
UITouchTypeStylus API_AVAILABLE(ios(9.1)) = UITouchTypePencil, // A touch from a stylus (deprecated name, use pencil)
} API_AVAILABLE(ios(9.0));
触摸阶段:
typedefNS_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)
};

UIEvent
每产生一个事件就会创建一个UIEvent对象,UIEvent对象记录着事件类型及产生事件的时间。也可以通过事件获取某个view上的UITouch对象,如下:
touchesForWindow、touchesForView、touchesForGestureRecognizer
事件类型:
typedefNS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses API_AVAILABLE(ios(9.0)),
};
事件子类行:
typedefNS_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、UIEvent可以自己创建,但是,无法指定详细信息。通过UIApplication.sharedApplication sendEvent(event)派发会出现问题

有了以上基础,我们来说说三个问题:
事件是怎么传递的?
如何响应事件?
响应者链是什么?

1.事件传递
当手指触碰屏幕,一个触摸事件就产生了。经过IPC(进程间通信)将事件传递给当前应用程序,并调用UIApplication的sendEvent方法将事件添加到事件队列中

所以,事件是从上往下传递的。AppDelegate->UIApplication->UIWindow->UIViewcontroller->UIView->topSubView->first respond view

2.事件响应
当事件产生之后,iOS系统通过Hit-Testing来确定触摸事件发生在哪个视图上,并返回可以处理这个事件的视图,该视图被称为第一响应者。

找到响应的视图需要经过三步:
1)控件能否接收事件:不接收用户交互(userInteractionEnable)、隐藏(hidden)、透明(alpha)
2)触摸点是否在控件边界之内:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
3)从后往前遍历子控件(从subviews数组的最后一个开始),重复步骤1、2
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
注意:
1)如果经过步骤1-3没有符合条件的子控件,那么就自己最适合处理
2)如果父控件不能接收事件,那么,子控件就不可能接收到事件

3.响应者链
iOS系统通过Hit-Testing确定触摸事件发生在哪个视图上时,将能接收事件并且触摸点在控件边界内的视图加入响应链,这样构成的一个链条就叫做“响应者链”

如果第一响应者不能处理该事件,那么,会沿着响应者链向下传递,一直到window。如果window不处理,会传递到Application,Application还是不能处理,该事件将被丢弃。

我们看官方的一个响应链图:
自己实际操作如下:


1.首先构建一个View,添加顺序是A、C、B,C上添加D,且每个View都重写了hitTset:withEvent:和pointInside:withEvent:方法
2.触摸D时,先调A的hitTest方法。如果A,不可接收事件,则返回nil。
3.如果A可以接收触摸事件,紧接着调用A的pointInside方法,如果pointInside返回NO,表示触摸点不在A的边界内,则返回nil;
4.如果pointInside返回YES,则会遍历A的subviews,从最后一个开始遍历,即B view。先调用B的hitTest方法,在调用B的pointInside方法,发现触摸点不在B边界内,会遍历C view。
5.类似步骤2、3、4,找到了D view,从D->C->B->A的hitTest方法中返回D view。
6.如果没有找到,参照官方图。

最后:
官方有两个注意点:
1)与加速度、陀螺仪、磁强计相关的运动事件不遵循响应链,Core Motion会将这些事件直接传递到指定对象
2)如果触摸位置超出视图的范围,即使它们碰巧包含触摸,hitTest:withEvent:方法将忽略该视图及其所有子视图
3)在其默认配置中,即使有多个手指触摸该视图,视图仅接收与事件关联的第一个UITouch对象。若要接收其他触摸,必须将视图的multipleTouchEnabled属性设置为true

下面这个方法,也遵循响应链的。
[UIApplication.sharedApplication sendAction:@selector(btnClick) to:nil from:nil forEvent:nil];

官方参考: