hitTest:withEvent:方法流程

一.hitTest:withEvent:调用过程

iOS系统检测到手指触摸(Touch)操作时会将其放入当前活动Application的事件队列,UIApplication会从事件队列中取出触摸事件并传递给key window(当前接收用户事件的窗口)处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view。

window对象会在首先在view hierarchy的顶级view上调用hitTest:withEvent:,此方法会在视图层级结构中的每个视图上调用pointInside:withEvent:,如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是hit-test view。

hitTest:withEvent:方法的处理流程如下:

首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;

若返回NO,则hitTest:withEvent:返回nil;

若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;

若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;

如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds属 性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的 pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写 pointInside:withEvent:方法来处理这种情况。

对于每个触摸操作都会有一个UITouch对 象,UITouch对象用来表示一个触摸操作,即一个手指在屏幕上按下、移动、离开的整个过程。UITouch对象在触摸操作的过程中在不断变化,所以在 使用UITouch对象时,不能直接retain,而需要使用其他手段存储UITouch的内部信息。UITouch对象有一个view属性,表示此触摸操作初始发生所在的视图,即上面检测到的hit-test view,此属性在UITouch的生命周期不再改变,即使触摸操作后续移动到其他视图之上。

二.定制hitTest:withEvent:方法

如果父视图需要对对哪个子视图可以响应触摸事件做特殊控制,则可以重写hitTest:withEvent:pointInside:withEvent:方法。

这里有几个例子:

hitTest Hacking the responder chain

在此例子中button,scrollview同为topView的子视图,但scrollview覆盖在button之上,这样在在button上的触 摸操作返回的hit-test view为scrollview,button无法响应,可以修改topView的hitTest:withEvent:方法如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

UIView *result = [super hitTest:point withEvent:event];

CGPoint buttonPoint = [underButton convertPoint:point fromView:self];

if ([underButton pointInside:buttonPoint withEvent:event]) {

return underButton;

}

return result;

}

这样如果触摸点在button的范围内,返回hittestView为button,从button按钮可以响应点击事件。

Paging-enabled UIScrollView with Previews

BSPreviewScrollView

关于这两个例子,可以看之前文章的说明,见Paging-enabled UIScrollView

三.hitTest:withEvent:探秘,诡异的三次调用

在测试中发现对每一次触摸操作实际会触发三次hitTest:withEvent:方法调用,有测试环境视图结构如下 UIWindow->UIScrollView->TapDetectingImageView

首先在TapDetectingImageView的三次hitTest:withEvent:打印两个参数查看有何不同

三次的打印结果分别为:

point:{356.25, 232.031} event: timestamp: 25269.8 touches: {()}

point:{356.25, 232.031} event: timestamp: 25269.8 touches: {()}

point:{356.25, 232.031} event: timestamp: 25272 touches: {()}

三次调用的point参数完全相同,event从指针看为同一对象,但时间戳有变化,第一次和第二次的时间戳相同,第三次的与之前有区别。

深入检查event参数

对于UIEvent对象我们还可以查看其内部的数据,UIEvent实际上是对GSEventRefs的包装,在GSEventRefs中又包含GSEventRecord,其结构如下:

typedef struct __GSEvent {

CFRuntimeBase _base;

GSEventRecord record;

} GSEvent; typedef struct __GSEvent* GSEventRef;

