虽然大多数苹果文档都写得很好,但我认为“Event Handling Guide for iOS”是个例外。我很难清楚地理解那里描述的内容。
文件说,
在命中测试中,窗口在视图层次结构的最顶层视图上调用 hitTest:withEvent:;此方法通过在视图层次结构中返回 YES 的每个视图上递归调用 pointInside:withEvent: 来继续,沿着层次结构向下进行,直到找到在其范围内发生触摸的子视图。该视图成为命中测试视图。
是不是只有最顶层视图的 hitTest:withEvent:
被系统调用,它调用所有子视图的 pointInside:withEvent:
,如果特定子视图的返回为 YES,则调用该子视图的 pointInside:withEvent:
子类?
我认为您将子类化与视图层次结构混淆了。医生说的内容如下。假设你有这个视图层次结构。通过层次结构我不是在谈论类层次结构,而是在视图层次结构中的视图,如下所示:
+----------------------------+
|A |
|+--------+ +------------+ |
||B | |C | |
|| | |+----------+| |
|+--------+ ||D || |
| |+----------+| |
| +------------+ |
+----------------------------+
假设您将手指放在 D
内。以下是将会发生的事情:
hitTest:withEvent: 在视图层次结构的最顶层视图 A 上调用。 pointInside:withEvent: 在每个视图上递归调用。 pointInside:withEvent: 在 A 上调用,返回 YES pointInside:withEvent: 在 B 上调用,返回 NO pointInside:withEvent: 在 C 上调用,返回 YES pointInside:withEvent: 在 D 上调用,返回 YES返回 YES 的视图,它将向下查看层次结构以查看发生触摸的子视图。在这种情况下,从 A、C 和 D 中,将是 D。D 将是命中测试视图
这似乎是一个非常基本的问题。但我同意你的观点,该文件不像其他文件那样清晰,所以这是我的回答。
UIResponder 中 hitTest:withEvent:
的实现执行以下操作:
它调用 pointInside:withEvent: of self
如果返回为 NO,hitTest:withEvent: 返回 nil。故事的结局。
如果返回是 YES,它会发送 hitTest:withEvent: 消息到它的子视图。它从顶级子视图开始,并继续到其他视图,直到子视图返回非零对象,或者所有子视图都收到消息。
如果子视图第一次返回非零对象,则第一个 hitTest:withEvent: 返回该对象。故事的结局。
如果没有子视图返回非 nil 对象,则第一个 hitTest:withEvent: 返回 self
这个过程递归地重复,所以通常最终返回视图层次结构的叶视图。
但是,您可以覆盖 hitTest:withEvent
以执行不同的操作。在许多情况下,覆盖 pointInside:withEvent:
更简单,并且仍然提供足够的选项来调整应用程序中的事件处理。
hitTest:withEvent:
最终都会执行吗?
hitTest:withEvent:
(如果需要,还可以覆盖 pointInside
),打印日志并调用 [super hitTest...
以找出谁的 hitTest:withEvent:
以何种顺序被调用。
我觉得这个Hit-Testing in iOS很有帮助
https://i.stack.imgur.com/FL27E.png
- (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
}
感谢您的回答,他们帮助我解决了“覆盖”视图的情况。
+----------------------------+
|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;
}
我们不应该为隐藏或透明视图发送触摸事件,或者 userInteractionEnabled 设置为 NO 的视图;如果触摸在自我内部,自我将被视为潜在结果。递归检查所有子视图是否命中。如果有,请退回。否则根据步骤 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 也提供了一些很好的插图。
希望这可以帮助某人。
它显示像这个片段!
- (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 触控
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
类图
命中测试
查找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
}
向第一响应者发送触摸事件
//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?)
我们来看一个例子
https://i.stack.imgur.com/FvbjU.png
响应者链
它是一种 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
@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)
}
hitTest:withEvent:
也被调用。如果 D 是 C 的子视图,而不是 A,会发生什么?我觉得我糊涂了……A
不会像C
和D
一样返回YES
吗?