iOS的事件处理 - hitTest:withEvent:和pointInside:withEvent:如何相关?

rea*_*f02 144 uikit uiview ios

虽然大多数苹果文档编写得很好,但我认为" iOS事件处理指南 "是一个例外.我很难清楚地了解那里描述的内容.

该文件说,

在命中测试中,窗口调用hitTest:withEvent:视图层次结构的最顶层视图; 此方法通过递归调用pointInside:withEvent:视图层次结构中返回YES的每个视图继续进行,继续向下移动层次结构,直到找到触摸发生在其边界内的子视图.该视图成为热门测试视图.

那么只有hitTest:withEvent:系统调用最顶层的视图,调用pointInside:withEvent:所有子视图,如果从特定子视图返回是YES,那么调用pointInside:withEvent:该子视图的子类?

pgb*_*pgb 294

我认为你混淆了子视图与视图层次结构.该文件所说的内容如下.假设您有此视图层次结构.按层次结构,我不是在讨论类层次结构,而是在视图层次结构中查看,如下所示:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+
Run Code Online (Sandbox Code Playgroud)

说你把手指放进去D.这是将要发生的事情:

  1. hitTest:withEvent:被调用A,视图层次结构的最顶层视图.
  2. pointInside:withEvent: 在每个视图上递归调用.
    1. pointInside:withEvent:被召唤A,并返回YES
    2. pointInside:withEvent:被召唤B,并返回NO
    3. pointInside:withEvent:被召唤C,并返回YES
    4. pointInside:withEvent:被召唤D,并返回YES
  3. 在返回的视图上YES,它将向下查看层次结构以查看触摸发生的子视图.在这种情况下,从A,C并且D,这将是D.
  4. D 将是热门测试视图

  • 在我的绘画中,D是C的子视图. (2认同)
  • 不要忘记不可见的视图(通过.hidden或不透明度低于0.1)或关闭用户交互将永远不会响应hitTest.我认为首先不会在这些对象上调用hitTest. (2认同)

MHC*_*MHC 170

这似乎是一个非常基本的问题.但我同意你的意见,文件不像其他文件那么清楚,所以这是我的答案.

hitTest:withEvent:在UIResponder中的实现执行以下操作:

  • 它调用pointInside:withEvent:self
  • 如果返回为NO,则hitTest:withEvent:返回nil.故事的结尾.
  • 如果返回为YES,则将hitTest:withEvent:消息发送到其子视图.它从顶级子视图开始,并继续到其他视图,直到子视图返回非nil对象,或者所有子视图都接收到该消息.
  • 如果子视图nil在第一次返回非对象,则第一次hitTest:withEvent:返回该对象.故事的结尾.
  • 如果没有子视图返回非nil对象,则第一个hitTest:withEvent:返回self

此过程以递归方式重复,因此通常最终返回视图层次结构的叶视图.

但是,您可以覆盖hitTest:withEvent以执行不同的操作.在许多情况下,覆盖pointInside:withEvent:更简单,并且仍然提供了足够的选项来调整应用程序中的事件处理.

  • 是.只需在视图中覆盖`hitTest:withEvent:`(如果需要,可以使用`pointInside`),打印日志并调用`[super hitTest ...`以查找以哪种顺序调用`hitTest:withEvent:`. (2认同)

onm*_*133 45

我发现iOS中的Hit-Testing非常有用

在此输入图像描述

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}
Run Code Online (Sandbox Code Playgroud)

编辑Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) {
        return super.hitTest(point, with: event)
    }
    guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
        return nil
    }

    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    return nil
}
Run Code Online (Sandbox Code Playgroud)


Lio*_*ion 21

感谢您的回答,他们帮我解决了"叠加"视图的情况.

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+
Run Code Online (Sandbox Code Playgroud)

假设X- 用户的触摸.pointInside:withEvent:B回报率NO,因此hitTest:withEvent:收益A.UIView当你需要在最顶级的可见视图上接收触摸时,我写了类别来处理问题.

- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
        return nil;

    // 2
    UIView *hitView = self;
    if (![self pointInside:point withEvent:event]) {
        if (self.clipsToBounds) return nil;
        else hitView = nil;
    }

    // 3
    for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
        CGPoint insideSubview = [self convertPoint:point toView:subview];
        UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
        if (sview) return sview;
    }

    // 4
    return hitView;
}
Run Code Online (Sandbox Code Playgroud)
  1. 我们不应该为隐藏或透明的视图或userInteractionEnabled设置为的视图发送触摸事件NO;
  2. 如果触摸在里面self,self将被视为潜在结果.
  3. 以递归方式检查所有子视图是否为hit.如果有,请退货.
  4. 否则返回self或nil,具体取决于步骤2的结果.

请注意,[self.subviewsreverseObjectEnumerator]需要遵循从最顶部到底部的视图层次结构.并检查clipsToBounds以确保不测试蒙版子视图.

用法:

  1. 在子类视图中导入类别.
  2. 替换hitTest:withEvent:为此
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}
Run Code Online (Sandbox Code Playgroud)

官方Apple的指南也提供了一些很好的插图.

希望这有助于某人.


yoA*_*ex5 5

iOS 触摸事件

1. user made a touch
2. system creates an event object with global coordinate of touch
3. hit testing by coordinate - find First Responder

4.1 send Touch Event to `UIGestureRecognizer`. After handling the touch can or can not(depends on setup) be forward to the First Responder

4.2 send Touch Event to the First Responder
4.2.1 handle Touch Event 
Run Code Online (Sandbox Code Playgroud)

类图

3 命中测试

点击测试来查找第一响应者 - 检查 UIView 及其后继者的层次结构(例如 UIWindow)。从最大(后)UIView(UIWindow是起点/根点)到最小(前)UIView开始递归地找到前视图。因此,在这种情况下,返回的First Responder是一个顶部(最小的)方法(内部使用) 。UIViewpoint()hitTest()point()true

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
Run Code Online (Sandbox Code Playgroud)

内部hitTest()考虑

  • point() == true
  • point()考虑超级视图内的矩形
  • isUserInteractionEnabled == true
  • isHidden == false
