乔帮主在发布会上提到,用户的手才是最好的输入设备,的确,iPhone之后,非触屏手机再已难觅。触摸是最基本的用户输入事件,理解iOS特有的触摸事件响应机制,能够良好管理程序中触摸响应方法,避免冲突的发生。
iOS中的事件
iOS中的事件主要分为三类:
- UIControl Actions: 使用target/action注册的SEL。
- User Events: 用户与应用之间的交互:触摸,输入文字,摇晃,远程控制等。
- System Events: 应用启动,切前后台,低内存等。
cocoa和cocoa touch的程序启动后,,会首先初始化一些基本资源:在主线程创建一个main event loop;初始化主UIWindow
。
应用启动过程-w500
main event loop本质上是一个NSRunLoop
,与其他辅助线程的run loop不同,其是自创建后自动开始运行的。主消息循环最大的特点是:它在创建时就与负责捕获用户事件的系统底层建立了连接,所以它的input source可以收到系统传递过来的用户事件。UIApplication
对象会将当前要处理的用户事件封装成UIEvent
,发送给UIWindow
,在由UIWindow
转发给对应的响应者。
iOS的触摸事件响应机制
hit-testing流程
iOS中hit-testing使用逆前序的深度遍历算法来确定用户点按的最低层级(最靠近用户)的视图,该hitTest视图是触摸事件的响应链头结点。
逆前序的深度遍历算法:根节点-->右子树-->左子树。
当收到触摸事件后,UIApplication
在当前视图层级中,从key window
开始(最顶级),从上往下遍历子视图调用hitTest:withEvent:
,若找到hitTest视图则停止遍历并返回。
当视图收到hitTest:withEvent:
方法后,通过下列条件判断是否在该视图执行hit-testing。-
pointInside:withEvent:
方法返回YES。pointInside:withEvent:
方法用来判断触摸点是否在当前视图内。 - hidden == NO。
- userInteractionEnabled == YES。
- alpha >= 0.01。若view的content绘制为透明的,则不受影响。
需要注意的是,当clipsToBounds == NO时,视图的子视图可能会超出其bounds,这种情况如果触摸点在子视图超出父视图的范围,那么hit-tesing不会再此视图树上执行。
hit-testing
如图,当用户触摸viewB.1
时,UIApplication
对象收到触摸事件,从key window
开始执行hit-testing,首先访问viewC
,由于pointInside:withEvent:
方法返回NO,取消执行并访问viewB
,满足执行,则从右往左开始访问其子视图(视图层级从下往上),找到viewB.1
,它没有子视图,则返回自己。最终UIWindow
对象将viewB.1
作为hitTest视图返回给UIApplication
对象。
hit-testing流程图
可以看到,当某一视图收到hitTest:withEvent:
方法后,它会向所有子视图发送hitTest:withEvent:
方法,若它的没有子视图或所有子视图返回nil,那么就返回自己,所有hit-testing流程最终一定会找到一个对象UIView/UIWindow
去接收触摸事件。
以下是hitTest:withEvent:
可能的实现。- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) { return nil; } if ([self pointInside:point withEvent:event]) { for (UIView *subview in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; }
responder chain
responder chain是
UIResponder
对象组成的链形结构,它以first responder为头结点,UIApplication
对象为尾节点,事件从头开始在响应链中向上传递。
UIResponder
用来设计处理事件,UIApplication
,UIViewController
,UIView
都是其子类,只要它们实现了UIResponder
中的钩子方法,就可以响应对应的事件。
UIResponder的继承关系
其中first responder
用来第一个接触事件,可以使用becomeFirstResponder
来设置它,主要要在视图层级已经完全建立之后再设置。If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO
默认情况下,
fist responder
是当前UIWindow
中最有可能响应事件的UIView
,这由UIkit
决定。
iOS中大部分的事件都依赖响应链来找到最终的响应者,在UIResponder
的头文件中可以看到,Touch events,Motion events,Remote events,UIControl Action,Text editing,press events等事件都可以在响应链中传递。寻找响应对象
当
UIApplication
在处理的事件时,触摸事件会交给hitTest view
开始的响应链处理,其他的动作事件,远程事件,系统事件等,会交给first responder
开始的响应链处理。
UIKit会将用户事件发送给理论上最合适的对象。所以当程序中的响应者要经过很长的查找路径时,这时就要考虑是否实现是否设计合理了。UIKit first sends the event to the object that is best suited to handle the event. For touch events, that object is the hit-test view, and for other events, that object is the first responder
对于触摸事件,hit-test视图获得了最先接受触摸对象的机会,但如果它不能处理对应的触摸事件,那么UIKit会沿着以hit-test开头的响应链寻找能够最终的响应者。
The responder chain on iOS
当找到响应者或已经到链尾(UIApplication)仍不能处理,UIKit会停止查找,对于后者,对应的事件会被丢弃。
除了
UIResponder
对象,UIGestureRecognizer
与UIControl
也可以响应触摸事件,但它们参与触摸事件响应的方式不同。-
UIGestureRecognizer
在响应链中的位置取决于依附的视图。 -
UIControl
参与响应的方式决定于其关联的target。
UIGestureRecognizer
要先于视图收到触摸事件,但需要注意的是,若该视图也可以响应触摸事件(实现了UITouch
生命周期函数),那么手势对象并不会阻碍视图的响应,双方是同时响应的,只不过存在先后顺序。
UIGestureRecognizer与UIView的接触事件的次序
响应触摸事件
当确定了响应链后,
UIWindow
会向hitTest View
发送以下方法:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
这是
UIResponder
用于响应触摸事件的方法,这些钩子方法的默认实现是向nextResponder
转发方法。
当触摸事件在响应链上传递时,判断当前UIResponder
能否响应的条件是:其是否实现了touchesBegan
方法。
在这些UITouches
序列的生命周期方法中,我们可以获取对应UIEvent
与UITouch
,利用它们所提供的信息,进一步决定如何响应用户的触摸事件。 -
网友评论