iOS 的事件处理 - hitTest:withEvent: 和 pointInside:withEvent: 是如何相关的?
Posted
技术标签:
【中文标题】iOS 的事件处理 - hitTest:withEvent: 和 pointInside:withEvent: 是如何相关的?【英文标题】:Event handling for iOS - how hitTest:withEvent: and pointInside:withEvent: are related? 【发布时间】:2011-06-25 02:04:36 【问题描述】:虽然大多数苹果文档都写得很好,但我认为“Event Handling Guide for ios”是个例外。我很难清楚地理解那里描述的内容。
文件说,
在命中测试中,一个窗口在视图层次结构的最顶层视图上调用
hitTest:withEvent:
;此方法通过在返回 YES 的视图层次结构中的每个视图上递归调用pointInside:withEvent:
来继续,沿着层次结构向下进行,直到它找到发生触摸的边界内的子视图。该视图成为命中测试视图。
是不是只有最顶层视图的hitTest:withEvent:
被系统调用,系统调用所有子视图的pointInside:withEvent:
,如果从特定子视图返回YES,则调用pointInside:withEvent:
该子视图的子类?
【问题讨论】:
一个非常好的教程,帮助了我link 与此等效的较新文档现在可能是 developer.apple.com/documentation/uikit/uiview/1622469-hittest 【参考方案1】:我认为您将子类化与视图层次结构混淆了。医生说的内容如下。假设你有这个视图层次结构。这里所说的层次不是类层次,而是视图层次中的视图,如下:
+----------------------------+
|A |
|+--------+ +------------+ |
||B | |C | |
|| | |+----------+| |
|+--------+ ||D || |
| |+----------+| |
| +------------+ |
+----------------------------+
假设您将手指放入D
。以下是将会发生的事情:
hitTest:withEvent:
在视图层次结构的最顶层视图 A
上调用。
pointInside:withEvent:
在每个视图上递归调用。
-
在
A
上调用pointInside:withEvent:
,并返回YES
在B
上调用pointInside:withEvent:
,并返回NO
在C
上调用pointInside:withEvent:
,并返回YES
在D
上调用pointInside:withEvent:
,并返回YES
YES
的视图上,它将向下查看层次结构以查看发生触摸的子视图。在这种情况下,从A
、C
和D
,它将是D
。
D
将是命中测试视图
【讨论】:
感谢您的回答。你描述的也是我的想法,但@MHC 说 B、C 和 D 的hitTest:withEvent:
也被调用。如果 D 是 C 的子视图,而不是 A,会发生什么?我想我很困惑......
在我的图中,D是C的子视图。
A
不会像 C
和 D
一样返回 YES
吗?
不要忘记不可见的视图(通过 .hidden 或低于 0.1 的不透明度)或关闭用户交互的视图将永远不会响应 hitTest。我认为首先没有在这些对象上调用 hitTest。
只是想添加 hitTest:withEvent: 可能会根据其层次结构在所有视图上调用。【参考方案2】:
这似乎是一个非常基本的问题。但我同意你的观点,该文件不像其他文件那样清晰,所以这是我的回答。
在 UIResponder 中 hitTest:withEvent:
的实现做了以下事情:
self
的pointInside:withEvent:
如果返回为 NO,hitTest:withEvent:
将返回 nil
。故事的结局。
如果返回是,它会发送hitTest:withEvent:
消息到它的子视图。
它从***子视图开始,并继续到其他视图,直到一个子视图
返回非nil
对象,或者所有子视图都收到消息。
如果子视图第一次返回非nil
对象,则第一个hitTest:withEvent:
返回该对象。故事的结局。
如果没有子视图返回非nil
对象,则第一个hitTest:withEvent:
返回self
这个过程递归地重复,所以通常最终返回视图层次结构的叶子视图。
但是,您可以覆盖 hitTest:withEvent
以执行不同的操作。在许多情况下,覆盖 pointInside:withEvent:
更简单,并且仍然提供足够的选项来调整应用程序中的事件处理。
【讨论】:
你的意思是hitTest:withEvent:
的所有子视图最终都被执行了吗?
是的。只需在您的视图中覆盖hitTest:withEvent:
(如果需要,还可以覆盖pointInside
),打印日志并调用[super hitTest...
以找出谁的hitTest:withEvent:
以何种顺序被调用。
不应该在第 3 步中提到“如果返回是 YES,它会发送 hitTest:withEvent: ...不应该是 pointInside:withEvent 吗?我认为它会将 pointInside 发送到所有子视图?
早在 2 月,它首先发送了 hitTest:withEvent:,其中一个 pointInside:withEvent: 被发送给了它自己。我没有使用以下 SDK 版本重新检查此行为,但我认为发送 hitTest:withEvent: 更有意义,因为它提供了对事件是否属于视图的更高级别的控制; pointInside:withEvent: 告诉事件位置是否在视图上,而不是事件是否属于视图。例如,子视图可能不想处理事件,即使它的位置在子视图上。
WWDC2014 Session 235 - Advanced Scrollviews and Touch handling Techniques 为这个问题提供了很好的解释和示例。【参考方案3】:
我觉得这个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
【讨论】:
所以你需要将它添加到 UIView 的子类中,并让你的层次结构中的所有视图都继承自它?【参考方案4】:感谢您的回答,他们帮助我解决了“覆盖”视图的问题。
+----------------------------+
|A +--------+ |
| |B +------------------+ |
| | |C X | |
| | +------------------+ |
| | | |
| +--------+ |
| |
+----------------------------+
假设X
- 用户的触摸。 pointInside:withEvent:
on 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;
-
我们不应该为隐藏或透明视图发送触摸事件,或者将
userInteractionEnabled
设置为NO
的视图;
如果触摸在self
内部,self
将被视为潜在结果。
递归检查所有子视图是否命中。如果有,请退回。
根据第 2 步的结果返回 self 或 nil。
注意,[self.subviewsreverseObjectEnumerator]
需要从上到下遵循视图层次结构。并检查clipsToBounds
以确保不测试被屏蔽的子视图。
用法:
-
在您的子类视图中导入类别。
用这个替换
hitTest:withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
return [self overlapHitTest:point withEvent:event];
Official Apple's Guide 也提供了一些很好的插图。
希望这对某人有所帮助。
【讨论】:
太棒了!感谢清晰的逻辑和出色的代码 sn-p,解决了我的难题! @Lion,很好的答案。您也可以在第一步检查相等性以清除颜色。【参考方案5】:它显示像这样 sn-p!
- (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;
【讨论】:
我发现这是最容易理解的答案,它与我对实际行为的观察非常吻合。唯一的区别是子视图以相反的顺序枚举,因此靠近前面的子视图优先接收触摸而不是它们后面的兄弟。 @DouglasHill 感谢您的更正。最好的问候【参考方案6】:@lion 的 sn-p 就像一个魅力。我将它移植到 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)
【讨论】:
【参考方案7】: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
在这种情况下是最深的UIView
point()
(hitTest()
在内部使用point()
) 方法,该方法返回true。它总是经过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
4 发送触摸事件到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
模式。它由可以处理UIEvent
的UIResponser
组成。在这种情况下,它从覆盖touch...
的第一响应者开始。 super.touch...
调用响应链中的下一个链接
Responder chain
也被 addTarget
或 sendAction
使用,例如事件总线
//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]
【讨论】:
以上是关于iOS 的事件处理 - hitTest:withEvent: 和 pointInside:withEvent: 是如何相关的?的主要内容,如果未能解决你的问题,请参考以下文章