iOS 触摸事件相关知识总结

1,700 阅读10分钟

<简书:恋空>

1.触摸事件和手势相关知识?

iOS 的事件分为三种,触摸事件(Touch Event)、加速器事件(Motion Events)、远程遥控事件(Remote Events)。这些事件对应的类为UIResponder

事件传递步骤:

(有个有趣的地方,UIApplication和AppDelegate也继承于UIResponder)

简单地说,自下而上。AppDelegate -> UIApplication -> UIWindow -> UIViewController -> UIView(父view一直遍历到子view,同层的view按后添加的view先遍历)。其遵循的规则如下:

自己是否能接收触摸事件?

不能接收的情况有三种

一、userInteractionEnabled = NO

二、 hidden = YES

三、 alpha = 0.0 ~ 0.01

触摸点是否在自己身上?

1.从后往前遍历子控件,重复前两个步骤。

2.若父控件不能接收触摸事件,不会传递给子控件。

3.如果没有符合条件的子控件,那么就自己最适合处理。

当事件传递给当前view时,当前view会调用- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法。寻找最适合的view。

返回谁,谁就是最合适的view。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

// 如果在这里直接返回yellowView

// 区域A的事件也会由B响应

// 所以这里还是直接调用父类方法

return [super hitTest:point withEvent:event];

// 本Demo中,甚至控制器view的点击事件也会被B响应,因为控制器view会遍历子控件最后一个(红色view),红色view调用这方法返回yellowView

}

事件响应步骤:

UIResponder -----> - (nullable UIResponder*)nextResponder;通过这个方法可以获取到当前view的控制器

@interface UIResponder : NSObject <UIResponderStandardEditActions>

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

- (nullable UIResponder*)nextResponder;

@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO

- (BOOL)canBecomeFirstResponder; // default is NO

- (BOOL)becomeFirstResponder;

@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES

- (BOOL)canResignFirstResponder; // default is YES

- (BOOL)resignFirstResponder;

@property(nonatomic, readonly) BOOL isFirstResponder;

- (BOOL)isFirstResponder;

// 触摸事件方法

// 手指触摸

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

@end

UITouch

UITouch对象记录 触摸的位置、时间、阶段。

一根手指对应一个UITouch对象。

手指移动时,系统会更新同一个UITouch对象。

手指离开屏幕时,UITouch对象被销毁。

@interface UITouch : NSObject

// 触摸产生时所处的窗口

@property (nonatomic, readonly, retain) UIWindow *window;

// 触摸产生时所处的视图

@property (nonatomic, readonly, retain) UIView *view;

// 短时间内点按屏幕的次数

@property (nonatomic, readonly) NSUInteger tapCount;

// 记录了触摸事件产生或变化的时间,单位:秒

@property (nonatomic, readonly) NSTimeInterval timestamp;

// 当前触摸事件所处的状态

@property (nonatomic, readonly) UITouchPhase phase;

typedef NS_ENUM(NSInteger, UITouchPhase) {

UITouchPhaseBegan, //(触摸开始)

UITouchPhaseMoved, // (接触点移动)

UITouchPhaseStationary, // (接触点无移动)

UITouchPhaseEnded, // (触摸结束)

UITouchPhaseCancelled, // (触摸取消)

};

// 返回触摸在view上的位置

// 相对view的坐标系

// 如果参数为nil,返回的是在UIWindow的位置

- (CGPoint)locationInView:(nullable UIView *)view;

// 返回上一个触摸点的位置

- (CGPoint)previousLocationInView:(nullable UIView *)view;

@end

UIEvent

每产生一个事件,就会产生一个UIEvent对象。记录事件产生的时刻和类型。本文探究的都是触摸事件。

响应链 --->简单地说,传递到最合适的view后,如果有实现touches方法那么就由此 View 响应,如果没有实现,那么就会自下而上,传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器-> UIWindow -> UIApplication -> AppDelegate】。

由这两张图,我们就可以知道每个UIResponder对象的nextResponder指向谁。

手势

手势识别和触摸事件是两个独立的概念。

UIResponder

UIResponder是iOS中用于处理用户事件的API,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent等方法,获取到对应的回调消息。UIResponder不只用来接收事件,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者处理。

应用程序通过响应者来接收和处理事件,响应者可以是继承自UIResponder的任何子类,例如UIView、UIViewController、UIApplication等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。

第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

查找第一响应者

基础API

查找第一响应者时,有两个非常关键的API,查找第一响应者就是通过不断调用子视图的这两个API完成的。

调用方法,获取到被点击的视图,也就是第一响应者。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:方法内部会通过调用这个方法,来判断点击区域是否在视图上,是则返回YES,不是则返回NO。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

查找第一响应者

应用程序接收到事件后,将事件交给keyWindow并转发给根视图,根视图按照视图层级逐级遍历子视图,并且遍历的过程中不断判断视图范围,并最终找到第一响应者。

视图的hidden等于YES。

视图的alpha小于等于0.01。

视图的userInteractionEnabled为NO。

如果点击事件是发生在视图外,但在其子视图内部,子视图也不能接收事件并成为第一响应者。这是因为在其父视图进行hitTest:withEvent:的过程中,就会将其忽略掉。

事件传递

传递过程

UIApplication接收到事件,将事件传递给keyWindow。

keyWindow遍历subViews的hitTest:withEvent:方法,找到点击区域内合适的视图来处理事件。

UIView的子视图也会遍历其subViews的hitTest:withEvent:方法,以此类推。

