锻炼吃饭的家伙

iOS 触摸、事件和响应

2019-04-29  本文已影响1人  LoveY34
touch

掏出手机->解锁手机->点开APP->低头刷手机,这一套流程是现在每个手机党每天重复最多的一套连击操作,那么作为开发人员的你有没有考虑过,你点击触摸手机屏幕的时候,手机系统做了些什么可以及时响应用户呢?

一.触摸&&事件&&响应者
1.触摸

触摸是这一切的源头,这是用户跟手机最直接的交互方式,这个物理行为对应到代码层面就是一个UITouch对象

@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);

// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);

@property(nullable,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);

注意:使用demo去查看UITouch对象生成的过程中,需要把UIView的属性multipleTouchEnabled置为true,否则UIView每次只能响应一个触摸,不管几根手指。。(别问我是咋知道的😂)

2.事件

触摸等操作的目的是为了生成事件以供响应者处理,一个事件对应一个UIEvent对象。

typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches,
    UIEventTypeMotion,
    UIEventTypeRemoteControl,
    UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};

最常见的就是由触摸产生的触摸事件,还有按压(3D touch)事件、远程控制事件和硬件运动事件。

3.响应者

事件产生后只有两种结果,要么就是被第一响应者接收并处理掉或者传递给别的响应者并处理掉,要么就是被第一响应者接收、传递但是这一过程中没有处理掉而被直接丢弃释放掉,这两种结果都需要一个重要的角色那就是响应者(UIResponder)。
响应者可以使任何继承自UIResponder的子类,像常见的UIViewUIViewControllerAppDelegateUIApplication等等。开发者可以通过UIResponder中的API去监听事件的生命周期,例如触摸事件的相关的几个API。

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (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);
二.接收事件&&传递事件&&处理事件

触摸屏幕到最后响应触摸事件整个过程看似在瞬间就完成了,其实这中间还是分很多步骤的,大体可以分为两个过程。(本文涉及到的触摸事件处理过程更多的属于软件层次,不涉及硬件层次)

1.寻找事件的第一响应者

当前应用程序接收到触摸事件后,会把触摸(UITouch)包裹在事件对象(UIEvent)中,再把该事件对象插入到一个由应用程序的UIApplication维护的事件队列中,并由UIApplication按照队列顺序依次分发。一次分发一个事件,但是响应者(UIResponder)众多,那事件到底应该分发给哪一个响应者呢?所以这第一步就是寻找第一响应者的这么一个过程。(我一直没弄明白这个事件分发队列是由系统维护的还是由当前应用程序的UIApplication维护的,若是由当前应用程序的UIApplication维护的话,那如果此时手机在桌面上的时候呢?这时候没有启动任何APP的,难道桌面其实也是一个APP??有大佬可以解惑下吗?欢迎在评论区留言。。)

查找第一响应者的过程本质上就是不断调用UIView的API去做试探

注意这里还不涉及到UIResponder的API哦!涉及的关键API有如下,而且都是UIView的API,所以寻找第一响应者的过程也叫做hitTest过程。

 // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  

简述一下查找第一响应者的过程就是:

a.如何判断当前视图能不能响应事件呢?

视图在以下情况下是不能响应事件的:

// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

b.视图如何把事件传递给子视图呢?

把事件传递的方式子视图的方式就是让循环子视图并让子视图调用hitTest:withEvent:,返回的UIView对象,就是当前视图层次中的响应者。

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event{
    if (!self.userInteractionEnabled ||
        !self.hidden ||
        self.alpha <= 0.01 ||
        ![self pointInside:point withEvent:event]) {
        //当前视图无法响应事件
        return nil;
    }
    
    __block UIView *fitView = self;
    __weak typeof(self) weakSelf = self;
    [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subView, NSUInteger idx, BOOL * _Nonnull stop) {
        CGPoint convertPoint = [weakSelf convertPoint:point toView:subView];
        UIView *subFitView = [subView hitTest:convertPoint withEvent:event];
        if (subFitView) {
            fitView = subFitView;
            *stop = YES;
        }
    }];
    
    return fitView;
}
2.事件的响应及传递

第一响应者找到后,它对事件具有处理权,它可以选择自己处理事件,也可以将事件传递给下一个响应者,而这个由响应者构成的视图链就称之为响应链。(敲黑板!这是重点,考试要考的。。)
响应者对于事件的拦截和传递都是通过touchesBegan:withEvent:方法控制的,任何一个UIResponder的对象都可以是响应者,对象中都会默认实现该方法,但是默认不会对事件做任何处理,只是将事件沿响应链传递,所以想要做自定义操作就需要重写方法。响应者一般对事件的处理方式有:

后两者的区别就在于有没有调用父类的touchesBegan:withEvent:方法。

响应链中的事件传递规则又是什么样的呢?

UIResponder有一个nextResponder API,用于获取当前响应者在响应链中的下一个响应者,因此第一响应者确定后,默认的响应链都已经通过nextResponder串起来了。
默认的nextResponder的规则

上一篇 下一篇

猜你喜欢

热点阅读