您的位置:首页 > 移动开发 > IOS开发

iOS触摸事件总结

2015-09-14 18:19 549 查看
前言:
不得不说一下,现在网上的资料鱼龙混杂,经常有些错误性或者说误导性的文章,有时候为了追求解决问题的效率,直接使用而没有去求证,因此导致一直持有一个错误的观念,这是非常致命的错误,以后要吸取教训,多看看官方的文档。

这篇文章主要是对官方的解释复习以及加深自己的理解:
ios的用户交互主要分2个部分讲
(1), 事件分发:如何确定当前点击的点由哪个view来处理?
(2), 事件响应:确定hit-view之后,如何处理事件?


(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 pointbecomes 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.





Figure 2-1 Hit-testing returns the subview that was touched

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 pointpassed 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 pointpassed into
hitTest:withEvent:
is not inside the bounds of the view, the first call to the
pointInside:withEvent:
method
returns
NO
, the pointis 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 pointin a subview that is outside of its superview can’t receive touch events because the touch pointhas 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

(1), 事件分发解释:

第一响应者(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:
点击的范围不再B内,因此B分支的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;
B的subViews也无需检查了,继续遍历A的子视图,

点击的范围在C内,即C的pointInside:withEvent:返回YES;遍历C的子视图D,E.

4、这时候有D和E两个分支:
点击的范围不再D内,因此D的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;
点击的范围在E内,即E的pointInside:withEvent:返回YES,由于E是叶子节点没有子视图(也可以理解成对E的子视图进行 hit-test时返回了nil),
因此,E的hitTest:withEvent:会将E返回,再往回回溯,就是C的hitTest:withEvent:返回E--->>A的hitTest:withEvent:返回E.
最终,此次touch的响应视图就是viewE,由viewE来作为第一响应者处理。

这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。
需要注意的几点:

1、如果最终hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;
2、hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=NO)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。
(1), hidden = YES;
(2), userInteractionEnabled = NO;
(3), alpha<0.01
(4), 子视图的区域超过父视图的bound区域(父视图的clipsToBounds=NO, 默认就是NO)
上面这4种情况下,hit-testting会返回nil(也就是忽略当前view),根据顺序继续遍历view,找到hit-view。

hitTest方法的定义:
- (UIView
*)hitTest:(CGPoint)
point
withEvent:(UIEvent *)
event


point
A pointspecified 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
nil
.


Return Value

The view object that is the farthest descendent the current view and contains
point
.
Returns
nil
if the pointlies 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 pointis 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 pointis in a transparent portion of that view’s content.
Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. This can occur if the current view’s
clipsToBounds
property
is set to
NO
and the affected subview extends beyond the view’s bounds.

(2),The Responder Chain Follows a Specific
Delivery Path (事件响应)

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.



Figure
2-2 The responder chain on iOS


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.

(2), 事件响应:

当确定了hit-view之后,第一响应者就是当前的hit-view,然后就会根据响应者链来处理触摸事件。
响应者链:
1,hit-view会尝试处理事件,若不能处理,则传递给它的父视图
2,若superView不能处理,则继续向上传递给父视图,若一直不能处理,直到传递给它的vc的view(vc最顶上view)为止.
3,若topmost view不能处理,则传递给它的vc
4,若vc不能处理,则传递给window
5,若window不能处理,则传递给app单例对象[UIApplication shareApplication]
6, 若app不能处理,丢弃这个事件

右边的那张响应者链图片与左边的有轻微的差别:
就是一个vc1的view加载了vc2.view,这时候,如果vc2.view不能响应就由vc2响应,若vc2不响应就由vc1.view来响应


情景应用

问题1:如果父视图userInteractionEnable是NO,这时候子视图能接收touch事件吗?
分析:不能
因为在hit-testing的时候父视图返回nil了,那么就轮不到子视图来hit-testing了。
这也是为何在imgView上面加载UIButton的时候,button无法响应的原因

问题2:如果一个视图A(A上面加载了手势处理)被视图B盖住了,A与B都是视图X的子视图,那么怎样让A的手势能响应?
分析: 因为B盖住了A,所以hit-test的结果之后,hit-view肯定是B,A的手势无法响应,
可以这么做:
1, 设置B.userInteractionEnable = NO;
2, B.hidden = YES;
3, B.alpha = 0;
上面的3种情况下,A都可以响应手势了。
因为这么设置之后,在hit-testing的时候,B视图的hitTest方法返回的是nil,所以触摸事件就轮到了A来处理。

问题3:如果一个view自己不愿意处理touch事件,但是希望它的subViews处理,怎么办?
分析:设置view.userInteractionEnable = NO;之后,虽然自己不会响应touch事件,但是它的子view也不会响应了,
所以不能这么做。这时候就需要使用hitTest来处理,
-(id)hitTest:(CGPoint)pointwithEvent:(UIEvent *)event
{

id hitView = [super hitTest:pointwithEvent:event]; // 此时hitView是已经检测出的hit-view了,是self
or subViews(hitted subView)

if (hitView == self) {

return nil; // 是self的时候, 不做处理

} else {

return hitView; // 是subView的时候,由subView去处理

}

}

更多信息可以参考测试工程TestGestureResponseChain
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: