zoukankan      html  css  js  c++  java
  • iOS之hitTest

    前言

    我负责努力,其余交给运气。

    写这篇文章,是因为之前写了一篇如何解决button点击范围过小的文章,然后评论区小伙伴说hitTest也可以,然后我就查了一下hitTest,发现真的有其牛逼之处,所以整理一下。

    一、什么是hitTest

    官方文档中介绍(若理解翻译的不对还请指正):- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

    • Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.
      自我理解:返回所能包含point的view和view.subviews中最后的一个view。

    • point:A point specified 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.
      自我理解:此方法可以正常响应的事件。如果从触发事件之外调用此方法,则可以指定为nil。

    • Return Value:The view object that is the farthest descendent of the current view and contains point. Returns nil if the point lies completely outside the receiver’s view hierarchy.
      自我理解:所能包含point的view和view.subviews中最后的一个view。如果point完全位于视图层次结构之外,则返回nil

    总的来说就是:该方法会被系统调用(可重写),在视图的层次结构中寻找到一个最适合的 view (理解为最上层view)来响应触摸事件,如果返回为nil,即事件有可能被丢弃。

    二、hitTest的调用顺序

    触摸事件寻找最佳响应者,即hitTest 的调用顺序大致如下:

    touch(UIEvent)->UIApplication->UIWindow->window.subviews->...->view
    
    1. 当App接收触摸事件时,主线程的runloop被唤醒,触发source1回调。source1回调又触发了一个source0回调,将接收到的触摸事件(IOHIDEvent对象)封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。source0回调将触摸事件添加到UIApplication的事件队列中。
    2. UIApplication会从事件队列中取出最早的事件进行分发处理,首先将事件传递给窗口对象(UIWindow),如果有多个UIWindow对象,则先选择最后加上的UIWindow对象。
    3. UIWindow会调用其hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件。
    三、触摸事件的传递顺序

    通过hitTest我们已经找到了最佳响应者,下面要做的事就是让这个最佳响应者响应触摸事件。这个最佳响应者对于触摸事件拥有决定权,它可以决定是自己独自响应这个事件,也可以自己响应之后还把它传递给其他响应者。

    事件传递顺序大致为:

    view -> superView ...- > UIViewController.view -> UIViewController -> UIWindow -> UIApplication -> 事件丢弃
    

    文字说明:

    • 1、 首先由 view 来尝试处理事件,如果他处理不了,事件将被传递到他的父视图superview
    • 2、superview 也尝试来处理事件,如果他处理不了,继续传递他的父视图
      UIViewcontroller.view
    • 3、UIViewController.view尝试来处理该事件,如果处理不了,将把该事件传递给UIViewController
    • 4、UIViewController尝试处理该事件,如果处理不了,将把该事件传递给主窗口Window
    • 5、主窗口Window尝试来处理该事件,如果处理不了,将传递给应用单例Application
    • 6、如果Application也处理不了,则该事件将会被丢弃。

    注:
    响应者对于事件的响应和传递都是在touchesBegan:withEvent:这个方法中完成的。该方法默认的实现是将该方法沿着响应链往下传递。
    响应者对于接收到的事件有三种操作:

    • 1.默认的操作。不拦截,事件会沿着默认的响应链自动往下传递。
    • 2.拦截,不再往下分发事件,重写touchesBegan:withEvent:方法,不调用父类的touchesBegan:withEvent:方法。
    • 3.不拦截,继续往下分发事件,重新touchesBegan:withEvent:方法,并调用父类的touchesBegan:withEvent:方法。
    四、hitTest的实现思路

    首先看一下系统的hitTest是怎么调用的,看代码:

    #import "ViewController.h"
    #import "AView.h"
    #import "BView.h"
    #import "CView.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        AView* aView = [[AView alloc] init];
        aView.frame = CGRectMake(100, 100, 100, 100);
        aView.backgroundColor = [UIColor orangeColor];
        
        BView* bView = [[BView alloc] init];
        bView.frame = CGRectMake(100, 100, 80, 80);
        bView.backgroundColor = [UIColor blueColor];
        
        CView* cView = [[CView alloc] init];
        cView.frame = CGRectMake(100, 100, 60, 60);
        cView.backgroundColor = [UIColor redColor];
        
        [self.view addSubview:aView];
        [self.view addSubview:bView];
        [self.view addSubview:cView];
        
        for (UIView* hitView in self.view.subviews) {
            NSLog(@"for %@",[hitView class]);
        }
    }
    
    @end
    
    #import "AView.h"
    
    @implementation AView
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        NSLog(@"-----hitTest star AView-----");
        UIView* view = [super hitTest:point withEvent:event];
        NSLog(@"-----hitTest end AView-----");
        return view;
    }
    
    @end
    

    AView、BView、CView都是继承UIView,都重写了它们的hitTest。在A、B、C之外点击,运行结果如下:

     
    运行结果

    我们可以看到:for in循环的打印结果,因为subviews是一个数组,所以有序,顺序为addSubview决定;而hitTest打印结果很明显,官方文档说是寻找最远View,看打印结果我理解是就是从subviews最后一个开始找,也就是最上层的view(虽然subviews可以说是同一层级,因为都在view上,但是后添加的确实会覆盖先添加的view,所以个人认为哪怕subviews都属于view层级,但是他们之间依然是后添加的相对来说在最上层。)

    且常见的视图不响应事件不外乎如下几种情况:

    1、view.userInteractionEnabled = NO;
    2、view.hidden = YES;
    3、view.alpha < 0.05;
    4、view 超出 superview 的 bounds;
    

    那么hitTest 就可根据上面 结果 大概模拟下 hitTest 方法的大概实现:

    - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        // 如果交互未打开,或者透明度小于0.05 或者 视图被隐藏
        if (self.userInteractionEnabled == NO || self.alpha < 0.05 || self.hidden == YES)
        {
            return nil;
        }
        // 如果 touch 的point 在 self 的bounds 内
        if ([self pointInside:point withEvent:event])
        {
            NSInteger count = self.subviews.count;
            for ( int i = 0; i < count; I++)
            {
                UIView* subView = self.subviews[count - 1 - I];
                //进行坐标转化
                CGPoint coverPoint = [subView convertPoint:point fromView:self];
                // 调用子视图的 hitTest 重复上面的步骤。找到了,返回hitTest view ,没找到返回有自身处理
                UIView *hitTestView = [subView hitTest:coverPoint withEvent:event];
                if (hitTestView)
                {
                    return hitTestView;
                }
            }
            return self;
        }
        return nil;
    }
    

    很多文章,直接用for in 遍历 subviews,应该是不对的。步骤文字说明:

    • 1、首先在当前视图的hitTest 方法中调用pointInside 方法判断触摸点是否在当前视图内
    • 2、若pointInside 方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest 返回nil ,该视图不处理该事件
    • 3、若pointInside 方法返回YES,说明触摸点在当前视图内,则从最上层的子视图开始(即从subviews 数组的末尾向前遍历),遍历当前视图的所有子视图,调用子视图的hitTest 方法重复步骤1-3
    • 4、直到有子视图的hitTest 方法返回非空对象或者全部子视图遍历完毕
    • 5、若第一次有子视图的hitTest 方法返回非空对象,则当前视图的hitTest 方法就返回此对象,处理结束
    • 6、若所有子视图的hitTest 方法都返回nil,则当前视图的hitTest 方法返回当前视图本身,最终由该对象处理触摸事件

    代码文字描述的可能比较复杂,下面图文再描述一遍,如图:

     
    视图

    A、B橘黄色View在白色View上,C、D在A上,E、F在B上:
     
    层次结构图

    addSubview顺序为A、B、C、D、E、F;

    结果:
    点击白色view区域:B->A
    点击F:B->F
    点击E:B->F->E
    点击C:B->A->D->C
    点击D:B->A->D

    结论与之前相同,hitTest 一直是在找包含触点的最上层View(subviews最后一个:最后add的View)

    五、hitTest的运用场景
    1、事件穿透

    我们可以让上层响应事件的同时,下层view同时响应。好像不太能碰得到,暂不举例说明(感觉情况有点多...不同层次结果可能解决方法是不一样的,有需要的可以留言...)。

    2、子视图超出父视图 范围
     
    我是盗图小能手

    类似与上图:发布按钮已然已经超出tabbar的范围,那么该按钮是如何响应点击事件的?
    解决办法:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    
         //将当前tabbar的触摸点转换坐标系,转换到中间按钮的身上,生成一个新的点
         CGPoint newP = [self convertPoint:point toView:self.centerBtn];
    
          //判断如果这个新的点是在中间按钮身上,那么处理点击事件最合适的view就是中间按钮
          if ( [self.centerBtn pointInside:newP withEvent:event]) 
          {
                return self.centerBtn;
           }
    
    
        return [super hitTest:point withEvent:event];
    
    }//重写hitTest方法,去监听中间按钮的点击,目的是为了让凸出的部分点击也有反应
    
    总结:

    hitTest其实最牛逼的地方在于,我们可以更好的了解一个事件触发后从App一直到响应的过程;我们也可以针对触点重写hitTest,让其在一定范围内响应某些事件;最后就是解决明明在触点下但是超出父视图事件不响应的问题。(有的时候发现很多东西,都是知其然不知其所以然,希望自己和大家,慢慢的探索,做到知其然知其所以然... 所以若有问题,欢迎并感谢大家指正)

    提问:

    最上面的输出结果大家也看到了,hitTest实际上走了两遍,不知道为什么,大家有知道的请留言哈...

    参考:

    参考文章1
    参考文章2
    参考文章3 iOS中触摸事件传递和响应原理
    官方文档

     
     
    5人点赞
     
     


    作者:陌路卖酱油
    链接:https://www.jianshu.com/p/d2f743acfc70
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    智能移动机器人背后蕴含的技术——激光雷达
    Kalman Filters
    Fiddler抓HttpClient的包
    VSCode开发WebApi EFCore的坑
    WPF之小米Logo超圆角的实现
    windows react打包发布
    jenkins in docker踩坑汇总
    Using ML.NET in Jupyter notebooks 在jupyter notebook中使用ML.NET ——No design time or full build available
    【Linux知识点】CentOS7 更换阿里云源
    【Golang 报错】exec gcc executable file not found in %PATH%
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/13212115.html
Copyright © 2011-2022 走看看