func hitTest() -> View? {
    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

    for subview in subviews {
        if subview.hitTest() != nil {
            return subview
        }
    }
    return nil
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 默认情况下,当您设置view.isUserInteractionEnabled = false此视图及其所有子视图时,将不会处理触摸事件
  • 默认情况下point()考虑位于超级视图中的矩形。这意味着如果触摸发生在从超级视图中绘制的视图部分上,则该视图及其所有子视图将不会处理触摸事件

[UIView.clipsToBounds]

4 发送UI事件

UIKit创建UIEvent并发送UIApplication.shared.sendEvent()main event loop[About]UIEvent contains one or more UITouch which contains -UIView`,位置...

总会经历UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>

UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>.touchesBegan()
UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>.touchesEnded()
Run Code Online (Sandbox Code Playgroud)
//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)
Run Code Online (Sandbox Code Playgroud)

4.1 发送触摸事件到UIGestureRecognizer

这是一种简单方便的手势操作方式。有一些开箱即用的,UIGestureRecognizer例如UITapGestureRecognizerUISwipeGestureRecognizer ...您可以创建自己的

let tap_v_0_2 = UITapGestureRecognizer(target: self, action: #selector(self.onView_0_2))
view_0_2.addGestureRecognizer(tap_v_0_2)

@objc func onView_0_2() {
    print("---> onView V_0_2")
}
Run Code Online (Sandbox Code Playgroud)

UIGestureRecognizer系统尝试在视图层次结构中查找。从第一响应者(使用UIView.superview)开始,直到UIWindow( 的子类UIView)。这意味着如果您设置UIGestureRecognizerUIWindow并且第一响应者UIView_0没有UIGestureRecognizer-UIWindow.UIGestureRecognizer将被调用

有一些函数可以处理之间的UIGestureRecognizer工作UIGestureRecognizerDelegate

以及一些处理转发事件到响应者链的函数,例如:cancelsTouchesInView,,delaysTouchesBegandelaysTouchesEnded

4.2 发送触摸事件到First Responder

这是更底层、更高级的方法,您可以在其中自定义逻辑。要使用它,您应该继承UIView重写一些方法,例如:

//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
Run Code Online (Sandbox Code Playgroud)

当 UIView_0_2 是第一响应者时:

4.2.1 处理触摸事件

当找到 FirstRessponder 时,就可以使用以下方法处理触摸事件了Responder Chain

响应者链

它是一种chain of responsibility延伸的图案UIResponder。每个UIResponder都有next指向链中下一个响应者的属性

默认情况下:

  • UIView.next-> 超级视图?UIViewController
  • UIViewController.next-> UINavigationController??UIWindow
  • UIWindow.next->UIApplication

摘要图:

从 UIView_0_2 开始打印响应者链:

UIView_0_2 -> UIView_0 -> ViewController -> UIDropShadowView -> UITransitionView -> CustomWindow -> UIWindowScene -> CustomApplication -> AppDelegate
Run Code Online (Sandbox Code Playgroud)

如果有触摸事件:

  • 它从 FirstResponder 开始,并尝试找到覆盖的适当方法(例如touchesBegan()...)如果此方法未被覆盖 -UIResponder.next用于查找下一个响应者并尝试在那里调用此方法
  • 如果您super.touchesBegan()在内部调用override func touchesBegan()-UIResponder.next用于查找下一个响应者并尝试在那里调用此方法

例如,如果第一响应者是 UIView_0_2 但在 UIWindow 中重写了 TouchBegan()(不在 UIView_0_2 中)- UIWindow 将处理此触摸事件

UIWindow hitTest BEGIN
UIWindow point result:true
    UIView_0 hitTest BEGIN
    UIView_0 point result:true
      UIView_0_1 hitTest BEGIN
      UIView_0_1 point result:false
      UIView_0_1 hitTest END result: nil

      UIView_0_2 hitTest BEGIN
      UIView_0_2 point result:true
        UIView_0_2_1 hitTest BEGIN
        UIView_0_2_1 hitTest END result: nil
      UIView_0_2 hitTest END result: UIView_0_2
    UIView_0 hitTest END result: UIView_0_2
UIWindow hitTest END result: UIView_0_2
Run Code Online (Sandbox Code Playgroud)

我们来看一个例子:

响应者链的附加用途

Responder chainUIControl.addTarget()也被事件总线等方法使用UIApplication.sendAction()

customButton.addTarget(nil, action: #selector(CustomApplication.onButton), for: .touchUpInside)
//target is nil. In other cases Responder Chain is not ussed

UIApplication.shared.sendAction(#selector(CustomApplication.foo), to: nil, from: self, for: nil)
Run Code Online (Sandbox Code Playgroud)

它在内部使用UIResponder.target()UIResponder.canPerformAction()。当第一个 UIRessponder 启动流程时 -target()被调用,如果super.target()在内部调用override func target()thencanPerformAction()被调用,如果canPerformAction()返回 false thennext用于查找下一个 UIResponder 并递归地重复这些步骤,如果canPerformAction()返回 true(默认情况下在该对象中找到选择器)然后该目标将递归返回并用于调用选择器

笔记:

  • 当 target 不包含选择器但canPerformAction()返回 true 时,会抛出下一个错误:

无法识别的选择器发送到实例

例如 - 您UIApplication.shared.sendAction(#selector(CustomApplication.foo), to: nil, from: self, for: nil)UIViewControllerand 调用(如您所见)foo选择器位于CustomApplication( UIApplication)

    UIViewController target Begin
    UIViewController canPerformAction result:false
    UIViewController next is: UIWindow
  UIWindow target Begin
  UIWindow canPerformAction result:false
  UIWindow next is UIApplication
UIApplication target begin
UIApplication canPerformAction result:true
UIApplication target result: UIApplication
  UIWindow target result: UIApplication
    UIViewController target result: UIApplication
//foo method body logic
Run Code Online (Sandbox Code Playgroud)

[Android onTouch]