iOS的事件处理-hitTest:withEvent:和pointInside:withEvent:有关系吗

bttbmeg0  于 2023-10-21  发布在  iOS
关注(0)|答案(7)|浏览(106)

虽然大多数苹果文档都写得很好,但我认为《iOS事件处理指南》是个例外。我很难清楚地理解那里所描述的内容。
文件说,
在命中测试中,窗口在视图层次结构的最顶层视图上调用hitTest:withEvent:;这个方法通过在返回YES的视图层次结构中的每个视图上递归地调用pointInside:withEvent:来继续,沿着层次结构继续,直到它找到在其边界内发生触摸的子视图。该视图成为命中测试视图。
那么,是不是只有最顶层视图的hitTest:withEvent:被系统调用,它调用所有子视图的pointInside:withEvent:,如果从特定子视图返回的是YES,那么调用该子视图的子类的pointInside:withEvent:

uqdfh47h

uqdfh47h1#

我认为你混淆了子类和视图层次结构。医生说的话如下。假设你有这个视图层次结构。通过层次结构,我不是在谈论类层次结构,而是在视图层次结构中的视图,如下所示:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

假设你把手指放在D里面。接下来会发生什么

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

kiz8lqtg2#

这似乎是一个很基本的问题。但我同意你的观点,这份文件不像其他文件那么清楚,所以我的答案是这样的。
hitTest:withEvent:在UIResponder中的实现如下:

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

这个过程递归地重复,所以通常最终返回视图层次结构的叶视图。
但是,您可以重写hitTest:withEvent以执行不同的操作。在许多情况下,重写pointInside:withEvent:更简单,并且仍然提供足够的选项来调整应用程序中的事件处理。

rseugnpd

rseugnpd3#

我觉得这个Hit-Testing in iOS很有帮助

- (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;
}

编辑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
}
iszxjhcz

iszxjhcz4#

感谢您的回答,他们帮助我解决了“叠加”视图的情况。

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

假设X-用户的触摸。B上的pointInside:withEvent:返回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;
}

1.我们不应该发送隐藏或透明视图的触摸事件,或者将userInteractionEnabled设置为NO的视图;
1.如果触摸在self内,则self将被视为潜在结果。
1.递归检查所有子视图是否命中。如果有的话,把它还回去。
1.否则返回self或nil,取决于步骤2的结果。
注意,[self.subviewsreverseObjectEnumerator]需要遵循从顶部到底部的视图层次结构。并检查clipsToBounds以确保不测试掩码子视图。
使用方法:
1.在子类视图中导入类别。
1.将hitTest:withEvent:替换为

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

苹果官方指南也提供了一些很好的插图。
希望这对某人有帮助。

flseospp

flseospp5#

就像这个片段!

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
    {
        return nil;
    }

    if (![self pointInside:point withEvent:event])
    {
        return nil;
    }

    __block UIView *hitView = self;

    [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {   

        CGPoint thePoint = [self convertPoint:point toView:obj];

        UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];

        if (theSubHitView != nil)
        {
            hitView = theSubHitView;

            *stop = YES;
        }

    }];

    return hitView;
}
3mpgtkmj

3mpgtkmj6#

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

类图

3命中测试

点击测试以找到第一响应者-检查UIView及其后继者的层次结构(例如,从最大的(后)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

内部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
}

备注:

  • 默认情况下,当你设置view.isUserInteractionEnabled = false这个视图和它的所有子视图将不处理触摸事件
  • 默认情况下,point()会考虑放置在超级视图中的矩形。这意味着如果一个触摸发生在一个视图部分,这是绘制出的superview,这个视图和它的所有子视图将不会处理触摸事件

UIView.clipsToBounds(https://stackoverflow.com/a/73513083/4770877)

4发送UI事件

UIKit创建UIEvent,由UIApplication.shared.sendEvent()发送到main event loopAbout(https://stackoverflow.com/a/75204210/4770877)。UIEvent contains one or more触摸which contains - UIView,位置. 它总是通过UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>`

UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>.touchesBegan()
UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>.touchesEnded()
//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)

4.1将Touch Event发送到UIGestureRecognizer

这是一种简单而方便的手势操作方法。有一些开箱即用的UIGestureRecognizerUITapGestureRecognizerUISwipeGestureRecognizer.你就能创造出你自己的

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")
}

系统尝试在视图层次结构中查找UIGestureRecognizer。从第一个响应者(使用UIView.superview)开始,直到UIWindowUIView的子类)。这意味着如果您在UIWindow上设置UIGestureRecognizer,并且第一响应者是UIView_0而没有UIGestureRecognizer-UIWindow.UIGestureRecognizer将被调用
UIGestureRecognizerDelegate中有一些函数可以处理UIGestureRecognizer之间的工作
还有一些函数来处理转发事件到响应器链,比如:cancelsTouchesInViewdelaysTouchesBegandelaysTouchesEnded

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?)

当UIView_0_2是第一响应者时:

4.2.1处理触摸事件

找到FirstResponder时,是时候使用Responder Chain处理触摸事件了

响应链

它是一种对UIResponder进行扩展的chain of responsibility模式。每个UIResponder都有next属性,指向链中的下一个响应者
默认情况下:

  • UIView.next -> superview??UIViewController
  • UIViewController.next -> UINavigationController??UIWindow
  • UIWindow.next -> UIApplication

摘要图:

从UIView_0_2开始打印响应器链:

UIView_0_2 -> UIView_0 -> ViewController -> UIDropShadowView -> UITransitionView -> CustomWindow -> UIWindowScene -> CustomApplication -> AppDelegate

如果发生触摸事件:

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

例如,如果第一个响应者是UIView_0_2,但在UIWindow中覆盖了touchesBegan()(而不是在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

让我们来看看一个例子:

Responder Chain的其他用法

Responder chain也用于UIControl.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)

内部使用UIResponder.target()UIResponder.canPerformAction()。当第一个UIResponder开始一个流时-target()被调用,如果override func target()内部调用super.target(),则canPerformAction()被调用,如果canPerformAction()返回false,则next用于查找下一个UIResponder并递归地重复这些步骤,如果canPerformAction()返回true(默认情况下,选择器在此对象中找到),则此目标将递归返回,并用于调用选择器
备注:

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

发送到示例的无法识别的选择器
例如,您从UIViewController调用UIApplication.shared.sendAction(#selector(CustomApplication.foo), to: nil, from: self, for: nil),并且(如您所见)foo选择器位于CustomApplicationUIApplication)中

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

Android onTouch(https://stackoverflow.com/a/57222691/4770877)

qqrboqgw

qqrboqgw7#

@lion的片段就像一种魅力。我把它移植到Swift 2.1,并把它作为UIView的扩展。我把它贴在这里以防有人需要。

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}

要使用它,只需在uiview中覆盖 hitTest:point:withEvent,如下所示:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}

相关问题