事件(UIEvent)是一个对象,被发送给 Application,包含了一些信息来告诉我们的程序用户干了些什么。事件在 iOS 中主要包含3种:MultiTouch Events、Accelerometer Events、Remote Control Events。
MultiTouch Events
用户触摸屏幕产生的事件或 Smart Pencil 接触屏幕产生的事件,如点击、捏合等等
Accelerometer Events
传感器产生的事件,如用户摇晃手机等。
Remote Control Events
使用遥控器或耳机线控等产生的事件。如播放、暂停等。
事件的响应及传递
当一个事件产生后,首先会发给 UIApplication 对象,UIApplication 对象会查找当前应用的keyWindow对象,然后将事件发送给它。keyWindow对象收到消息后,通常情况下不会自己处理事件,而是将事件中的触摸事件提取出来,然后调用-[UIResponder touches*:withEvent:]系列方法交给 UIResponder 处理。
我们常见的UIWindow、UIViewController、UIView都是UIResponder的子类。
UIResponder 提供了一下一些方法,并提供了基础实现。
// UIResponder的touches系列方法(MultiTouch Events)
// 手指开始点击屏幕
- (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:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet * _Nonnull)touches NS_AVAILABLE_IOS(9_1);其他的还有:
// Accelerometer Events
// 加速度感应器事件
- (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);
// 按压事件(如iPhone 6s/6s Plus及以上机型的3D Touch)
- (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);
// Remote Control Events
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);既然我们UIWindow、UIViewController、UIView都是UIResponder的子类,那么当一个事件到来后,keyWindow对象会发送给谁来响应呢?
在这里要讲一下 Responder Chain 的概念:
每一个UIResponder对象都不是孤立的对象,而是(由系统将他们)串成了一个链条。第一个UIResponder对象即为First Responder,而后通过nextResponder属性指向下一个UIResponder对象。
- (nullable UIResponder*)nextResponder;而First Responder则由如下方法管理:
// default is NO
- (BOOL)canBecomeFirstResponder;
- (BOOL)becomeFirstResponder;
// default is YES
- (BOOL)canResignFirstResponder;
- (BOOL)resignFirstResponder;
- (BOOL)isFirstResponder;所以,事件将交给 First Responder 来处理,如果 First Responder 不响应事件,则按照 Responder Chain 依次将进行。
但是,MultiTouch Events 会略有不同,事件的响应顺序跟上面一样,但用户的触摸事件可能会改变 First Responder。
在每一个UIView中,都有:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;所以,当用户与屏幕交互后(如touchesBegan、touchesMoved、touchesEnded等),会从当前(全屏)视图的最下面一个 View 开始,依次执行hitTest方法。
默认实现的hitTest方法会调用pointInside方法询问当前用户交互的坐标是否在自己bounds里面,如果是,则继续遍历其所有subView,递归询问每个pointInside为YES的 subView。所以,如果一个 View 的pointInside方法返回NO,那么其所有 subView 将被略过。
所以,如果一个 View 的clipsToBounds值为默认的NO,并且其有子类的边界超出了其 Bounds 范围,如果不重写pointInside方法,那么在其 Bounds 范围之外的区域用户是点不到的。
最终会找到距离屏幕最近(subView 子树最末端)的 View,并返回这个 View。
然后询问其是否可以成为First Responder,如果可以,则使其成为First Responder。
在此之后,如果其不响应事件,则按照之前所说的按照 Responder Chain 依次询问,直到某一个UIResponder响应事件。
通常情况下,事件会传递给 superView,superView 继续往上传递。如果其中某个 View 有 ViewController 指向此 View,则会将事件传递给 ViewController,然后再传递给其 superView。直到传递给 UIWindow 对象,如果 UIWindow 依然不处理此事件,则会传给 UIApplication 对象。到此,整个事件结束。
响应事件
想要响应什么事件,就实现对应的方法就可以了。
例如想要响应触摸开始事件,实现如下方法就可以了:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;UITouch
// 触摸事件的坐标
- (CGPoint)locationInView:(nullable UIView *)view;
// 上一次触摸事件的坐标
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// 时间戳
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase phase;
/*
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)
};
*/
// touch down within a certain point within a certain amount of time
@property(nonatomic,readonly) NSUInteger tapCount;
// 触摸类型,如手指在屏幕上点击、间接地(非屏幕)触摸、使用触控笔Touch
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);
// Force of the touch, where 1.0 represents the force of an average touch
// 压力力度
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
//触控笔倾斜角度
@property(nonatomic,readonly) CGFloat altitudeAngle NS_AVAILABLE_IOS(9_1);
//更多地属性请查看UITouch类UIView要支持多点触控,需要将multipleTouchEnabled设为YES,默认为NO。
UIEvent
//
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);
/*
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, //触摸
UIEventTypeMotion, //运动(加速度传感器等)
UIEventTypeRemoteControl, //
UIEventTypePresses //按压(3D Touch等) NS_ENUM_AVAILABLE_IOS(9_0),
};
*/
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
/*
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,
};
*/
@property(nonatomic,readonly) NSTimeInterval timestamp;