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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4961386/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 18:51:51  来源:igfitidea点击:

Event handling for iOS - how hitTest:withEvent: and pointInside:withEvent: are related?

iosuiviewuikit

提问by realstuff02

While most apple documents are very well written, I think 'Event Handling Guide for iOS' is an exception. It's hard for me to clearly understand what's been described there.

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

The document says,

文件说,

In hit-testing, a window calls hitTest:withEvent:on the top-most view of the view hierarchy; this method proceeds by recursively calling pointInside:withEvent:on each view in the view hierarchy that returns YES, proceeding down the hierarchy until it finds the subview within whose bounds the touch took place. That view becomes the hit-test view.

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

So is it like that only hitTest:withEvent:of the top-most view is called by the system, which calls pointInside:withEvent:of all of subviews, and if the return from a specific subview is YES, then calls pointInside:withEvent:of that subview's subclasses?

那么是不是系统只hitTest:withEvent:调用最顶层的视图,调用pointInside:withEvent:所有的子视图,如果某个子视图的返回值为YES,则调用pointInside:withEvent:该子视图的子类?

回答by pgb

I think you are confusing subclassing with the view hierarchy. What the doc says is as follows. Say you have this view hierarchy. By hierarchy I'm not talking about class hierarchy, but views within views hierarchy, as follows:

我认为您将子类与视图层次结构混淆了。医生说的内容如下。假设您有这个视图层次结构。按层次结构我不是在谈论类层次结构,而是在视图层次结构中的视图,如下所示:

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

Say you put your finger inside D. Here's what will happen:

假设你把你的手指放在里面D。下面是会发生的事情:

  1. hitTest:withEvent:is called on A, the top-most view of the view hierarchy.
  2. pointInside:withEvent:is called recursively on each view.
    1. pointInside:withEvent:is called on A, and returns YES
    2. pointInside:withEvent:is called on B, and returns NO
    3. pointInside:withEvent:is called on C, and returns YES
    4. pointInside:withEvent:is called on D, and returns YES
  3. On the views that returned YES, it will look down on the hierarchy to see the subview where the touch took place. In this case, from A, Cand D, it will be D.
  4. Dwill be the hit-test view
  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,它将向下查看层次结构以查看发生触摸的子视图。在这种情况下,从ACD,它将是D
  4. D将是命中测试视图

回答by MHC

It seems quite a basic question. But I agree with you the document is not as clear as other documents, so here is my answer.

这似乎是一个非常基本的问题。但我同意你的意见,该文件不像其他文件那样清晰,所以这是我的回答。

The implementation of hitTest:withEvent:in UIResponder does the following:

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

  • It calls pointInside:withEvent:of self
  • If the return is NO, hitTest:withEvent:returns nil. the end of the story.
  • If the return is YES, it sends hitTest:withEvent:messages to its subviews. it starts from the top-level subview, and continues to other views until a subview returns a non-nilobject, or all subviews receive the message.
  • If a subview returns a non-nilobject in the first time, the first hitTest:withEvent:returns that object. the end of the story.
  • If no subview returns a non-nilobject, the first hitTest:withEvent:returns self
  • 它调用pointInside:withEvent:self
  • 如果返回为 NO,则hitTest:withEvent:返回nil。故事的结局。
  • 如果返回是 YES,它会向hitTest:withEvent:它的子视图发送消息。它从顶层子视图开始,一直到其他视图,直到一个子视图返回一个非nil对象,或者所有子视图都收到消息。
  • 如果子视图nil第一次返回非对象,则第一个hitTest:withEvent:返回该对象。故事的结局。
  • 如果没有子视图返回非nil对象,则第一个hitTest:withEvent:返回self

This process repeats recursively, so normally the leaf view of the view hierarchy is returned eventually.

这个过程递归地重复,所以通常最终返回视图层次结构的叶视图。

However, you might override hitTest:withEventto do something differently. In many cases, overriding pointInside:withEvent:is simpler and still provides enough options to tweak event handling in your application.

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

回答by onmyway133

I find this Hit-Testing in iOSto be very helpful

我发现iOS 中的命中测试非常有用

enter image description here

在此处输入图片说明

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

Edit Swift 4:

编辑斯威夫特 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
}

回答by Lion

Thanks for answers, they helped me to solve situation with "overlay" views.

感谢您的解答,他们帮我解决的情况与“叠加”的观点。

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

Assume X- user's touch. pointInside:withEvent:on Breturns NO, so hitTest:withEvent:returns A. I wrote category on UIViewto handle issue when you need to receive touch on top most visibleview.

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

- (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. We should not send touch events for hidden or transparent views, or views with userInteractionEnabledset to NO;
  2. If touch is inside self, selfwill be considered as potential result.
  3. Check recursively all subviews for hit. If any, return it.
  4. Else return self or nil depending on result from step 2.
  1. 我们不应该为隐藏或透明视图或userInteractionEnabled设置为 的视图发送触摸事件NO
  2. 如果触摸在里面selfself将被视为潜在的结果。
  3. 递归检查所有子视图是否命中。有的话,退货。
  4. 否则根据第 2 步的结果返回 self 或 nil。

Note, [self.subviewsreverseObjectEnumerator]needed to follow view hierarchy from top most to bottom. And check for clipsToBoundsto ensure not to test masked subviews.

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

Usage:

用法:

  1. Import category in your subclassed view.
  2. Replace hitTest:withEvent:with this
  1. 在子类视图中导入类别。
  2. hitTest:withEvent:用这个替换
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Official Apple's Guideprovides some good illustrations too.

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

Hope this helps somebody.

希望这可以帮助某人。

回答by hippo

It shows like this snippet!

它显示像这个片段!

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

回答by mortadelo

The snippet of @lion works like a charm. I ported it to swift 2.1 and used it as an extension to UIView. I'm posting it here in case somebody needs it.

@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
    }
}

To use it, just override hitTest:point:withEvent in your uiview as follows:

要使用它,只需在您的 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)
}

回答by yoAlex5

Class diagram

类图

Hit Testing

命中测试

Find a First Responder

找到 First Responder

First Responderin this case is the deepest UIViewpoint()method of which returned true

First Responder在这种情况下是UIViewpoint()返回 true的最深方法

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

Internally hitTest()looks like

内部hitTest()看起来像

hitTest() {

    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

    for subview in subviews {
        if subview.hitTest() != nil {
            return subview
        }
    }

    return nil

}

Send Touch Event to the First Responder

将触摸事件发送到 First Responder

//UIApplication.shared.sendEvent()

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

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

Let's take a look at example

我们来看一个例子

Responder Chain

响应链

//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

Take a look at example

看个例子

class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc
    func foo() {
        //this method is called using Responder Chain
        print("foo") //foo
    }
}

class ViewController: UIViewController {
    func send() {
        UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
    }
}

[Android onTouch]

[Android onTouch]