iOS-hitTest:withEvent与自定义hit-testing规则

在做tableView嵌套scrollView的时候怕手势冲突,研究了一下hitTest,虽然最后没用上,但是觉得比较有用,写了一个DEMO,通过重写hitTest:withEvent,实现了超出父视图范围响应触摸事件等自定义hit-testing规则,我的理解还很粗浅,如果有错误或者更优解,欢迎大家指出,我看到后会立即修正~

DEMO:https://github.com/liulishuo/LLSHitTestView

预备知识(M了个JiOS developer library

对于触摸事件的响应,首先要找到能够响应该事件的对象,iOS是用hit-testing 来找到哪个视图被触摸了(hit-test view),也就是以keyWindow为起点,hit-test view为终点,逐级调用hitTest:withEvent。

226702-dd53b5a6df2f3ea5.png

MJ大神的图

  • 测试用例:在每个视图类的hitTest:withEvent:打印两次log:1.调用时 2.返回值时

226702-eab3ef65163dae50.png

打印log的位置

226702-6ef62adca392eb91.png

触摸view2

226702-683d0d3aa91beb41.png

线索log

hitTest:withEvent:调用顺序:...->base->view2->view3

hitTest:withEvent:返回顺序: view3(nil) -> view2(self) -> base(view2)->...

00.png

触摸view1

01.png

屏幕快照 2015-12-03 09.52.39.png

hitTest:withEvent:调用顺序:...->base->view2(nil)-> base->view1

hitTest:withEvent:返回顺序: view2(nil)->base, view1(self)->base(view1)->...

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

  • 先调用pointInside:withEvent:判断触摸点是否在当前视图内

1.如果返回YES,那么该视图的所有子视图调用hitTest:withEvent,调用顺序由层级低到高(top->bottom)依次调用。

2.如果返回NO,那么hitTest:withEvent返回nil,该视图的所有子视图的分支全部被忽略

  • 如果某视图的pointInside:withEvent:返回YES,并且他的所有子视图hitTest:withEvent:都返回nil,或者该视图没有子视图,那么该视图的hitTest:withEvent:返回自己。

  • 如果子视图的hitTest:withEvent:返回非空对象,那么当前视图的hitTest:withEvent:也返回这个对象,也就是沿原路回推,最终将hit-test view传递给keyWindow

  • 以下视图的hitTest:withEvent:方法会返回nil,导致自身和其所有子视图不能被hit-testing发现,无法响应触摸事件:

1.隐藏(hidden=YES)的视图

2.禁止用户操作(userInteractionEnabled=NO)的视图

3.alpha<0.01的视图

4.视图超出父视图的区域

思路

既然系统通过hitTest:withEvent:做传递链取回hit-test view,那么我们可以在其中一环修改传递回的对象,从而改变正常的事件响应链。

实现

  • 强制指定某视图响应触摸事件:

将截获的对象替换成指定的对象,可以随便替换,只要在替换时你能拿到要替换的对象的实例。穿透scrollView点击scrollView后面的button就是这样做的。可以试试换成一个(hidden=YES、userInteractionEnabled=NO、alpha<0.01)的对象,比较违反直觉,被隐藏\禁用手势的视图一样能响应触摸事件。

经测试,将返回的hit-test view替换为加了手势的view,该view hidden=YES、userInteractionEnabled=NO、alpha<0.01三种情况都可响应事件,但是如果替换为button,并且button的userInteractionEnabled=NO或者enable=NO那么无法响应事件。

  • 忽略指定的视图:

在hitTest:withEvent:里筛选返回值,针对指定的对象返回nil

1
2
3
4
if ([view isEqual:XXX])
{
      return  nil;
}

这样做的好处是不会阻断hit-testing检测,既可忽略指定的视图又不会屏蔽其子视图。

  • 定制触摸事件的响应范围

在hitTest:withEvent:里筛选point,判断point在不在指定的范围内

1
2
3
4
5
6
7
     if (_path)
     {
           if (!CGPathContainsPoint(_path.CGPath, NULL, point, NO))
           {
               return  nil;
           }
     }

_path 是一段bezier曲线,详见代码。

  • 超出父视图范围响应

选定一个节点,遍历他的所有子节点用pointInside:withEvent:判断是否命中,直到找到命中的最低层级的视图,此时我们已经抛弃了系统的hit-testing规则。

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
- (UIView *)getTargetView:(UIView *)view
                     point:(CGPoint)point
                     event:(UIEvent *)event
{
 
     __block UIView *subView;
     
     //逆序 由层级最低 也就是最上层的子视图开始
     [view.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
         //point 从view 转到 obj中
         CGPoint hitPoint = [obj convertPoint:point fromView:view];
         //        NSLog(@"%@ - %@",NSStringFromCGPoint(point),NSStringFromCGPoint(hitPoint));
         
         if ([obj pointInside:hitPoint withEvent:event]) //在当前视图范围内
         {
             if (obj.subviews.count != 0)
             {
                 //如果有子视图 递归
                 subView = [self getTargetView:obj point:hitPoint event:event];
                 
                 if (!subView)
                 {
                     //如果没找到 提交当前视图
                     subView = obj;
                 }
             }
             else
             {
                 subView = obj;
             }
             
             *stop = YES;
         }
         else //不在当前视图范围内
         {
             if (obj.subviews.count != 0)
             {
                 //如果有子视图 递归
                 subView = [self getTargetView:obj point:hitPoint event:event];
             }
         }
     }];
     
     return  subView;
     
}

