#前言:
项目需求:在iPhone上通过触摸和手势来操作远程的鼠标。
iOS中事件分为3种:
触控事件(Touch Event)(单点、多点触控以及手势)
传感器事件(Motion Event)(重力、加速器等)
远程控制事件(Remote-Control Event)(远程遥控iOS设备多媒体播放等)
参考链接:
https://developer.apple.com
- 触摸事件(触屏事件)
因为UIView是UIResponder的子类,所以可以覆盖以下4个方法,来处理4中不同的触摸事件。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; (1或者多根手指开始触摸屏幕)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; (1或者多根手指在屏幕中移动,相关的对象会持续发送该消息)
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; (1或者多根手指离开屏幕)
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; (在触摸正常结束前,某个系统事件发生了,打断触摸事件)
当系统检测到触摸事件时,会创建UITouch对象(一根手指的触摸事件对应一个UITouch对象)。发生触摸事件的UIView对象会收到touchesBegan: withEvent:
。系统给你的第一个参数touches(NSSet对象)并、包含了所有相关的UITouch对象。
当手指在屏幕上移动时,系统更新对应的UITouch对象,会把手指在屏幕上的位置更新到对应的UITouch中。最初发生触摸事件的UIView会收到touchesMoved: withEvent:
消息。
当手指离开屏幕时,系统会最后一次更新相应的UITouch对象,比如对应手指在屏幕上的位置。接着,最初发生该触摸事件的视图会收到touchesEnded: withEvent:
消息。当收到该消息的视图执行完touchesEnded:withEvent:后,系统会释放和当前事件有关的UITouch对象。
下面对UITouch
对象和事件响应方法的工作机制做一个小归纳。
一个
UITouch
对象对应屏幕上的一根手指。只要手指没有离开屏幕,对应的UITouch对象就会一直存在。这些UITouch
对象都会保存对应的手指在屏幕上的当前位置。在触摸事件持续的过程中,最初发生触摸事件的那个视图都会在各个阶段收到相应的触摸事件消息。即使手指在移动时离开了这个视图的frame区域,系统还是会向该视图发送
touchesMoved:withEvent:
消息和touchesEnded:withEvent:
消息。换句话说就是,当一个视图发生触摸事件后,该视图将一直拥有当时创建的所有UITouch对象。(编写代码时不需要也不应该保留任何UITouch对象)- 当多根手指在同一个视图、同一个时刻,执行相同的触摸动作时,UIApplication实例会使用单个消息、一次分发所有相关的UITouch对象。该实例在发送特定的UIResponder消息时,会传入一个NSSet对象,该对象将包含所有相关的UITouch对象(所有手指)。但是,因为UIApplication实例对“同一时刻”的判断标准很严格,所以通常情况下,哪怕一组事件都是在很短的一段时间内发生的,该实例也会发送多个UIResponder消息。分批发送UITouch对象。
- 响应者链 (Responder Chain)
响应者链就是由一系列的响应者对象
构成的一个层次结构。响应者对象
:指的是有响应和处理事件能力的对象。它开始于第一个响应者然后结束于application对象。如果第一响应者无法处理事件,它会将事件沿响应者链中的next responder传递。
UIResponder是所有响应对象的基类,在UIResponder累中定义了出来上述各种事件的接口。UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。(note :core animation layers不是响应者对象)
第一响应者是设计为第一个接收事件的。通常,第一响应者是一个view对象。一个对象要成为第一个响应者可以通过做下面的2件事:
- 重写
canBecomeFirstResponder
方法来返回YES
. - 并接收
becomeFirstResponder
消息。如果需要,一个对象可以给他自己发送这个消息。
Note: Make sure that your app has established its object graph before assigning an object to be the first responder. For example, you typically call the becomeFirstResponder method in an override of the viewDidAppear: method. If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO.
Events不仅仅是唯一一个依赖于响应者链的对象。响应者链是可以用到下面的:
- Touch events 触摸事件:如果hit-test view不能处理触摸事件,该事件将从hit-test view开始沿响应者链传递。
- Motion events 运动事件:为了处理shaking-motion事件。第一响应者必须需要实现UIResponder类中
motionBegan:withEvent:
或者motionEnded:withEvent:
方法。 - Remote control events 远程控制事件:为了处理远程控制事件,第一响应者必须实现UIResponder类中
remoteControlReceivedWithEvent:
方法。 - Action messages 动作消息:当用户操作有个控件比如button或者switch,然后这个的action的target是nil的时候,这个消息会通过这个控件的视图开始往响应者链传递。
- Editing-menu messages 编辑菜单:当用户点击了编辑菜单中的命令时,iOS会通过响应者链来找到一个实现了necessary方法(例如cut:,copy:,and paste:)的对象
- Text editing 文本编辑:当用户点击了text field或者一个text view,该view会自动成为第一响应者。默认的,虚拟键盘会出现或者text view会聚焦到编辑区域。如果合适的话,你可以展示一个自定义的输入界面来替代键盘。你也可以增加一个自定义的输入视图给任意一个响应者对象。
7.
UIKit automatically sets the text field or text view that a user taps to be the first responder; Apps must explicitly set all other first responder objects with the becomeFirstResponder method
响应者链按照特殊的传递路径触底。
如果初始对象-既不是hit-test view也不是first responder–没有处理任何事件,UIKit会将事件沿响应者链传递给the bext responder。每一个响应者决定是否想要处理该事件或者是通过调用nextResponder
方法来将事件传递给它的next responder。这个过程会一直继续直到一个响应者对象处理该事件或者没有更多其他的响应者。
当iOS检测到一个事件,响应者链便开始序列并传递给一个初始的对象(一般是一个view)。这个初始视图拥有第一个机会来处理一个事件。
左边的图片的事件通过下面这样子的路径传递:
- 初始视图尝试处理这个事件(或者消息)。如果它不能处理这个事件,便将该事件传递给它的superview,因为这个初始视图不是这个view controller视图层次中最顶层的视图。
- 这superview尝试处理这个事件。如果它也不能处理该事件,便将事件传递给它的superview,因为它也不是视图层次中最顶层的视图(top most view)。
- 这个view controller中的topmost view尝试处理该事件,如果它也不能处理该事件,便将该事件传递给它的view controller。
- 这个view controller尝试处理该事件,如果也不能处理,传递该事件给window。
- 如果这个window object也不能处理,便将该事件传递给singleton app object。
- 如果这个app object也不能处理,那么它便会丢弃该事件。
右边图片的事件通过稍微的不同的路径传递,但是所有的事件传递路径都是根据下面:
- 一个视图传递一个事件给它的view controller的视图层次直到到达topmost view。
- 然后这个topmost view传递事件给它的view controller。
- 这个view controller传递事件到它的topmost view’s superview。(步骤1-3一直重复直到事件到达root view controller)
- root view controller传递事件给window对象
- window传递事件给app对象。
Important: If you implement a custom view to handle remote control events, action messages, shake-motion events with UIKit, or editing-menu messages, don’t forward the event or message to nextResponder directly to send it up the responder chain. Instead, invoke the superclass implementation of the current event handling method and let UIKit handle the traversal of the responder chain for you.
从图中可看出:
- 响应者链通常是由视图(UIView)构成的。
- 一个视图的下一个响应者是它的视图控制器(UIViewController)(如果有的话),然后再转给它的父视图(super View)。
- 视图控制器(如果有的话)的下一个响应者是该视图控制器管理的视图的父视图。
- 窗口(UIWindow)的内容视图将指向窗口本身作为它的下一个响应者。 cocoa touch应用不像cocoa应用,它只有一个UIWindow对象,因此整个响应者链要简单一点;
- 应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向nil,来结束整个循环。
- 事件分发 (Event Delivery)
第一响应者(First responder)指的是当前接受触摸的响应者对象(通常是一个UIView对象),即当前正在与用户交互,它是响应者链的开端。整个响应者链和事件分发的使命都是找出第一响应者。
UIWindow对象以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看它是否能进行处理。
iOS使用hit-testing 来查找在触摸底下的那个对应的视图。hit-testing检测触摸事件是否在任何有关联的视图的范围内。如果有在范围内,这个视图会递归调用它所有的子视图。在该包含触摸点的视图层次结构中最低的视图(就是最顶层的视图)成为hit-test view。在iOS找到hit-test view后,它会传递触摸事件到这个view来处理。
下面结合上图介绍hit-test view的流程,假如用户点击了View E:
1、触摸发生在View A的范围内,所以开始检测A的子视图B和C;
2、触摸不在View B的范围内,但在View C内,所以开始检测C的子视图D和E;
3、触摸不在View D的范围内,但在View E的范围内:
View E是在包含触摸的视图层次中最低的视图(就是最顶层的视图),所以它变成hit-test View;
hitTest: withEvent:
方法会返回给定的在命中的view(hit test view)的CGPoint
和UIEvent
。这个hitTest: withEvent:
首先调用它本身的pointInside:withEvent:
方法。如果这个触摸点传递给hitTest:withEvent:
是在这个view的范围内,则pointInside:withEvent:
返回YES
.然后这个方法会递归调用它的每一个子视图中hitTest:withEvent:
返回YES
的方法。
如果这个点传递给hitTest:withEvent:
是不在这个view的范围内,那么第一个调用pointInside:withEvent:
方法会返回NO
,然后这个点会被忽略,hitTest:withEvent:
返回nil
.如果一个子视图返回NO
,那么整个的视图层次结构中的该分支会被忽略。以为这个触摸不发生在这个子视图中,因此也不会发生在该子视图的子视图中。意味着在一个子视图的任何点是在它的父视图(superview)以外的是不能接收到触摸事件的,因为触摸点必须是在父视图和子视图的范围内的。当它的子视图的clipsToBounds
的属性设置为NO
时,这是会发生的。
点击的范围在E内,即E的pointInside:withEvent:返回YES,由于E没有子视图(也可以理解成对E的子视图进行hit-test时返回了nil),因此,E的hitTest:withEvent:会将E返回,再往回回溯,就是C的hitTest:withEvent:返回E—>>A的hitTest:withEvent:返回E。
至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。
三、说明
1、如果最终hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;
2、hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。
3、我们可以重写hitTest:withEvent:来达到某些特定的目的,下面的链接就是一个有趣的应用举例,当然实际应用中很少用到这些。
参考文档:
https://developer.apple.com
事例代码从这里下载
[bynomial.com][http://bynomial.com/blog/?p=74]
- 事件传递
iOS中事件传递首先是从UIApplication开始,接着传递到Window,再接着往下传递到View之前,window会将事件交给GestureRecognizer,如果在此期间,GestureRecognizer识别了传递过来的事件,则该事件将不会继续传递到View去,而是交给Target(ViewController)进行处理。