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

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

文件上说,

在命中测试中,一个窗口在视图层次结构的最顶层视图上调用 hitTest:withEvent:; 这个方法通过递归地在返回 YES 的视图层次结构中的每个视图上调用 pointInside:withEvent:,沿着层次结构继续下去,直到它找到发生触摸的界限所在的子视图。该视图成为命中测试视图。

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

85756 次浏览

这似乎是个很基本的问题。但我同意你的看法,这份文件没有其他文件那么清楚,所以我的回答是。

在 UIResponder 中实施 hitTest:withEvent:的作用如下:

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

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

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

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

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

假设你把手指伸进 D,会发生这样的情况:

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

谢谢你的回答,他们帮助我用“叠加”的观点来解决问题。

+----------------------------+
|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的视图;
  2. 如果触摸是在 selfself将被视为潜在的结果。
  3. 递归地检查所有子视图是否命中。如果有,返回它。
  4. 否则根据步骤2的结果返回 self 或 nil。

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

用法:

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

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

希望这对谁有帮助。

就像这个片段!

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

我发现这个 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;
}

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

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

IOS touch

1. User touch
2. event is created
3. hit testing by coordinates - find first responder - UIView and successors (UIWindow)
3.1 hit testing - recursive find the most deep view
3.1.1 point inside - check coordinates
4. Send Touch Event to the First Responder

类图

3点击测试

找到 First Responder

在这种情况下,First Responder是返回 true 的最深 UIViewpoint()(hitTest()在内部使用 point())方法。它总是通过 UIApplication -> UIWindow -> First Responder

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

内部 hitTest()看起来像

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
}

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

让我们来看一个例子

响应链

这是一种 chain of responsibility模式。它由能够处理 UIEventUIResponser组成。在这种情况下,它从覆盖 touch...的第一响应者开始。super.touch...呼叫响应链中的下一个环节

Responder chain也被 addTargetsendAction方法(如事件总线)所使用

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

看看这个例子

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

* 在处理多点触摸时考虑到 isExclusiveTouch

[ Android onTouch ]