一. Hit-Testing
-
什么是Hit-Testing?
- 对于触摸事件, window首先会尝试将事件交给事件触发点所在的View来处理, 也就是
hit-test view
- 而寻找
hit-test view
的这个过程, 被称之为hit-testing
-
Hit-Testing Returns the View Where a Touch Occurred
, 这个过程会返回点击事件所在的View
- 对于触摸事件, window首先会尝试将事件交给事件触发点所在的View来处理, 也就是
-
Hit-Testing的过程
- 系统首先找到触摸点所在位置的View, 然后找到这个View的
最高级父控件
- 从最高级的父控件开始进行寻找, 首先会调用
hit-test
方法 - 在
hit-test
内部调用pointInside方法
, 来判断触摸点point
是否在当前的View中- 如果在, 则返回YES, 则进入这个View中, 继续遍历他的子控件, 执行
hit-test
方法 - 如果不在, 则返回NO, 离开这个View的
hit-test
检查, 执行同级另一个View的hit-test
继续查找
- 如果在, 则返回YES, 则进入这个View中, 继续遍历他的子控件, 执行
- 遍历到在View等级系统中, 处于最低级的View, 他没有子控件, 因此这个就是最合适的
hit-test view
, 点击事件就会交给他来处理 - 递归: 进入hit-test -> 调用pointInside -> (如果返回YES, 则进入子控件的hit-test -> 回到第一步) -> 离开hit-test
- 系统首先找到触摸点所在位置的View, 然后找到这个View的
-
Hit-tset的一般作用
阻止某个View的响应(应用的较少), 让
pointInside:
方法始终返回NO, 即可阻止这个View的响应-
扩大按钮的响应区域(如果按钮过小, 但是你想扩大他的响应范围)
- (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; // 修改响应的宽度 // 响应的X值 -= (响应的宽度 - 原宽度) / 2 (X) 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; }
-
子控件超出父控件后无法响应(TabBar按钮超出父控件范围)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) { return nil; } /** * 此注释掉的方法用来判断点击是否在父View Bounds内, * 如果不在父view内,就会直接不会去其子View中寻找HitTestView,return 返回 */ // 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]; // 寻找该控件中的hit-test view if (hitTestView) { return hitTestView; } } return self; // } return nil; }
二. 响应者链条由响应者组成(The Responder Chain Is Made Up of Responder Objects)
hit-test view是最优先处理事件的View, 如果hit-test view不能处理事件的话, 那么事件会沿着响应者链条找到能够处理这个事件的View.
-
响应者链条的事件传递:
- 响应者链条是由一系列的响应者对象构成的, 很多事件都是依靠响应者链条来实现事件传递
- 传递从first responder开始, 到Application结束
- 如果first responder不能处理这个事件, 那么系统会顺着响应者链条向前寻找下一个响应者
-
响应者对象(Responder object):
- 响应者对象能够响应和处理事件,
UIResponder
类是所有响应者对象的基类, 他定义了事件处理以及一般响应行为 - 凡是继承自UIResponder, 例如UIView,UIApplication,UIViewController等, 都可以响应事件.
- 注意: CoreAnimation Layers不是响应者
- 响应者对象能够响应和处理事件,
-
第一响应者(First Responder):
- First Responder用于第一个来接收事件.
- 一般First Responder都是一个View对象, 一个对象变为First Responder需要通过一下两种情况:
- 重写
canBecomeFirstResponder
方法, 并且返回YES - 接收
becomeFirstResponder
消息, 响应者对象可以给自己发送这个消息
- 重写
-
要在View完全渲染结束的时候, 才能指定其成为First Responder:
- 例如: 如果在
ViewWillAppear
方法中调用becomeFirstResponder
方法, 那么这个方法总是会返回NO - 因此, 应该在
viewDidAppear
方法中, 指定称为First Responder
- 例如: 如果在
-
除了Event事件, 还有其他几种情况需要使用到响应者链:
- 触摸事件(TouchEvent): 如果
hitTest view
无法处理一个触摸时间, 那么这个事件就会放弃这个hit-test view的响应者链 - 运动事件(MotionEvent): 如果要处理
shake-motion
摇动事件, 那么当前First responder对象必须要实现motionBegan:withEvent:
或motionEnded:withEvent:
两个方法之一 - 远程控制事件(RemoteControlEvent): 处理这类时间, 当前的First responder必须实现
remoteControlReceivedWithEvent
方法 - 行动消息(ActionMessage): 当用户点击一个UIButton或UISwitch等控件的时候, 如果消息的发出者
target
为nil的话, 那么这条消息会由First Responder的响应链的开始发出 - 编辑菜单消息(文本的赋值粘贴)(Editing-menu messages): 当点击编辑菜单的命令时, iOS会通过响应者链条寻找实现了
cut: copy paste:
方法的对象 - 文本编辑(TextEditing): 当用户使用
UITextField或UITextView
时, 这个View会自动的变为First Responder, 通常情况下, 键盘会自动的弹出并且TextView会进入编辑状态. 你可以展示一个自定义的输入View来替代键盘, 同时可以给任意一个响应者对象添加一个自定义的输入View.
- 触摸事件(TouchEvent): 如果
只有TextView和TextField才会自动变成First Responder, 其他的响应者对象, 需要主动调用
becomeFirstResponder
方法来称为第一响应者
The Responder Chain Is Made Up of Responder Objects(响应者链条是由响应者对象构成的)
Many types of events rely on(依靠) a responder chain for event delivery(事件传递). The responder chain is a series of linked responder objects(一系列相连的响应者对象). It starts with the first responder and ends with the application object(从第一响应者开始, 到Application结束). If the first responder cannot handle an event, it forwards the event to the next responder in the responder chain.(如果第一响应者无法处理, 则顺着链条向前找下一个响应者)
A responder object is an object that can respond to and handle events(响应者对象能响应和处理时间). The UIResponder class is the base class for all responder objects, and it defines the programmatic interface not only for event handling but also for common responder behavior(UIResponder类定义了事件处理以及一般响应行为). Instances of the UIApplication, UIViewController, and UIView classes are responders, which means that all views and most key controller objects are responders. Note that Core Animation layers are not responders(CALayer不是响应者).
The first responder is designated to receive events first(First responder用于第一个接收事件). Typically, the first responder is a view object. An object becomes the first responder by doing two things(一个对象变成First responder通过两件事):
- Overriding the canBecomeFirstResponder method to return YES重写canBecomeFirstResponder方法, 并返回YES.
- Receiving a becomeFirstResponder message. If necessary, an object can send itself this message接收becomeFirstResponder消息, 一个对象可以给自己发这个消息.
Note: Make sure that your app has established its object graph(建立他的对象图表) before assigning an object to be the first responder(在指定一个对象变为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如果在你的View没有渲染完毕时让他成为第一响应者, 这时候这个方法始终会返回NO.
Events are not the only objects that rely on the responder chain(事件不只是唯一的依靠于响应者链条的). The responder chain is used in all of the following:
Touch events(触摸事件). If the hit-test view cannot handle a touch event, the event is passed up错过 a chain of responders响应者链条 that starts with the hit-test view从hit-test view开始的.
Motion events(运动事件). To handle shake-motion(摇动事件) events with UIKit, the first responder must implement either the motionBegan:withEvent: or motionEnded:withEvent: method of the UIResponder class, as described in Detecting Shake-Motion Events with UIEvent第一响应者需要实现
motionBegan
或motionEnded
两个方法之一.Remote control events(远程控制事件). To handle remote control events, the first responder must implement the remoteControlReceivedWithEvent: method of the UIResponder class.远程控制事件, 必须实现
remoteControlReceivedWithEvent
方法Action messages(行动消息(按钮)). When the user manipulates(操控) a control, such as a button or switch, and the target(目标为nil) for the action method is nil, the message is sent through a chain of responders starting with the first responder, which can be the control view itself(当action message被处罚, 并且没有方法对应的target的时候, 这个message由first responder的响应者链条的开始发出).
Editing-menu messages(编辑菜单消息(文本的赋值粘贴)). When a user taps the commands of the editing menu, iOS uses a responder chain to find an object that implements the necessary methods (such as cut:, copy:, and paste:)(当点击编辑菜单的命令时, iOS会通过响应者链条寻找实现了
cut: copy paste:
方法的对象). For more information, see Displaying and Managing the Edit Menu and the sample code project, CopyPasteTile.Text editing(文本编辑). When a user taps a text field or a text view(TextField和TextView), that view automatically becomes the first responder(这个View会自动的变成first responder). By default, the virtual keyboard appears and the text field or text view becomes the focus of editing(键盘弹出, textView进入编辑). You can display a custom input view instead of the keyboard(你可以显示一个自定义的输入View, 而不是键盘) if it’s appropriate for your app. You can also add a custom input view to any responder object(你可以给任意一个响应者对象添加input view). For more information, see Custom Views for Data Input.
UIKit automatically sets the text field or text view that a user taps to be the first responder(UIKit 自动的将textView和textField设置为first responder当开始编辑的时候); Apps must explicitly set all other first responder objects with the becomeFirstResponder method(其他的响应者对象, 必须手动调用becomeFirstResponder
方法).
三. 响应者链的传递方式(The Responder Chain Follows a Specific Delivery Path)
- 响应者链的传递方式:
- 首先, 当触发一个事件的时候, 被点击的View是初始View, 他会先从这个view开始寻找事件处理者
- 如果这个View无法处理这个事件, 那么会顺着他的父级(响应者链)继续寻找下一个响应者
- 如果有响应者能够处理这个事件, 或没有找到能够处理这个事件的响应者, 则传递结束
- 传递的顺序: view -> ViewController -> window -> Application -> 丢弃
If the initial object(初始对象)—either the hit-test view or the first responder—doesn’t handle an event, UIKit passes the event to the next responder in the chain(如果hit-test view和first responder都无法处理事件, 那么UIKit会沿着响应者连寻找下一个响应者). Each responder decides whether it wants to handle the event or pass it along to its own next responder by calling the nextResponder method(每个响应者都可以决定是否要去处理时事件或者通过调用nextResponder
方法, 交个下一个响应者处理).This process continues until a responder object either handles the event or there are no more responders(当有响应者处理这个事件, 或没有更多响应者的时候结束传递).
The responder chain sequence(链条) begins when iOS detects(检测到) an event and passes it to an initial object, which is typically a view. The initial view has the first opportunity to handle an event. Figure 2-2 shows two different event delivery paths for two app configurations. An app’s event delivery path depends on its specific construction(当前的构造), but all event delivery paths adhere to(依附) the same heuristics(探索法).
Figure 2-2 The responder chain on iOS
For the app on the left, the event follows this path:
The initial view attempts to handle the event or message. If it can’t handle the event, it passes the event to its superview, because the initial view is not the top most view in its view controller’s view hierarchy(如果接收到事件的初始View无法处理事件, 那么这个事件会交给他的SuperView, 因为他不是viewController等级中的最高级View).
The superview attempts to handle the event. If the superview can’t handle the event, it passes the event to its superview, because it is still not the top most view in the view hierarchy.
The topmost view in the view controller’s view hierarchy attempts to handle the event. If the topmost view can’t handle the event, it passes the event to its view controller.
The view controller attempts to handle the event, and if it can’t, passes the event to the window.
If the window object can’t handle the event, it passes the event to the singleton app object.
If the app object can’t handle the event, it discards(丢弃) the event.
view -> ViewController -> window -> Application -> 丢弃
The app on the right follows a slightly different path, but all event delivery paths follow these heuristics:
A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.
The topmost view passes the event to its view controller.
The view controller passes the event to its topmost view’s superview.
Steps 1-3 repeat until the event reaches the root view controller.The root view controller passes the event to the window object.
The window passes the event to the 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(不要手动直接调用nextResponder
方法, 将事件直接传递给下一个响应者). Instead, invoke the superclass implementation of the current event handling method and let UIKit handle the traversal of the responder chain for you(应该调用父类的时间处理方法的实现, 并且让UIKit通过响应者链来传送事件).
四. 总结
- 响应者链是由当前你所点击的view的父级结构 + window + Application组成
- 事件的响应分为两个阶段, hit-test和寻找响应者
- hit-test
- 当用户触摸屏幕的时候, 系统会从最高级, 也就是RootView开始调用
hitTest
和pointInside
方法, 来寻找最适合处理事件的View, 即Hit-test View
- 这个过程总是从View等级中最高的View开始, 不断的遍历Subview
- 可以用这个方法来手动选择负责响应事件的View
- 当用户触摸屏幕的时候, 系统会从最高级, 也就是RootView开始调用
- Fine Responder
- 当hit-test寻找到hit-test View之后, 会从这个view开始判断能否处理事件
- 如果不能, 系统会自动通过响应者链来寻找下一个响应者继续判断
- 如果找到了能够响应事件的响应者, 那么这个事件由这个响应者来处理
- 如果没有响应者能够接收直到Application也无法处理, 那么这个事件将会被丢弃
- hit-test