iOS 触摸事件相关知识总结
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;
}