(1), Hit-Testing Returns the View Where a Touch Occurred(事件分发)
iOS uses hit-testing to find the view that is under a touch. Hit-testing involves checking whether a touch is within the bounds of any relevant view objects. If it is, it recursively checks all of that view’s subviews. The lowest view in the view hierarchy that contains the touch point becomes the hit-test view. After iOS determines the hit-test view, it passes the touch event to that view for handling.
To illustrate, suppose that the user touches view E in Figure 2-1. iOS finds the hit-test view by checking the subviews in this order:
-
1, The touch is within the bounds of view A, so it checks subviews B and C.
-
2, The touch is not within the bounds of view B, but it’s within the bounds of view C, so it checks subviews D and E.
-
3, The touch is not within the bounds of view D, but it’s within the bounds of view E.
View E is the lowest view in the view hierarchy that contains the touch, so it becomes the hit-test view.
The hitTest:withEvent:
method returns the hit test view for a given CGPoint
and UIEvent
. The hitTest:withEvent:
method begins by calling the pointInside:withEvent:
method on itself. If the point passed into hitTest:withEvent:
is inside the bounds of the view, pointInside:withEvent:
returns YES
. Then, the method recursively calls hitTest:withEvent:
on every subview that returns YES
.
If the point passed into hitTest:withEvent:
is not inside the bounds of the view, the first call to the pointInside:withEvent:
method returns NO
, the point is ignored, and hitTest:withEvent:
returns nil
. If a subview returns NO
, that whole branch of the view hierarchy is ignored, because if the touch did not occur in that subview, it also did not occur in any of that subview’s subviews. This means that any point in a subview that is outside of its superview can’t receive touch events because the touch point has to be within the bounds of the superview and the subview. This can occur if the subview’s clipsToBounds
property is set to NO
.
Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.
The hit-test view is given the first opportunity to handle a touch event. If the hit-test view cannot handle an event, the event travels up that view’s chain of responders as described in The Responder Chain Is Made Up of Responder Objects until the system finds an object that can handle it
第一响应者(First responder)指的是当前接受触摸的响应者对象(通常是一个UIView对象),即表示当前该对象正在与用户交互,它是响应者链的开端。整个响应者链和事件分发的使命都是找出第一响应者。
UIWindow对象以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。
iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。
UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。
hitTest:withEvent:方法的处理流程如下:
首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
1, 若返回NO,则hitTest:withEvent:返回nil;
2, 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直 到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
3, 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
4, 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
假如用户点击了View E,hit-testing的流程:
1、A是UIWindow的根视图,因此,UIWindwo对象会首相对A进行hit-test;
2、显然用户点击的范围是在A的范围内,因此,pointInside:withEvent:返回了YES,这时会继续检查A的子视图;
3、这时候会有两个分支,B和C:
点击的范围在C内,即C的pointInside:withEvent:返回YES;遍历C的子视图D,E.
4、这时候有D和E两个分支:
这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。
需要注意的几点:
1、如果最终hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;
point |
A point specified in the receiver’s local coordinate system (bounds). |
event |
The event that warranted a call to this method. If you are calling this method from outside your event-handling code, you may specify |
Return Value
The view object that is the farthest descendent the current view and contains point
. Returns nil
if the point lies completely outside the receiver’s view hierarchy.
Discussion
This method traverses the view hierarchy by calling the pointInside:withEvent:
method of each subview to determine which subview should receive a touch event. If pointInside:withEvent:
returns YES
, then the subview’s hierarchy is similarly traversed until the frontmost view containing the specified point is found. If a view does not contain the point, its branch of the view hierarchy is ignored. You rarely need to call this method yourself, but you might override it to hide touch events from subviews.
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01
. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.
clipsToBounds
property is set to NO
and the affected subview extends beyond the view’s bounds.
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. Each responder decides whether it wants to handle the event or pass it along to its own next responder by calling the nextResponder
method.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.
For the app on the left, the event follows this path:
-
1, 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.
-
2, 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.
-
3, 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.
-
4, The view controller attempts to handle the event, and if it can’t, passes the event to the window.
-
5, If the window object can’t handle the event, it passes the event to the singleton app object.
-
6, If the app object can’t handle the event, it discards the event.
The app on the right follows a slightly different path, but all event delivery paths follow these heuristics:
-
1, A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.
-
2, The topmost view passes the event to its view controller.
-
3, The view controller passes the event to its topmost view’s superview.
4, Steps 1-3 repeat until the event reaches the root view controller.
-
5, The root view controller passes the event to the window object.
-
6, The window passes the event to the app object.
id hitView = [ super hitTest :point withEvent :event]; // 此时 hitView是 已经检测出的 hit-view 了 , 是 self or subViews(hitted subView)
if (hitView == self ) {
return nil ; // 是 self 的时候 , 不做处理
} else {
return hitView; // 是 subView 的时候 , 由 subView 去处理
}
}