iOS中的事件大概分为三种,分别是 Milti-Touch Events, Motion Events 和Remote Control Events(events for controlling multimedia)。
本文将主要针对TouchEvents的分发,做一个详细的介绍。先抛出一个问题,文章的后续部分会对问题进行解答:iOS7原生的自带NavigationController可以实现从最左侧拖动PopViewController(大约13pt),不管当前可见的ViewController有没有其他的滑动手势或者事件,这是为什么?如何实现。
我们已经处理过太多触摸事件了,比如按钮的点击事件,一些View的手势等等。那到底我们点一下屏幕,当前的View是如何知道他被点击了呢,这个就要通过HitTest来确定了
每当我们点击了一下iOS设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前active的app(官方原文说:Then it places the event object in the active app’s event queue.)
告知当前活动的app有事件之后,UIApplication 单例就会从事件队列中去取最新的事件,然后分发给能够处理该事件的对象。UIApplication 获取到Event之后,Application就纠结于到底要把这个事件传递给谁,这时候就要依靠HitTest来决定了。
iOS中,hit-Testing的作用就是找出这个触摸点下面的View是什么,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。来一个简单的图说明一下
假设我们现在点击到了图中的E,hit-testing将进行如下步骤的检测(不包含重写hit-test并且返回非默认View的情况)
1、触摸点在ViewA内,所以检查ViewA的Subview B、C
2、触摸点不在ViewB内,触摸点在ViewC内部,所以检查ViewC的Subview D、E
3、触摸点不在ViewD内,触摸点发生在ViewE内部,并且ViewE没有subview,所以ViewE属于ViewA中包含这个点的最小单位,所以ViewE变成了该次触摸事件的hit-TestView
PS.
1、默认的hit-testing顺序是按照UIView中Subviews的逆顺序
2、如果View的同级别Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil, 再检查底部的Subview
3、Hit-Test也是比较聪明的,检测过程中有这么一点,就是说如果点击没有发生在某View中,那么该事件就不可能发生在View的Subview中,所以检测过程中发现该事件不在ViewB内,也直接就不会检测在不在ViewF内。也就是说,如果你的Subview设置了clipsToBounds=NO,实际显示区域可能超出了superView的frame,你点击超出的部分,是不会处理你的事件的,就是这么任性!
Hit-Test的检查机制如上所示,当确定了Hit-TestView时,如果当前的application没有忽略触摸事件 (UIApplication:isIgnoringInteractionEvents),则application就会去分发事件(sendEvent:->keywindow:sendEvent:)
UIView中提供两个方法用来确定hit-testing View,如下所示 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds
当一个View收到hitTest消息时,会调用自己的pointInside:withEvent:方法,如果pointInside返回YES,则表明触摸事件发生在我自己内部,则会遍历自己的所有Subview去寻找最小单位(没有任何子view)的UIView,如果当前View.userInteractionEnabled = NO,enabled=NO(UIControl),或者alpha<=0.01, hidden等情况的时候,hitTest就不会调用自己的pointInside了,直接返回nil,然后系统就回去遍历兄弟节点。简而言之,可以写成这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-
(
UIView
*
)
hitTest
:
(
CGPoint
)
point
withEvent
:
(
UIEvent
*
)
event
{
if
(
self
.
alpha
&
lt
;
=
0.01
||
!
self
.
userInteractionEnabled
||
self
.
hidden
)
{
return
nil
;
}
BOOL
inside
=
[
self
pointInside
:point
withEvent
:event
]
;
UIView
*hitView
=
nil
;
if
(
inside
)
{
NSEnumerator
*enumerator
=
[
self
.
subviews
reverseObjectEnumerator
]
;
for
(
UIView
*subview
in
enumerator
)
{
hitView
=
[
subview
hitTest
:point
withEvent
:event
]
;
if
(
hitView
)
{
break
;
}
}
if
(
!
hitView
)
{
hitView
=
self
;
}
return
hitView
;
}
else
{
return
nil
;
}
}
|
hit-Test 是事件分发的第一步,就算你的app忽略了事件,也会发生hit-Test。确定了hit-TestView之后,才会开始进行下一步的事件分发。
我们可以利用hit-Test做一些事情,比如我们点击了ViewA,我们想让ViewB响应,这个时候,我们只需要重写View’s hitTest方法,返回ViewB就可以了,虽然可能用不到,但是偶尔还是会用到的。大概代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
@interface
STPView
: UIView
@end
@implementation
STPView
-
(
instancetype
)
initWithFrame
:
(
CGRect
)
frame
{
self
=
[
super
initWithFrame
:frame
]
;
if
(
self
)
{
UIButton
*button
=
[
UIButton
buttonWithType
:UIButtonTypeCustom
]
;
button
.
frame
=
CGRectMake
(
0
,
0
,
CGRectGetWidth
(
frame
)
,
CGRectGetHeight
(
frame
)
/
2
)
;
button
.
tag
=
10001
;
button
.
backgroundColor
=
[
UIColor
grayColor
]
;
[
button
setTitle
:
@
&
quot
;
Button1
&
quot
;
forState
:UIControlStateNormal
]
;
[
self
addSubview
:button
]
;
[
button
addTarget
:self
action
:
@selector
(
_buttonActionFired
:
)
forControlEvents
:UIControlEventTouchDown
]
;
UIButton
*button2
=
[
UIButton
buttonWithType
:UIButtonTypeCustom
]
;
button2
.
frame
=
CGRectMake
(
0
,
CGRectGetHeight
(
frame
)
/
2
,
CGRectGetWidth
(
frame
)
,
CGRectGetHeight
(
frame
)
/
2
)
;
button2
.
tag
=
10002
;
button2
.
backgroundColor
=
[
UIColor
darkGrayColor
]
;
[
button2
setTitle
:
@
&
quot
;
Button2
&
quot
;
forState
:UIControlStateNormal
]
;
[
self
addSubview
:button2
]
;
[
button2
addTarget
:self
action
:
@selector
(
_buttonActionFired
:
)
forControlEvents
:UIControlEventTouchDown
]
;
}
return
self
;
}
-
(
void
)
_buttonActionFired
:
(
UIButton
*
)
button
{
NSLog
(
@
&
quot
;
===
==
Button
Titled
%
@
ActionFired
&
quot
;
,
[
button
titleForState
:UIControlStateNormal
]
)
;
}
-
(
UIView
*
)
hitTest
:
(
CGPoint
)
point
withEvent
:
(
UIEvent
*
)
event
{
UIView
*hitView
=
[
super
hitTest
:point
withEvent
:event
]
;
if
(
hitView
==
[
self
viewWithTag
:
10001
]
)
{
return
[
self
viewWithTag
:
10002
]
;
}
return
hitView
;
}
@end
|
大家可以试一试,上述代码在点击上面的按钮的时候,实际会触发下面按钮的事件,不是经常用到,但是也算是涨姿势了,这里给大家提供一个Category,来自STKit,这个category的目的就是方便的编写hitTest方法,由于hitTest方法是override,而不是delegate,所以使用默认的实现方式就比较麻烦。Category如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/**
* @abstract hitTestBlock
*
* @param 其余参数 参考UIView hitTest:withEvent:
* @param returnSuper 是否返回Super的值。如果*returnSuper=YES,则代表会返回 super hitTest:withEvent:, 否则则按照block的返回值(即使是nil)
*
* @discussion 切记,千万不要在这个block中调用self hitTest:withPoint,否则则会造成递归调用。这个方法就是hitTest:withEvent的一个代替。
*/
typedef
UIView
*
(
^
STHitTestViewBlock
)
(
CGPoint
point
,
UIEvent
*event
,
BOOL
*returnSuper
)
;
typedef
BOOL
(
^
STPointInsideBlock
)
(
CGPoint
point
,
UIEvent
*event
,
BOOL
*returnSuper
)
;
@interface
UIView
(
STHitTest
)
/// althought this is strong ,but i deal it with copy
@property
(
nonatomic
,
strong
)
STHitTestViewBlock
hitTestBlock
;
@property
(
nonatomic
,
strong
)
STPointInsideBlock
pointInsideBlock
;
@end
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
@implementation
UIView
(
STHitTest
)
const
static
NSString
*STHitTestViewBlockKey
=
@
&
quot
;
STHitTestViewBlockKey
&
quot
;
;
const
static
NSString
*STPointInsideBlockKey
=
@
&
quot
;
STPointInsideBlockKey
&
quot
;
;
+
(
void
)
load
{
method_exchangeImplementations
(
class_getInstanceMethod
(
self
,
@selector
(
hitTest
:withEvent
:
)
)
,
class_getInstanceMethod
(
self
,
@selector
(
st_hitTest
:withEvent
:
)
)
)
;
method_exchangeImplementations
(
class_getInstanceMethod
(
self
,
@selector
(
pointInside
:withEvent
:
)
)
,
class_getInstanceMethod
(
self
,
@selector
(
st_pointInside
:withEvent
:
)
)
)
;
}
-
(
UIView
*
)
st_hitTest
:
(
CGPoint
)
point
withEvent
:
(
UIEvent
*
)
event
{
NSMutableString
*spaces
=
[
NSMutableString
stringWithCapacity
:
20
]
;
UIView
*superView
=
self
.
superview
;
while
(
superView
)
{
[
spaces
appendString
:
@
&
quot
;
--
--
&
quot
;
]
;
superView
=
superView
.
superview
;
}
NSLog
(
@
&
quot
;
%
@
%
@
:
[
hitTest
:withEvent
:
]
&
quot
;
,
spaces
,
NSStringFromClass
(
self
.
class
)
)
;
UIView
*deliveredView
=
nil
;
// 如果有hitTestBlock的实现,则调用block
if
(
self
.
hitTestBlock
)
{
BOOL
returnSuper
=
NO
;
deliveredView
=
self
.
hitTestBlock
(
point
,
event
,
&
amp
;
returnSuper
)
;
if
(
returnSuper
)
{
deliveredView
=
[
self
st_hitTest
:point
withEvent
:event
]
;
}
}
else
{
deliveredView
=
[
self
st_hitTest
:point
withEvent
:event
]
;
}
// NSLog(@"%@%@:[hitTest:withEvent:] Result:%@", spaces, NSStringFromClass(self.class), NSStringFromClass(deliveredView.class));
return
deliveredView
;
}
-
(
BOOL
)
st_pointInside
:
(
CGPoint
)
point
withEvent
:
(
UIEvent
*
)
event
{
NSMutableString
*spaces
=
[
NSMutableString
stringWithCapacity
:
20
]
;
UIView
*superView
=
self
.
superview
;
while
(
superView
)
{
[
spaces
appendString
:
@
&
quot
;
--
--
&
quot
;
]
;
superView
=
superView
.
superview
;
}
NSLog
(
@
&
quot
;
%
@
%
@
:
[
pointInside
:withEvent
:
]
&
quot
;
,
spaces
,
NSStringFromClass
(
self
.
class
)
)
;
BOOL
pointInside
=
NO
;
if
(
self
.
pointInsideBlock
)
{
BOOL
returnSuper
=
NO
;
pointInside
=
self
.
pointInsideBlock
(
point
,
event
,
&
amp
;
returnSuper
)
;
if
(
returnSuper
)
{
pointInside
=
[
self
st_pointInside
:point
withEvent
:event
]
;
}
}
else
{
pointInside
=
[
self
st_pointInside
:point
withEvent
:event
]
;
}
return
pointInside
;
}
-
(
void
)
setHitTestBlock
:
(
STHitTestViewBlock
)
hitTestBlock
{
objc_setAssociatedObject
(
self
,
(
__bridge
const
void
*
)
(
STHitTestViewBlockKey
)
,
hitTestBlock
,
OBJC_ASSOCIATION_COPY
)
;
}
-
(
STHitTestViewBlock
)
hitTestBlock
{
return
objc_getAssociatedObject
(
self
,
(
__bridge
const
void
*
)
(
STHitTestViewBlockKey
)
)
;
}
-
(
void
)
setPointInsideBlock
:
(
STPointInsideBlock
)
pointInsideBlock
{
objc_setAssociatedObject
(
self
,
(
__bridge
const
void
*
)
(
STPointInsideBlockKey
)
,
pointInsideBlock
,
OBJC_ASSOCIATION_COPY
)
;
}
-
(
STPointInsideBlock
)
pointInsideBlock
{
return
objc_getAssociatedObject
(
self
,
(
__bridge
const
void
*
)
(
STPointInsideBlockKey
)
)
;
}
@end
|
代码很简单,就是利用iOS的runtime能力,在hitTest执行之前,插入了一个方法。如果有看不懂的,可以参考我以前的博客 iOS面向切面编程
现在回到我们开始提出的题目,其实题目很简单,就是简单的可以把题目转换为
如果我们触摸点的坐标 point.x < 13, 我们就让hit-Test 返回NavigationController.view, 把所有的事件入口交给他,否则就返回super,该怎么处理怎么处理
这样就能满足我们的条件,即使当前的VC上面有ScrollView,但是由于点击特定区域的时候,ScrollView根本得不到事件,所以系统会专心处理NavigationController的拖拽手势,而不是ScrollView的事件,当没有点击特定区域的时候,NavigationController的手势不会触发,系统会专心处理ScrollView的事件,互不影响,大家可以尝试实现,代码量不多。
虽然iOS8新增了UIScreenEdgePanGestureRecognizer 手势,但是单纯的用这个手势无法解决当前VC上面有ScrollView的问题,有关手势方面的事件分发,之后的文章会对此进行说明,这里就不多说了。
当我们确定了HitTestView之后,我们的事件分发就正式开始了,如果hitTestView可以直接处理的,就处理,不能处理的,则交给 The Responder Chain/ GestureRecognizer。后续文章会对分发进行进一步说明。
附上一些测试查找hitTestView过程中打印的日志,可以观察一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
STPWindow
:
[
hitTest
:withEvent
:
]
--
--
UIView
:
[
hitTest
:withEvent
:
]
--
--
--
--
STPView
:
[
hitTest
:withEvent
:
]
--
--
--
--
UICollectionView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
UIImageView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
UIImageView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
STDefaultRefreshControl
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
STPFeedCell
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
STPFeedCell
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
--
--
UIView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
--
--
--
--
UIImageView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
--
--
--
--
--
--
UIImageView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
--
--
--
--
--
--
UIView
:
[
hitTest
:withEvent
:
]
--
--
--
--
--
--
--
--
--
--
--
--
STImageView
:
[
hitTest
:withEvent
:
]
|
其中—-表示View的层次结构