typedef struct GSEventRecord {

GSEventType type; // 0x8 //2

GSEventSubType subtype;    // 0xC //3

CGPoint location;    // 0x10 //4

CGPoint windowLocation;    // 0x18 //6

int windowContextId;    // 0x20 //8

uint64_t timestamp;    // 0x24, from mach_absolute_time //9

GSWindowRef window;    // 0x2C //

GSEventFlags flags;    // 0x30 //12

unsigned senderPID;    // 0x34 //13

CFIndex infoSize; // 0x38 //14 } GSEventRecord;

在GSEventRecord中我们可以获取到GSEvent事件类型type,windowLocation(在窗口坐标系中的位置)等参数。

访问UIEvent中的GSEventRecord可以使用以下代码

if ([event respondsToSelector:@selector(_gsEvent)]) {

#define GSEVENT_TYPE 2

#define GSEVENT_SUBTYPE 3

#define GSEVENT_X_OFFSET 6

#define GSEVENT_Y_OFFSET 7

#define GSEVENT_FLAGS 12

#define GSEVENTKEY_KEYCODE 15

#define GSEVENT_TYPE_KEYUP 11

int *eventMem;

eventMem = (int *)objc_unretainedPointer([event performSelector:@selector(_gsEvent)]);

if (eventMem) {

int eventType = eventMem[GSEVENT_TYPE];

int eventSubType = eventMem[GSEVENT_SUBTYPE];

float xOffset =  *((float*)(eventMem + GSEVENT_X_OFFSET));

float yOffset =  *((float*)(eventMem + GSEVENT_Y_OFFSET));

}

}

按照上文描述的方法我们获取到UIEvent内部的windowLocation数据,同时将接收到的point参数在window坐标系中的位置也打印出,这样三次调用的数据分别为

point:{356.25, 232.031} windowPoint:{152, 232} event: timestamp: 25269.8 touches: {()} gsEventType:3001 gsXoffset:213.000000 gsYoffset:316.000000

point:{356.25, 232.031} windowPoint:{152, 232} event: timestamp: 25269.8 touches: {()} gsEventType:3001 gsXoffset:213.000000 gsYoffset:316.000000

point:{356.25, 232.031} windowPoint:{152, 232} event: timestamp: 25272 touches: {()} gsEventType:3001 gsXoffset:152.000000 gsYoffset:232.000000

第一次和第二次 调用的时间戳相同,GSEvent中的windowLocation也相同,但windowLocation并不是当前请求的point位置,第三次请求 的时间戳与前两次不同,GSEvent中的windowLocation与当前请求的point位置一致。

多次点击可发现,第一次和第二次调用中的windowLocation数据实际上是上次点击操作的位置,猜测前两次hitTest是对上次点击操作的终结?第三次hitTest才是针对当前点击的。

调用栈分析

使用

[NSThreadcallStackSymbols];

可以获取到当前线程的调用栈数据,在TapDetectingImageView的hitTest:withEvent:中打印调用栈信息,分别为:

第一次调用:

0  RenrenPhoto                        0x0002b52d -[TapDetectingImageView hitTest:withEvent:] + 93

1  UIKit                              0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132

2  CoreFoundation                      0x01b515a7 __NSArrayChunkIterate + 359

3  CoreFoundation                      0x01b2903f __NSArrayEnumerate + 1023

4  CoreFoundation                      0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102

5  UIKit                              0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640

6  UIKit                              0x00497397 -[UIScrollView hitTest:withEvent:] + 79

7  UIKit                              0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132

8  CoreFoundation                      0x01b515a7 __NSArrayChunkIterate + 359

9  CoreFoundation                      0x01b2903f __NSArrayEnumerate + 1023

10  CoreFoundation                      0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102

11  UIKit                              0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640

12  RenrenPhoto                        0x0002e01b -[UIEventProbeWindow hitTest:withEvent:] + 395

13  UIKit                              0x00477a02 __47+[UIWindow _hitTestToPoint:pathIndex:forEvent:]_block_invoke_0 + 150

14  UIKit                              0x00477888 +[UIWindow _topVisibleWindowPassingTest:] + 196

15  UIKit                              0x00477965 +[UIWindow _hitTestToPoint:pathIndex:forEvent:] + 177

16  UIKit                              0x0043cd06 _UIApplicationHandleEvent + 1696

17  GraphicsServices                    0x01ff8df9 _PurpleEventCallback + 339

18  GraphicsServices                    0x01ff8ad0 PurpleEventCallback + 46

19  CoreFoundation                      0x01aa4bf5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53

20  CoreFoundation                      0x01aa4962 __CFRunLoopDoSource1 + 146

21  CoreFoundation                      0x01ad5bb6 __CFRunLoopRun + 2118

22  CoreFoundation                      0x01ad4f44 CFRunLoopRunSpecific + 276

23  CoreFoundation                      0x01ad4e1b CFRunLoopRunInMode + 123

24  GraphicsServices                    0x01ff77e3 GSEventRunModal + 88

25  GraphicsServices                    0x01ff7668 GSEventRun + 104

26  UIKit                              0x0043c65c UIApplicationMain + 1211

27  RenrenPhoto                        0x000026b2 main + 178

28  RenrenPhoto                        0x000025b5 start + 53

29  ???                                0x00000001 0x0 + 1

第二次调用

0  RenrenPhoto                        0x0002b52d -[TapDetectingImageView hitTest:withEvent:] + 93

1  UIKit                              0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132

2  CoreFoundation                      0x01b515a7 __NSArrayChunkIterate + 359

3  CoreFoundation                      0x01b2903f __NSArrayEnumerate + 1023

4  CoreFoundation                      0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102

5  UIKit                              0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640

6  UIKit                              0x00497397 -[UIScrollView hitTest:withEvent:] + 79

7  UIKit                              0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132

8  CoreFoundation                      0x01b515a7 __NSArrayChunkIterate + 359

9  CoreFoundation                      0x01b2903f __NSArrayEnumerate + 1023

10  CoreFoundation                      0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102

11  UIKit                              0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640

12  RenrenPhoto                        0x0002e01b -[UIEventProbeWindow hitTest:withEvent:] + 395

13  UIKit                              0x00477a02 __47+[UIWindow _hitTestToPoint:pathIndex:forEvent:]_block_invoke_0 + 150

14  UIKit                              0x00477888 +[UIWindow _topVisibleWindowPassingTest:] + 196

15  UIKit                              0x00477965 +[UIWindow _hitTestToPoint:pathIndex:forEvent:] + 177

16  UIKit                              0x0043cfd3 _UIApplicationHandleEvent + 2413

17  GraphicsServices                    0x01ff8df9 _PurpleEventCallback + 339

18  GraphicsServices                    0x01ff8ad0 PurpleEventCallback + 46

19  CoreFoundation                      0x01aa4bf5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53

20  CoreFoundation                      0x01aa4962 __CFRunLoopDoSource1 + 146

21  CoreFoundation                      0x01ad5bb6 __CFRunLoopRun + 2118

22  CoreFoundation                      0x01ad4f44 CFRunLoopRunSpecific + 276

23  CoreFoundation                      0x01ad4e1b CFRunLoopRunInMode + 123

24  GraphicsServices                    0x01ff77e3 GSEventRunModal + 88

25  GraphicsServices                    0x01ff7668 GSEventRun + 104

26  UIKit                              0x0043c65c UIApplicationMain + 1211

27  RenrenPhoto                        0x000026b2 main + 178

28  RenrenPhoto                        0x000025b5 start + 53

29  ???                                0x00000001 0x0 + 1

第三次调用:

0  RenrenPhoto                        0x0002b52d -[TapDetectingImageView hitTest:withEvent:] + 93

1  UIKit                              0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132

2  CoreFoundation                      0x01b515a7 __NSArrayChunkIterate + 359

3  CoreFoundation                      0x01b2903f __NSArrayEnumerate + 1023

4  CoreFoundation                      0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102

5  UIKit                              0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640

6  UIKit                              0x00497397 -[UIScrollView hitTest:withEvent:] + 79

7  UIKit                              0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132

8  CoreFoundation                      0x01b515a7 __NSArrayChunkIterate + 359

9  CoreFoundation                      0x01b2903f __NSArrayEnumerate + 1023

10  CoreFoundation                      0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102

11  UIKit                              0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640

12  RenrenPhoto                        0x0002e01b -[UIEventProbeWindow hitTest:withEvent:] + 395

13  UIKit                              0x0043d986 _UIApplicationHandleEvent + 4896

14  GraphicsServices                    0x01ff8df9 _PurpleEventCallback + 339

15  GraphicsServices                    0x01ff8ad0 PurpleEventCallback + 46

16  CoreFoundation                      0x01aa4bf5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53

17  CoreFoundation                      0x01aa4962 __CFRunLoopDoSource1 + 146

18  CoreFoundation                      0x01ad5bb6 __CFRunLoopRun + 2118

19  CoreFoundation                      0x01ad4f44 CFRunLoopRunSpecific + 276

20  CoreFoundation                      0x01ad4e1b CFRunLoopRunInMode + 123

21  GraphicsServices                    0x01ff77e3 GSEventRunModal + 88

22  GraphicsServices                    0x01ff7668 GSEventRun + 104

23  UIKit                              0x0043c65c UIApplicationMain + 1211

24  RenrenPhoto                        0x000026b2 main + 178

25  RenrenPhoto                        0x000025b5 start + 53

26  ???                                0x00000001 0x0 + 1

从调用栈上看,三次调用的路径都不相同,关键区分点在_UIApplicationHandleEvent函数中,

第一次的调用位置为_UIApplicationHandleEvent + 1696

第二次的调用位置为_UIApplicationHandleEvent + 2413

第三次的调用位置为_UIApplicationHandleEvent + 4896

结论

没有结论,具体机制仍然是扑朔迷离,搞不懂….,实际写代码时也不需要考虑这些。

参考:

Event Handling Guide for iOS - Event Delivery

Event Handling Guide for iOS - Hit-Testing

UIView Class Reference

Event handling for iOS - how hitTest:withEvent: and pointInside:withEvent: are related?

UITouch Class Reference

hitTest Hacking the responder chain

Paging-enabled UIScrollView with Previews

BSPreviewScrollView

Paging-enabled UIScrollView

Catching Keyboard Events in iOS

Kenny TM GoogleCode Repo - GSEvent.h (a bit old but still useful)

Kenny TM Github Repo - GSEvent.h

Intercepting status bar touches on the iPhone

Synthesizing a touch event on the iPhone

/*

**当按钮超过了父视图范围,点击是没有反应的。因为消息的传递是从最下层的父视图开始调用hittest方法。

[objc] view plain copy print?

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

UIView *view = [super hitTest:point withEvent:event];

return view;

}

**当存在view时才会传递对应的event,现在点击了父视图以外的范围,自然返回的是nil。所以当子视图(比如按钮btn)因为一些原因超出了父视图范围,就要重写hittest方法,让其返回对应的子视图,来接收事件。即:点击的点在自视图范围内(self.button)就返回self.button此时就会执行self.button的点击事件

*/

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {

//先执行父类的方法返回响应对象

UIView*view = [superhitTest:pointwithEvent:event];

//点击了父类以外的地方所以返回nil

if(view ==nil) {

//点转化成在self.button内的坐标

CGPointtempoint = [self.buttonconvertPoint:pointfromView:self];

//判断是不是在self.button.bounds的范围内如果是就响应事件

if(CGRectContainsPoint(self.button.bounds, tempoint))

{

view =self.button;

}

}

returnview;

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容