QQ截图20160108105216.png

QQ截图20160108105216.png

LLSHitTestView

33.png

层级关系

我们在层级比较高的view1使用自定义的hit-testing规则,其上的2、3、4,无论是否超出边界,均能正常响应点击事件,详见代码。

问题:为什么一次触摸会触发两次hitTest:withEvent:?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 MFC 中,可以通过以下步骤为 ListCtrl 控件添加右键自定义菜单: 1.在 C++ 代码中添加 WM_CONTEXTMENU 消息处理函数。这个消息处理函数将在用户右键单击 ListCtrl 控件时被调用。 2.在 WM_CONTEXTMENU 消息处理函数中,获取鼠标单击位置的屏幕坐标,并将其转换为 ListCtrl 控件的客户区坐标。 3.使用 HitTest() 函数获取鼠标单击位置所在的 ListCtrl 控件项,并保存其索引。 4.创建自定义菜单,并使用 TrackPopupMenu() 函数在鼠标单击位置显示菜单。 下面是一个示例代码,可以作为参考: ``` void CMyListCtrlDlg::OnContextMenu(CWnd* pWnd, CPoint point) { CMenu menu; menu.CreatePopupMenu(); // 创建自定义菜单 // 将菜单项添加到自定义菜单中,ID 可以自行定义 menu.AppendMenu(MF_STRING, ID_MENUITEM1, _T("菜单项1")); menu.AppendMenu(MF_STRING, ID_MENUITEM2, _T("菜单项2")); // 将屏幕坐标转换为客户区坐标 CPoint ptClient = point; ScreenToClient(&ptClient); // 获取鼠标单击位置所在的 ListCtrl 控件项 int nItem = HitTest(ptClient); // 如果鼠标单击位置在 ListCtrl 控件项上 if (nItem >= 0) { // 选择当前项 SetItemState(nItem, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); // 显示自定义菜单 menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, point.x, point.y, this); } else { // 如果鼠标单击位置不在 ListCtrl 控件项上,则显示默认菜单 CDialogEx::OnContextMenu(pWnd, point); } } ``` 需要注意的是,上述代码只是一个示例,实际应用中需要根据具体情况进行修改。同时,还需要在资源文件中添加自定义菜单的定义和命令 ID 的定义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值