直到找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication。

在查找第一响应者的过程中,已经形成了一个响应者链。

应用程序会先调用第一响应者处理事件。

如果第一响应者不能处理事件,则调用其nextResponder方法,一直找响应者链中能处理该事件的对象。

最后到UIApplication后仍然没有能处理该事件的对象,则该事件被废弃。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {

return nil;

}

BOOL inside = [self pointInside:point withEvent:event];

if (inside) {

NSArray *subViews = self.subviews;

// 对子视图从上向下找

for (NSInteger i = subViews.count - 1; i >= 0; i--) {

UIView *subView = subViews[i];

CGPoint insidePoint = [self convertPoint:point toView:subView];

UIView *hitView = [subView hitTest:insidePoint withEvent:event];

if (hitView) {

return hitView;

}

}

return self;

}

return nil;

}

如上图所示,响应者链如下:

如果点击UITextField后其会成为第一响应者。

如果textField未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。

父视图未处理事件则继续向下传递,也就是UIViewController的View。

如果控制器的View未处理事件,则会交给控制器处理。

控制器未处理则会交给UIWindow。

然后会交给UIApplication。

最后交给UIApplicationDelegate,如果其未处理则丢弃事件。

事件通过UITouch进行传递,在事件到来时,第一响应者会分配对应的UITouch,UITouch会一直跟随着第一响应者,并且根据当前事件的变化UITouch也会变化,当事件结束后则UITouch被释放。

UIViewController没有hitTest:withEvent:方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者

注意

在执行hitTest:withEvent:方法时,如果该视图是hidden等于NO的那三种被忽略的情况,则改视图返回nil。

如果当前视图在响应者链中,但其没有处理事件,则不考虑其兄弟视图,即使其兄弟视图和其都在点击范围内。

UIImageView的userInteractionEnabled默认为NO,如果想要UIImageView响应交互事件,将属性设置为YES即可响应事件。

事件控制

事件拦截 --- > 有时候想让指定视图来响应事件,不再向其子视图继续传递事件,可以通过重写hitTest:withEvent:方法。在执行到方法后,直接将该视图返回,而不再继续遍历子视图,这样响应者链的终端就是当前视图。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

return self;

}

事件转发

在开发过程中,经常会遇到子视图显示范围超出父视图的情况,这时候可以重写该视图的pointInside:withEvent:方法,将点击区域扩大到能够覆盖所有子视图。

事件逐级传递

如果想让响应者链中,每一级UIResponder都可以响应事件,可以在每级UIResponder中都实现touches并调用super方法,即可实现响应者链事件逐级传递。

只不过这并不包含UIControl子类以及UIGestureRecognizer的子类,这两类会直接打断响应者链。

Gesture Recognizer

如果有事件到来时,视图有附加的手势识别器,则手势识别器优先处理事件。如果手势识别器没有处理事件,则将事件交给视图处理,视图如果未处理则顺着响应者链继续向后传递。

当响应者链和手势同时出现时,也就是既实现了touches方法又添加了手势,会发现touches方法有时会失效,这是因为手势的执行优先级是高于响应者链的。

事件到来后先会执行hitTest和pointInside操作,通过这两个方法找到第一响应者,这个在上面已经详细讲过了。当找到第一响应者并将其返回给UIApplication后,UIApplication会向第一响应者派发事件,并且遍历整个响应者链。如果响应者链中能够处理当前事件的手势,则将事件交给手势处理,并调用touches的calcelled方法将响应者链取消。

在UIApplication向第一响应者派发事件,并且遍历响应者链查找手势时,会开始执行响应者链中的touches系列方法。会先执行touchesBegan和touchesMoved方法,如果响应者链能够继续响应事件,则执行touchesEnded方法表示事件完成,如果将事件交给手势处理则调用touchesCancelled方法将响应者链打断。

根据苹果的官方文档,手势不参与响应者链传递事件,但是也通过hitTest的方式查找响应的视图,手势和响应者链一样都需要通过hitTest方法来确定响应者链的。在UIApplication向响应者链派发消息时,只要响应者链中存在能够处理事件的手势,则手势响应事件,如果手势不在响应者链中则不能处理事件。

UIControl

根据上面的手势和响应者链的处理规则,我们会发现UIButton或者UISlider等控件,并不符合这个处理规则。UIButton可以在其父视图已经添加tapGestureRecognizer的情况下,依然正常响应事件,并且tap手势不响应。

以UIButton为例,UIButton也是通过hitTest的方式查找第一响应者的。区别在于,如果UIButton是第一响应者,则直接由UIApplication派发事件,不通过Responder Chain派发。如果其不能处理事件,则交给手势处理或响应者链传递。

不只UIButton是直接由UIApplication派发事件的,所有继承自UIControl的类,都是由UIApplication直接派发事件的。

小技巧

在开发中,有时会有找到当前View对应的控制器的需求,这时候就可以利用我们上面所学,根据响应者链来找到最近的控制器。

在UIResponder中提供了nextResponder方法,通过这个方法可以找到当前响应环节的上一级响应对象。可以从当前UIView开始不断调用nextResponder,查找上一级响应者链的对象,就可以找到离自己最近的UIViewController。

示例代码:

- (UIViewController *)parentController {

UIResponder *responder = [self nextResponder];

while (responder) {

if ([responder isKindOfClass:[UIViewController class]]) {

return (UIViewController *)responder;

}

responder = [responder nextResponder];

}

return nil;

}