转载请注明出处! http://joakimliu.github.io/2017/01/15/iOS%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86/ 谢谢!
最近把 Event Handling Guide for iOS 看了几遍,算是对 iOS 的事件处理有了个整体的概念, 本文较长,可以先看后面的总结部分 。对于事件处理,我们最熟悉的莫过于下面的 Target-Action
模式代码。
UIControl addTarget:action:forControlEvents: UIGestureRecognizer initWithTarget:action:
这些都是比较高级的用法了,因为 UIKit 都帮我们处理了, Gesture recognizers convert low-level event handling code into higher-level actions.
低级的事件处理就是所谓的自定义事件处理。在这之前,我们先谈谈 iOS 中表示事件的相关类。
A UIEvent object (or, simply, an event object) represents an event in iOS.
在 iOS 中,事件是由 UIEvent 类表示的,大致可以分为四种类型
typedef NS_ENUM(NSInteger, UIEventType) { UIEventTypeTouches, // 触摸事件,按钮、手势等 UIEventTypeMotion, // 运动事件,摇一摇、指南针等 UIEventTypeRemoteControl, // 远程控制,耳机等 UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0), // 3D touch };
这里只说 UIEventTypeTouches 触摸事件(注:本文所说的事件都是触摸事件)。 event 里面包含一个或者多个 touch (代表手指触摸屏幕,由 UITouch 类表示,下面会说)。当触摸事件发生时,系统会将它路由到合适的响应者,然后通过 UIResponder 的 touchesBegan:withEvent: 等方法传递。系统会评估这个事件并且找到合适的对象来处理它(包括 hit-testing
和 first responder
),一般情况下,我们不需要做特殊的处理。所以 UIEvent 类里面有多个获取 UITouch 对象的方法:
- (nullable NSSet <UITouch *> *)allTouches; - (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window; - (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view; - (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2); - (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0); - (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
这里有两点需要注意一下:
You should never retain an event object or any object returned from an event object.
不要持有事件对象或者从事件 对象中返回的对象。因为 UIEvent 对象在多点触摸序列(指手指触摸屏幕到离开屏幕)中是持久的, UIKit 会重用 UIEvent 对象,如果你需要持有 event 或者 touch 的相关信息时,你可以拷贝相关信息,赋值给相关变量。 它代表手指触摸到屏幕上的位置,大小等相关信息。一个手指代表一个 UITouch 对象,所以可以根据 tapCount
属性来判断是单击、双击、三击。根据 touch 可以我们可以得知
当然还可以知道接受该 touch 对象的 gestureRecognizers 手势识别器。
我们还可以根据 phase 属性获取 touch 的相关状态。
UITouchPhaseBegan: A finger for a given event touched the screen. UITouchPhaseMoved: A finger for a given event moved on the screen. UITouchPhaseStationary: A finger is touching the surface but hasn't moved since the previous event. UITouchPhaseEnded: A finger for a given event was lifted from the screen. UITouchPhaseCancelled: The system canceled tracking for the touch, as when (for example) the user moves the device against his or her face.
除了 UITouchPhaseStationary 状态以外,每个状态都对应着 UIResponder 的 touchXxxxx:withEvent:
类似的方法。拿到 UITouch ,我们就可以指定这个触摸事件所在的 view 以及位置。拿到位置以后,我们就可以做我们想要做的事情了。
The UIResponder class defines an interface for objects that respond to and handle events. It is the superclass of UIApplication, UIView and its subclasses (which include UIWindow). Instances of these classes are sometimes referred to as responder objects or, simply, responders.
它定义了响应和处理事件的接口。像一些能够处理相应事件的类(UIApplication, UIView 等)都是它的子类。
// 处理触摸事件的主要方法,所以我们自定义事件处理时,就需要在这几个方法里面做文章。 - (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; // 收到系统的事件(如,来电话了或者低内存警告等)取消触摸事件
只要手指触摸在屏幕上,不管是手指拖动还是离开屏幕, UIEvent 对象就会生成,而它由包含相关的 UITouch 对象。入参说明:
This differs from the set of touches because some of the touch objects in the event may not have changed since the previous event message.
它跟上面的 touches 的区别就在于它可能包含发生改变的 touch 。强调一个状态的改变。 Returns the receiver's next responder, or nil if it has none.
接受者的下一个响应者,如果没有的话就为 nil。nextResponder 就是响应链中下一个处理事件的对象。
UIResponder 类不会自动存储 nextResponder,所以默认返回 nil。子类化的时候需要重载该方法自己设置。一般情况下,UIView 一般返回它所在的 UIViewController 或者它的 superview;而 UIViewController 返回它 view 的 superview 或者 UIViewController (会一直循环找,直到找到 UIWindow -> UIApplication);UIWindow 返回 UIApplication;UIApplication 返回 nil。所以,响应链在视图层级构建的时候就已经形成了。
Returns a Boolean value indicating whether the receiver is the first responder.
是否是第一响应者。默认为 YES。
Returns a Boolean value indicating whether the receiver can become first responder.
是否能够成为第一响应者。默认为 NO。
如果返回 YES ,就说明它能够成为第一响应者, it becomes the first responder and can receive touch events and action messages.
能够接受触摸事件和动作消息。子类如果想要成为第一响应者,那么必须重载这个方法。注意,你只有当 view 已经添加到 view 层级里面才能发送这个消息(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.
注意:在一个对象成为 first repsonder 之前要确保建立好 object graph。例如,你通常在 viewDidAppear: 方法里面调用 becomeFirstResponder。如果 viewWillAppear: 方法里面设置 first responder,这个时候 object graph 还没建立好,所以 becomeFirstResponder 会返回 NO。
Notifies the receiver that it is about to become first responder in its window.
报告接受者它将要在 window 上成为为第一响应者。默认返回 YES。
A responder object only becomes the first responder if the current responder can resign first-responder status (canResignFirstResponder) and the new responder can become first responder.
只有当前的响应者能够辞去第一响应者,新的响应者才能够成为第一响应者。也就是说第一响应者永远只有一个。
If the view’s window property holds a UIWindow object, it has been installed in a view hierarchy;if it returns nil, the view is detached from any hierarchy.
view 的 window 属性持有 UIWindow 对象时才表示这个 view 已经添加到 view 层级中。也就说只有 view 层级确定成功后才能成为第一响应者。
Returns a Boolean value indicating whether the receiver is willing to relinquish first-responder status.
是否能够将要放弃作为第一响应者的状态。默认为 YES。
As an example, a text field in the middle of editing might want to implement this method to return NO to keep itself active during editing.
例如,编辑中的文本输入框可能想实现这个方法返回 NO 来保持自己的编辑状态(不过,这种情况目前还没有遇到过。)。
Notifies the receiver that it has been asked to relinquish its status as first responder in its window.
通知接受者它被询问是否放弃在 window 上作为第一响应者的状态。 默认为 YES。 注意:子类重载该方法的时候,必须实现父类的方法。
点击某个 view 出现 copy 等菜单的 UIMenuController 时,我们会重载 canBecomeFirstResponder 方法并返回 YES;
A major role of your app’s application object is to handle the initial routing of incoming user events. It dispatches action messages forwarded to it by control objects (instances of the UIControl class) to appropriate target objects.
application 主要的职责是处理用户事件。
sendEvent:
分发一个消息给合适的响应者对象。你可以子类 UIApplication 对象并且重载这个方法来拦截事件。但是拦截完后记得调用父类的实现 [super sendEvent:event]。
sendAction:to:from:forEvent:
转发消息给特定的对象。
target:接受消息的对象,如果为 nil,那么 APP 会发送给第一响应者,然后沿着响应链传递。
sender:发送 action 消息的对象。默认的 sender 是 UIControl 对象。
The UIControl class implements common behavior for visual elements that convey a specific action or intention in response to user interactions.
UIControl 为响应用户的交互而对那些可见的元素实现了共同的行为,其实也是事件的高级处理。它使用来 Target-Action 机制向 APP 报告用户的交互。
UIControl 由 UIControlState 类型的属性 state 决定它的外观和支持用户交互的能力。
The control handles all the work of tracking incoming touch events and determining when to call your methods.
处理所有的跟踪将要来的触摸事件的工作,并且决定什么时候调用你的方法。通过 addTarget:action:forControlEvents: 方法添加 target 和 action ,target 可以为任何对象,一般是包含 control 的 view controller,如果 target 为 nil,那么控件会通过响应链查找定义了该方法的响应者。
The control maintains a list of its attached targets and actions along and the events each supports.
里面维持了一个数组来存储它的 target、action 已经所支持的事件。control 不会 retain target。可以参考 iOS-Runtime-Headers _targetActions数组
用 Xcode 在 UIControl 的响应事件里面断点可以看到 _targetActions 数组
sendActionsForControlEvents:
This method iterates over the control’s registered targets and action methods and calls the sendAction:to:forEvent: method for each one that is associated with an event in the controlEvents parameter.
从 iterates
可以看出 UIControl 里面是维持了一个数组。
响应方法有三种形式
- (IBAction)doSomething; - (IBAction)doSomething:(id)sender; - (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event; // sender就是调用这个方法的对象,一般就是control自己;而event就是触发这个control的相关事件
根据 UIControlEvents 来指定用户交互的特定形式,例如:UIButton 就是 UIControlEventTouchDown 或者 UIControlEventTouchUpInside 触发 action 方法,而 UISlider,则是 UIControlEventValueChanged。
When a control-specific event occurs, the control calls any associated action methods right away. Action methods are dispatched through the current UIApplication object, which finds an appropriate object to handle the message, following the responder chain if needed. For more information about responders and the responder chain, see Event Handling Guide for iOS.
当一个特定的事件发生时,control 就正确的调用相关的 action 方法。通过 UIApplication 对象(它能够找到相应的对象来处理消息)来分发 action 方法,如果需要的话,则通过响应链来找到。
子类化 UIControl 使你能够简单支持事件处理。用下面两种方法中的一种来改变它的行为。
sendAction:to:forEvent:
调用一个特定的方法。这个方法带着提供的信息并且将它转发给单例 UIApplication 去分发。
beginTrackingWithTouch:withEvent:
当 touch 事件发生在控件里面时会调用这个方法。
Target-action is a design pattern in which an object holds the information necessary to send a message to another object when an event occurs.
Target-action
是一种设计模式,当某个事件发生时,持有信息的对象发送消息给另外一个对象。持有的信息包括接受消息的对象以及消息。
上面图片所表示的可以用下面的代码表示:
// viewcontroller - (void)viewDidLoad { [super viewDidLoad]; UIControl *control = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; [control addTarget:self action:@selector(restoreDefaults:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:control]; } - (void)restoreDefaults:(id)sender { }
即 UIControlEventTouchUpInside 类型的事件发生时,事件会传递到 control 对象,然后由 control 去触发 target 的 action 行为。UIGestureRecognizer 也是类似的。
上面有提到 Gesture recognizers convert low-level event handling code into higher-level actions.
UIGestureRecognizer is an abstract base class for concrete gesture-recognizer classes.
A gesture recognizer doesn’t participate in the view’s responder chain.
尽管它是添加在 view 上的,但是它不参与 view 的响应链。
那来看看 Gesture Recognizers 是怎么个高级法? 当它添加到 view 上时,它能够让 view 像 control 一样响应特定的事件。
系统已经帮我们内置几个非常实用的手势:
每个 gesture recognizer 都跟一个 view 相关联,所以它得添加到 view 上。一个 view 可以有多个 gesture recognizer,通过 gestureRecognizers
属性来获取。
When a user touches that view, the gesture recognizer receives a message that a touch occurred before the view object does. As a result, the gesture recognizer can respond to touches on behalf of the view.
当用户触摸 view 的时候,gesture recognizer 会在 view(靠 touchBegan、moved、ended、cancelled:withEvent:
这几个方法来处理 touch 事件)之前收到这个 touch 事件。所以 gesture recognizer 能够代表 view 来响应这个 touch。
gesture recognizer 分为离散的和连续的。从下图可以看出,离散的只会发送一次 action message
给 target ,而连续的则会发送多次直到这个触摸队列完成。
使用手势的方法也很简单:
- (void)viewDidLoad { [super viewDidLoad]; // Create and initialize a tap gesture UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondToTapGesture:)]; // Specify that the gesture must be a single tap tapRecognizer.numberOfTapsRequired = 1; // Add the tap gesture recognizer to the view [self.view addGestureRecognizer:tapRecognizer]; } // Respond to a rotation gesture 离散的手势需要在响应方法里面判断它的状态 - (IBAction)showGestureForRotationRecognizer:(UIRotationGestureRecognizer *)recognizer { if (([recognizer state] == UIGestureRecognizerStateEnded) || ([recognizer state] == UIGestureRecognizerStateCancelled)) { [UIView animateWithDuration:0.5 animations:^{ self.imageView.alpha = 0.0; self.imageView.transform = CGAffineTransformIdentity; }]; } }
gesture recognizer 可以从一个状态变换到另外一个状态。
根据某种特定的条件,状态是会变的。离散手势和连续手势的机制不同。
离散手势直接从 Possible -> Failed or Recognized。注意,Ended 是 Recognized 的别名,其实这两个状态都代表手势结束了。
只要 gesture recognizer 改变它的状态,它就会给 target 发送 action message ,除非它的状态变为 Failed 或者 Canceled 。所以,离散的手势就会当状态从 Possible -> Recognized 的时候发送一次 action message 。而 连续手势会发送多次。当 gesture recognizer 到达 Recognized 状态的时候,它会 reset 重置到 Possible 状态(到这个状态不会发送 action message )。(在后面提到的,自定义手势的时候,置为 Recognized 的时候也要手动 reset 重置,将该 gesture recognizer 的一些属性啥的都置为初始状态。)注意:变为 Failed 或者 Canceled 是不会发送消息的。
一个 view 可以有多个 gesture recognizer 。通过 gestureRecognizers 属性可以知道该它有多少个 gesture recognizer 。当然也可以通过 addGestureRecognizer: 和 removeGestureRecognizer: 方法添加、移除某个 gesture recognizer。那么问题来了,如果有多个 gesture recognizers 的话,怎么处理这些事件,它们之间会存在着竞争关系。
When a view has multiple gesture recognizers attached to it, you may want to alter how the competing gesture recognizers receive and analyze touch events. By default, there is no set order for which gesture recognizers receive a touch first, and for this reason touches can be passed to gesture recognizers in a different order each time.
如果一个 view 上有多个 gesture recognizer 时,你可能想改变这些竞争手势如何接受和处理触摸事件的。默认情况下,这些手势谁第一个接受到 touch 是无序的,所以手势可能每次都发生在不同的顺序。所以,我们可以用 delegate 和子类化来处理改变这些行为。
通过下面的相关代理指定某个手势识别的时候另外一个手势识别失败。
手势的处理使用是 analyze
分析到 handle
处理。这个比上面更吊,根本就不让分析触摸事件。通过下面两个代理方法来完成。
是两个手势同时发生。按道理两个手势是不能同时响应的,但是有的时候你希望 pinch 和 rotate 手势同时发生,可以使用下面的代理方法。
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
Note:You need to implement a delegate and return YES on only one of your gesture recognizers to allow simultaneous recognition. However, that also means that returning NO doesn’t necessarily prevent simultaneous recognition because the other gesture recognizer's delegate could return YES.
注意:实现一个代理,让它返回 YES,就能够允许同时发生。当然它也意味着返回 NO 不能防止它不同时发生,因为很有可能其他的代理返回 YES。
子类重载 canPreventGestureRecognizer:
或者 canBePreventedByGestureRecognizer:
方法返回 NO 来处理。默认返回 YES。
例如,rotation 能够防止 pinch,而 pinch 不能防止 rotation,就可以用下面的代理来处理。
[rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];
或者重载 rotation 手势的方法返回 NO。
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior.
iOS6 以后,默认的 control actions 控制会阻止覆盖手势行为。例如,按钮的事件就是一个 tap 手势。如果你在按钮的父视图上添加了一个手势,那么当用户点击按钮的时候,按钮会收到响应触摸事件,它的父视图不会响应。当然这仅仅作用于默认有 control action
的手势识别上,它还包括:
当你想要重载 control 默认的 action,在 control 上面添加手势时,手势第一次收到触摸事件。但是你得注意了,需要去看看 iOS Human Interface Guidelines
以确保能够为用户提供直觉的体验(这样做是不推荐的,记得看 view programming
的时候看到不推荐在 UIButton 上面手势等之类的事件)。
那么手势是怎样处理一个 view 的触摸事件呢?我们先了解 touches 和 events 的术语。
UIEvent 和 UITouch 在前面已经讲了。这里的 Multitouch Sequence
触摸队列是指从手指触摸屏幕开始到最后手指离开屏幕结束。还有需要注意的是,手指的精确性比鼠标要低。并且它的触摸区域是椭圆形的,比用户期望的要差。它还受手指的大小、方向、压力、哪根手指以及其他因素的影响。
前面讲 UIResponder 的时候,已经提到,在触摸队列中,当某个 touch phase 有新的或改变的 touch 发生时,APP 就会通过下面的方法发送消息:
- (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;
每个方法都对应一个 touch phase。例如,touchesBegan:withEvent: 对应着 UITouchPhaseBegan。注意:这些方法跟 gesture recognizer 状态(例如:UIGestureRecognizerStateBegan)没有关系。
你有多少次想 view 在 gesture recognizer 之前收到 touch 。
view 的 touch 分发是从 UIApplication -> UIWindow 。然后 window 在发送 touch 到 view 之前,会先发给 view (或者它 superView 上)的 gesture recognizers。
A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first.
window 会延迟传递 touch 对象到 view,所以手势能够最先识别这个 touch。如果手势识别了这个触摸事件,那么 window 不会分发剩下到 view 上,并且还会取消之前发送出去的触摸事件。
例如,有一个离散的手势需要两根手指,所以有两个 touch 对象,传递流程如下图所示。
假如最后一步,gesture recognizer 还没有识别到,那么它的状态就变成 failed,然后 window 就会将那两个 touch 对象通过 touchesEnded:withEvent: 消息传递给它所附属的 view。
连续的 gesture recognizer 跟上面的行为差不多,除了它可能在 Ended phase 之前就已经识别到那个手势了。一旦识别到手势,它就会变成 Began 状态。window 就会把触摸队列中剩下的 touch 对象都发送给这个 gesture recognizer,而不是它所附属的 view。
所以我们可以改变一些 gesture recognizer 的属性来改变默认的事件分发路径。可以参考上面离散两个手指的例子来理解下面的属性。
cancelsTouchesInView:默认为 YES。当 gesture recognizer 识别到手势后,window 不会分发它们给 view。并且 window 会通过 touchesCancelled:withEvent: 方法来取消之前传递的 touch。如果 gesture recognizer 没有识别到,那么 view 会收到触摸队列里面所有的 touch。
delaysTouchesBegan: 默认为 NO。正常情况下,window 发送 began 和 moved 状态的 touch 给 view 和 gesture recognizer。将它置为 YES,防止 window 发送 Began phase 给这个 view。这能够保证当 gesture recognizer 识别到 touch 时,没有任何 touch 事件分发到这个它附属的 view 上。小心设置这个属性,因为它会使你界面感觉没有响应。这个属性跟 UIScrollView 的 delaysContentTouches 属性类似。当手指触摸滚动开始后,scrollview 的所有 subview 不会接受 touch 事件,所以没有闪光的视觉反馈。
delaysTouchesEnded:默认为 YES。当它为 YES 时,它能保证 view 不会马上完成一个动作,因为 gesture recognizer 后面可能会想要取消。当 gesture recognizer 正在分析一个 touch 事件时,window 不会分发 Ended phase 状态的 touch 给所它附属的view。如果 gesture recognizer 识别到了,那么 touch 将会被取消(即不会分发给 view)。如果 gesture recognizer 识别不到,那么 window 会通过 touchesEnded:withEvent: 消息分发这些 touch 对象给 view。设置它为 NO 时,允许 view 和 gesture recognizer 同时在 Ended phase 分析这些 touch 对象。 例如,当一个 view 有一个双击手势时。delaysTouchesEnded 为 YES,这个 view 收到 touchesBegan:withEvent:, touchesBegan:withEvent:, touchesCancelled:withEvent:, and touchesCancelled:withEvent: 的消息。当它为 NO 时,它会收到 touchesBegan:withEvent:, touchesEnded:withEvent:, touchesBegan:withEvent:, and touchesCancelled:withEvent:,这就意味着在 touchesBegan:withEvent: 时,就能识别它是 double tap(实际情况,应该是在 End 才识别到,Apple 自定义模拟处理单击手势时就是在 touchesEnded:withEvent: 方法里面处理的)。
如果 gesture recognizer 检测到一个 touch 不属于它,它能够直接传递给它的 view。通过 gesture recognizer 调用 ignoreTouch:forEvent: 方法,将这个 touch 传递。(问题:难道如果 gesture recognizer 处理不了,window 就不会将它传递给它所附加的 view 吗?view 是能够接受到 touch 的,gesture recognizer 只是一个高级的封装,所以 window 会传下去。)
自定义手势需要创建 UIGestureRecognizer 的子类。需要引入
#import <UIKit/UIGestureRecognizerSubclass.h>
通过实现下面的方法。
- (void)reset; - (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 的那个四个方法签名一样。你重载这些方法的时候,必须调用父类的实现,即使它是个空的实现。重要的是在这些方法里面改变 status 属性的值,并且在 reset 方法里面将一些值置为初始值(因为 gesture recognizer 转成 Recognized/Ended, Canceled, or Failed 时,reset 方法在变成 Possible 状态前会被调用)。
官方文档 Listing 1-8 Implementation of a checkmark gesture recognizer 有实现一个自定义的手势,这里就不贴代码了。还可以看看 YYGestureRecognizer 。
我们想动态的响应事件。例如:一个 touch 可以出现在屏幕上的不同对象上,你必须去决定你想要哪个对象去响应这个事件,理解这些对象怎样接受这个事件。
当用户的触摸事件发生时,UIKit 创建一个事件对象,它包含了需要处理这个事件的一些信息。然后它将这个事件对象放在 App 的 event queue 里面(这里就跟 runloop 有关系了)。对于 touch 触摸事件,这个事件对象就是在 UIEvent 对象。对于 motion 事件,这个事件对象就取决于你使用的是哪个 framework 和哪种你感兴趣的 motion 事件了。
一个事件会以一条特定的路线去传递,直到它找到可以处理它某个对象为止。首先,UIApplication 从系统队列的顶层取出一个事件并分发它。通常,这个事件会被发送给 key window,key window 会将它发送给 initial object 去处理。initial object 取决于事件类型。
这些事件路径的最终目标就是找到一个能够处理响应这个事件的对象。所以,UIKit 第一次会将它发送给最适合来处理它的对象。就是上面提到的 hit test view 或者 first responder。
iOS 用 hit-testing 去寻找 touch 下面的那个 view。hit-testing 会检测 touch 是否在相关 view 的 bounds (这里是 bounds,而不是 frame)里面。如果是,会循环检查这个 view 的所有 subviews。view 层级最低(也就是是最上面那个 subview)的包含这个 touch point 的就是 hit-test view。然后 iOS 会将这个 touch 事件交给这个 view 去处理。这里有张 hit-testing 的经典图。它会从 window 开始从下往上开始寻找。
hitTest:withEvent: 方法根据给定的入参 CGPoint 和 UIEvent 返回 hit test view。在调用 hitTest:withEvent: 方法之前会先调用 pointInside:withEvent: 方法。如果传到 hitTest:withEvent: 的 point 在这个 view 的 bounds 里面,那么 pointInside:withEvent: 返回 YES。然后,会循环的在每个返回 YES 的 subview 上调用 hitTest:withEvent: 方法。
如果传进去的 point 不在 view 的 bounds里面,那么第一次调用 pointInside:withEvent: 会返回 NO,这个 point 会被忽略掉,hitTest:withEvent: 返回 nil。如果某个 subview 返回 NO,那么它这个 view 的整个层级都是被忽略掉的,因为既然它都不会出现在 subview 上,那么自然不会出现在 subview 的 subview 上面嘛。这就意味在一个 subview 上的任何点,它如果在 superview 之外,是接受不到触摸事件的。当 subview clipsToBounds 属性为 NO (允许 subview 超过 superview 的边界)时,这个事情会发生。
Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.
注意,一个 touch 对象在 hit-test view 的生命周期内都跟它关联的,即使这个 touch 后面移动到它外面。
hit-test view 给了第一次去处理这个触摸事件的机会。如果 hit-test view 处理不了,那么它会沿着响应链向 application 的方向去寻找可以处理它的对象。
许多类型的事件都依赖响应链去分发。响应链是由一系列相连接的响应对象组成(在 view 的层级确定后,响应链就连接完成了)。它由 first responder 开始,application object 结束(它的传递顺序是从上至下的)。如果 frist responder 不能处理这个事件,那么会在响应链里将这个事件向前转发。一个响应对象它能够响应处理事件,UIResponder 就是它的基类(上面有提到过)。像 UIApplication, UIViewController, 和 UIView 都是 responders,但是注意,Core Animation layers 不是。
first responder 指定首先收到事件。通常,first responder 是一个 view。关于 UIResponder,可以看上面的相应内容。
Events不是依赖响应链的唯一对象。响应链还用于以下:
如果 initial object(hit-test view 或者 the first responder,它通常是个 view)处理不了这个事件,UIKit 会在响应链中将它传递给 next responder。每个响应对象决定它是否处理这个事件,还是通过调用 nextResponder
传递下去。
这个过程持续到 app object,如果 app object 都处理不了,那么就丢弃掉这个事件。
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.
重要:不要直接调用 nextResponder 方法,而应该调用父类当前事件处理的实现,让 UIKit 来处理。
通常情况下,我们能够用 UIKit 里面标准的 control 和 gesture recognizers 来处理几乎所有的触摸事件了。当然有些情况,我们需要自定义,也就是上面所提及的高层和低级的问题。
首先就是要创建 UIResponder 的子类,也可以是 UIView、UIViewController、UIControl、UIApplication、UIWindow 的子类,不过子类化 UIApplication、UIWindow 比较罕见,一般都是继承 UIView、UIControl。
然后还有3件事需要处理
在触摸队列中,App 发送一系列的事件消息给目标响应者。为了接受处理这些消息,这个响应者对象必须实现下面的 UIResponder 的事件处理方法
- (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;
每个方法都对应一个 touch 对象的状态,UITouchPhaseBegan,UITouchPhaseMoved,UITouchPhaseEnded,UITouchPhaseCancelled。当有某个状态有新的或者改变的 touch 对象时,App 就会调用上面的相关方法。
入参说明:
处理触摸事件的所有 view 都希望能够收到完整的事件流,所以在创建你的子类时,要注意
当然如果事件的某个状态没有接受处理,这会导致后果可能不定义的或者不良的。如果在事件处理的时候创建了恒久的对象,那么在 touchesCancelled:withEvent: 方法里,记得销毁这些对象,即让它们回到原始状态。当有电话打进来时,app 就会调用 touchesCancelled:withEvent: 方法。然后在 touchesEnded:withEvent: 方法里面也要销毁这些东西,回到原始状态,强调一个有始有终。
用自定义触摸事件来实现相关 UIGestureRecognizer。这一小节的详细代码可见 Multitouch Events Listing 3-1
至 Listing 3-7
。
用 UITouch 的 tapCount 属性来判断是单击还是双击还是三击。最好是在 touchesEnded:withEvent: 方法里面做判断处理,因为它是用户手指离开 App 时才响应的,要确保它真的是个 tap 手势,而不是拖动啥的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *aTouch in touches) { if (aTouch.tapCount >= 2) { // The view responds to the tap [self respondToDoubleTapGesture:aTouch]; } } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { }
从三个角度判断它是否是一个滑动手势
具体代码如下:
#define HORIZ_SWIPE_DRAG_MIN 12 #define VERT_SWIPE_DRAG_MAX 4 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject]; // startTouchPosition is a property self.startTouchPosition = [aTouch locationInView:self]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject]; CGPoint currentTouchPosition = [aTouch locationInView:self]; // Check if direction of touch is horizontal and long enough if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN && fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX) { // If touch appears to be a swipe if (self.startTouchPosition.x < currentTouchPosition.x) { [self myProcessRightSwipe:touches withEvent:event]; } else { [self myProcessLeftSwipe:touches withEvent:event]; } self.startTouchPosition = CGPointZero; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { self.startTouchPosition = CGPointZero; }
简单的一根手指拖动的相关代码。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject]; CGPoint loc = [aTouch locationInView:self]; CGPoint prevloc = [aTouch previousLocationInView:self]; CGRect myFrame = self.frame; float deltaX = loc.x - prevloc.x; float deltaY = loc.y - prevloc.y; myFrame.origin.x += deltaX; myFrame.origin.y += deltaY; [self setFrame:myFrame]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { }
taps,drags,swipes 通常都只涉及了一个 touch,比较简单去跟踪。但是处理由多个 touches 组成的触摸事件时,比较有挑战性。需要去记录 touch 的所有相关属性,并且改变它的 state 等等。需要做到两点:
Determining when the last touch in a multitouch sequence has ended,判断 multitouch sequence 里的最后一个 touch 是否结束,可以用下面的代码
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { if ([touches count] == [[event touchesForView:self] count]) { // Last finger has lifted } }
通过改变一些属性去改变事件流的处理。
Turn on delivery of multiple touches. 默认值为NO,意味着只会接受触摸队列里面的第一个 touch,其他的会忽略掉。所以, [touches anyObject]
方法就只会返回一个对象。将属性 multipleTouchEnabled 设为 YES,则可以处理多个 touches。
Restrict event delivery to a single view. 即只有一个 view 响应事件。默认情况下,view 的 exclusiveTouch 属性为 NO,这就意味着,一个 view 不会阻塞 window里的其他 view 去接受事件。如果将某个 view 的 exclusiveTouch 设为 YES,那么当它接受 touches 时,只会有它一个接收 touches 。这里举例说明了 exclusiveTouch 属性,A、B、C 3个 view 多点触摸的例子。
If the user touches inside A, it recognizes the touch. But if a user holds one finger inside view B and also touches inside view A, then view A does not receive the touch because it was not the only view tracking touches. Similarly, if a user holds one finger inside view A and also touches inside view B, then view B does not receive the touch because view A is the only view tracking touches. At any time, the user can still touch both B and C, and those views can track their touches simultaneously.
exclusiveTouch 这个属性比较傲娇,只有当设置它的为 YES 的 view 首先收到触摸事件时,它才能响应。
Restrict event delivery to subviews. 重载 hitTest:withEvent: 方法返回自己 self。
beginIgnoringInteractionEvents 方法关闭,endIgnoringInteractionEvents 方法开启。这个方法是 UIApplication 的,所以能做一些全局性的事情。
你可以将一个事件转发给另外一个响应对象(响应链就是这样玩的嘛),当你使用这个技术的时候得小心,因为 UIKit 没有设计去接受不属于它们的事件。所以,你不要转发给 UIKit 框架的对象。如果你想要有条件的去转发事件给其他响应对象时,那么这些对象应该是 UIView 的实例,并且这些对象关心事件的转发,并且能够处理这些事件。原因如下:
For a responder object to handle a touch, the touch’s view property must hold a reference to the responder.
一个 responder 对象想要处理一个 touch ,那么 touch 的 view 属性必须持有这个 responder。
事件的转发经常需要去分析 touch 对象觉得它是否应该转发事件。这里有一些方法你可以采取去分析:
重载 sendEvent: 方法可以监听 App 事件的接收。UIApplication 和 UIWindow 都是用这个方法来分发事件的,所以它就是事件进入 App 的管道一样。当你重载的时候,务必调用父类的实现,[super sendEvent:event]。在 control 和 gesture recognizer 的响应事件里面打断点,可以看到,事件走的 UIKit 开始传递都是先走的,[UIApplication sendEvent:]、[UIWindow sendEvent:],最终都是走的 [UIApplication sendAction:to:from:forEvent:]。
当处理 touch 和 motion 事件时,这里有一些值得推荐的技巧和模式:
事件传递的最终目标是找到一个能够处理响应这个事件的对象(UIResponder 的子类)。如果找不到就丢弃它。
能够处理事件的对象需要完成下面3个条件:
实现这四个方法
- (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;
可以交互的。即 userInteractionEnabled 属性为 YES。
换汤不要药,跟前面的前提条件一样,只不过是另外一种形式来完成而已。
通过实现跟 UIResponder 相同签名的方法来完成。参考例子,上面有提到,官方文档 Listing 1-8 Implementation of a checkmark gesture recognizer 和 YYGestureRecognizer 。
- (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 相同功能的方法来完成,里面通过一个 _targetActions 数组来存储各种 UIControlEvents 状态的事件。可以参考 Chameleon UIControl 和 SVSegmentedControl 。
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event; - (void)cancelTrackingWithEvent:(UIEvent *)event;
手指触摸屏幕就会生成 UIEvent 对象,然后放在 application 的队列里面,application 会从系统队列的顶层取出一个事件并分发它。application(sendEvent:) -> window(sendEvent:) -> initial object(hit-test view or frist responder)。
而 application 和 window 则是通过 Hit-Testing 和响应链来找到 initial object。一般情况下,都不需要我们去干涉 UIKit 的这个分发过程。但是,我们可以在这个过程去干涉达到自己的需求。
这个章节的相关代码参考自
我们绘制 UIButton 的时候,想要扩大它的响应区域。我们可以在 UIButton 里面处理 Hit-Testing 的那两个方法其中一个里面做处理。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGRect touchRect = CGRectInset(self.bounds, -10, -10); if (CGRectContainsPoint(touchRect, point)) { return self; } return [super hitTest:point withEvent:event]; }
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event { return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point); } CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) { CGRect hitTestingBounds = bounds; if (minimumHitTestWidth > bounds.size.width) { hitTestingBounds.size.width = minimumHitTestWidth; hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2; } if (minimumHitTestHeight > bounds.size.height) { hitTestingBounds.size.height = minimumHitTestHeight; hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2; } return hitTestingBounds; }
这个在 限制事件分发到 subviews 上
小节里面就有说过。重载 hitTest:withEvent: 方法返回自己 self。
评论使用的是 disqus,FQ 就可以用了。