[转]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:方法的处理流程如下:

  1. 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
  2. 若返回NO,则hitTest:withEvent:返回nil;
  3. 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
  4. 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
  5. 如所有子视图都返回非,则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:方法。

这里有几个例子:

  1. 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按钮可以响应点击事件。

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

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

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

  1. 首先在TapDetectingImageView的三次hitTest:withEvent:打印两个参数查看有何不同
    三次的打印结果分别为:

     

    point:{356.25, 232.031} event:<UITouchesEvent: 0x8653ea0> timestamp: 25269.8 touches: {()}
    point:{356.25, 232.031} event:<UITouchesEvent: 0x8653ea0> timestamp: 25269.8 touches: {()}
    point:{356.25, 232.031} event:<UITouchesEvent: 0x8653ea0> timestamp: 25272 touches: {()}

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

  2. 深入检查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:<UITouchesEvent: 0x8653ea0> timestamp: 25269.8 touches: {()} gsEventType:3001 gsXoffset:213.000000 gsYoffset:316.000000
    point:{356.25, 232.031} windowPoint:{152, 232} event:<UITouchesEvent: 0x8653ea0> timestamp: 25269.8 touches: {()} gsEventType:3001 gsXoffset:213.000000 gsYoffset:316.000000
    point:{356.25, 232.031} windowPoint:{152, 232} event:<UITouchesEvent: 0x8653ea0> timestamp: 25272 touches: {()} gsEventType:3001 gsXoffset:152.000000 gsYoffset:232.000000

     

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

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

  3. 调用栈分析
    使用
    [NSThread callStackSymbols];

    可以获取到当前线程的调用栈数据,在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

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

参考:
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

本文出自 清风徐来,水波不兴 的博客,转载时请注明出处及相应链接。

本文永久链接: http://www.winddisk.com/2012/10/11/hittest_withevent/

posted @ 2013-04-22 21:47  欧阳蓝缺  阅读(9254)  评论(0编辑  收藏  举报