关于iPhone:为什么UINavigationBar会窃取触摸事件?

Why does UINavigationBar steal touch events?

我有一个添加了UILabel作为子视图的自定义UIButton。 仅当我触摸选择器约15%的上限时,按钮才会执行给定选择器。 当我在该区域上方点击时,什么也没有发生。

我发现它不是由错误创建按钮和标签引起的,因为在我将按钮降低到大约15像素后,它可以正常工作。

更新我忘了说位于UINavigationBar下的按钮和按钮上部的1/3不会发生触摸事件。

图片在这里

带4个按钮的视图位于NavigationBar下。 而当触摸顶部的"篮球"时,BackButton将获得触摸事件,而当触摸顶部的"钢琴"时,则rightBarButton(如果存在)将得到触摸。 如果不存在,则什么也没有发生。

我没有在App文档中找到此文档化功能。

我也发现了与我的问题有关的主题,但也没有答案。


我注意到,如果将userInteractionEnabled设置为OFF,则NavigationBar不再"窃取"触摸。

因此,您必须继承UINavigationBar的子类,并在CustomNavigationBar中执行以下操作:

1
2
3
4
5
6
7
8
9
10
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if ([self pointInside:point withEvent:event]) {
        self.userInteractionEnabled = YES;
    } else {
        self.userInteractionEnabled = NO;
    }

    return [super hitTest:point withEvent:event];
}

有关如何子类化UINavigationBar的信息,您可以在这里找到。


我在这里找到了答案(苹果开发者论坛)。

Keith在2010年5月18日的Apple开发人员技术支持(iPhone OS 3)中:

I recommend that you avoid having touch-sensitive UI in such close proximity to the nav bar or toolbar. These areas are typically known as"slop factors" making it easier for users to perform touch events on buttons without the difficulty of performing precision touches. This is also the case for UIButtons for example.

But if you want to capture the touch event before the navigation bar or toolbar receives it, you can subclass UIWindow and override:
-(void)sendEvent:(UIEvent *)event;

我还发现,当我触摸UINavigationBar下的区域时,location.y定义为64,尽管不是。
所以我做了这个:

CustomWindow.h

1
2
@interface CustomWindow: UIWindow
@end

CustomWindow.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@implementation CustomWindow
- (void) sendEvent:(UIEvent *)event
{      
  BOOL flag = YES;
  switch ([event type])
  {
   case UIEventTypeTouches:
        //[self catchUIEventTypeTouches: event]; perform if you need to do something with event        
        for (UITouch *touch in [event allTouches]) {
          if ([touch phase] == UITouchPhaseBegan) {
            for (int i=0; i<[self.subviews count]; i++) {
                //GET THE FINGER LOCATION ON THE SCREEN
                CGPoint location = [touch locationInView:[self.subviews objectAtIndex:i]];

                //REPORT THE TOUCH
                NSLog(@"[%@] touchesBegan (%i,%i)",  [[self.subviews objectAtIndex:i] class],(NSInteger) location.x, (NSInteger) location.y);
                if (((NSInteger)location.y) == 64) {
                    flag = NO;
                }
             }
           }  
        }

        break;      

   default:
        break;
  }
  if(!flag) return; //to do nothing

    /*IMPORTANT*/[super sendEvent:(UIEvent *)event];/*IMPORTANT*/
}

@end

在AppDelegate类中,我使用CustomWindow而不是UIWindow。

现在,当我触摸导航栏下方的区域时,什么也没有发生。

我的按钮仍然没有触摸事件,因为我不知道如何使用按钮将事件(以及更改坐标)发送到我的视图。


子类化UINavigationBar并添加此方法。除非点击子视图(例如按钮),否则它将导致点击通过。

1
2
3
4
5
 -(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
 {
    UIView *v = [super hitTest:point withEvent:event];
    return v == self? nil: v;
 }


对我来说,解决方法如下:

第一:
在您的应用程序中(无论在什么位置输入此代码)添加UINavigationBar的扩展名,如下所示:
下面的代码只是在点按navigationBar时使用点和事件调度通知。

1
2
3
4
5
6
extension UINavigationBar {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    NotificationCenter.default.post(name: NSNotification.Name(rawValue:"tapNavigationBar"), object: nil, userInfo: ["point": point,"event": event as Any])
    return super.hitTest(point, with: event)
    }
}

然后,在特定的视图控制器中,您必须通过在viewDidLoad中添加以下行来收听此通知:

1
NotificationCenter.default.addObserver(self, selector: #selector(tapNavigationBar), name: NSNotification.Name(rawValue:"tapNavigationBar"), object: nil)

然后,您需要像这样在视图控制器中创建方法tapNavigationBar

1
2
3
4
5
6
7
8
9
10
func tapNavigationBar(notification: Notification) {
    let pointOpt = notification.userInfo?["point"] as? CGPoint
    let eventOpt = notification.userInfo?["event"] as? UIEvent?
    guard let point = pointOpt, let event = eventOpt else { return }

    let convertedPoint = YOUR_VIEW_BEHIND_THE_NAVBAR.convert(point, from: self.navigationController?.navigationBar)
    if YOUR_VIEW_BEHIND_THE_NAVBAR.point(inside: convertedPoint, with: event) {
        //Dispatch whatever you wanted at the first place.
    }
}

PD:不要忘了删除deinit中的观察值,如下所示:

1
2
3
deinit {
    NotificationCenter.default.removeObserver(self)
}

就是这样...有点"棘手",但这是一个不错的解决方法,它可以在点击navigationBar时不进行子类化和获取通知。


我只是想分享另一个解决此问题的前景。这不是设计问题,而是旨在帮助用户返回或导航。但是,我们需要将内容紧紧放在导航栏内或下方,否则情况看起来很难过。

首先让我们看一下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyNavigationBar: UINavigationBar {
private var secondTap = false
private var firstTapPoint = CGPointZero

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    if !self.secondTap{
        self.firstTapPoint = point
    }

    defer{
        self.secondTap = !self.secondTap
    }

    return  super.pointInside(firstTapPoint, withEvent: event)
}

}

您可能会明白为什么我要进行二次触摸处理。有解决方案。

命中测试两次被调用。第一次报告窗口上的实际点。一切顺利。在第二遍,这发生了。

如果系统看到导航栏,并且击点在Y侧多了约9个像素,它将尝试将其逐渐减小到导航栏所在的44点以下。

看一下屏幕是否清晰。

enter image description here

因此,有一种机制将在命中测试的第二遍使用附近的逻辑。如果我们可以知道它的第二次通过,然后通过第一个命中测试点调用超级。任务完成。

上面的代码正是这样做的。


以下为我工作:

1
self.navigationController?.isNavigationBarHidden = true

根据Bart Whiteley提供扩展版本。无需子类化。

1
2
3
4
5
6
7
8
9
@implementation UINavigationBar(Xxxxxx)

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *v = [super hitTest:point withEvent:event];
    return v == self ? nil: v;
}

@end


根据Alexandar提供的答案,一种对我有用的替代解决方案:

1
self.navigationController?.barHideOnTapGestureRecognizer.enabled = false

除了覆盖UIWindow,您还可以仅禁用负责UINavigationBar上"倾斜区域"的手势识别器。


这解决了我的问题。

我在我的导航栏子类中添加了hitTest:withEvent:代码。

1
2
3
4
5
6
7
8
9
10
11
12
 -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        int errorMargin = 5;// space left to decrease the click event area
        CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height);
        BOOL isTouchAllowed =  (CGRectContainsPoint(smallerFrame, point) == 1);

        if (isTouchAllowed) {
            self.userInteractionEnabled = YES;
        } else {
            self.userInteractionEnabled = NO;
        }
        return [super hitTest:point withEvent:event];
    }

扩展亚历山大的解决方案:

步骤1.子类UIWindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
@interface ChunyuWindow : UIWindow {
    NSMutableArray * _views;

@private
    UIView *_touchView;
}

- (void)addViewForTouchPriority:(UIView*)view;
- (void)removeViewForTouchPriority:(UIView*)view;

@end
// .m File
// #import"ChunyuWindow.h"

@implementation ChunyuWindow
- (void) dealloc {
    TT_RELEASE_SAFELY(_views);
    [super dealloc];
}


- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
    if (UIEventSubtypeMotionShake == motion
        && [TTNavigator navigator].supportsShakeToReload) {
        // If you're going to use a custom navigator implementation, you need to ensure that you
        // implement the reload method. If you're inheriting from TTNavigator, then you're fine.
        TTDASSERT([[TTNavigator navigator] respondsToSelector:@selector(reload)]);
        [(TTNavigator*)[TTNavigator navigator] reload];
    }
}

- (void)addViewForTouchPriority:(UIView*)view {
    if ( !_views ) {
        _views = [[NSMutableArray alloc] init];
    }
    if (![_views containsObject: view]) {
        [_views addObject:view];
    }
}

- (void)removeViewForTouchPriority:(UIView*)view {
    if ( !_views ) {
        return;
    }

    if ([_views containsObject: view]) {
        [_views removeObject:view];
    }
}

- (void)sendEvent:(UIEvent *)event {
    if ( !_views || _views.count == 0 ) {
        [super sendEvent:event];
        return;
    }

    UITouch *touch = [[event allTouches] anyObject];
    switch (touch.phase) {
        case UITouchPhaseBegan: {
            for ( UIView *view in _views ) {
                if ( CGRectContainsPoint(view.frame, [touch locationInView:[view superview]]) ) {
                    _touchView = view;
                    [_touchView touchesBegan:[event allTouches] withEvent:event];
                    return;
                }
            }
            break;
        }
        case UITouchPhaseMoved: {
            if ( _touchView ) {
                [_touchView touchesMoved:[event allTouches] withEvent:event];
                return;
            }
            break;
        }
        case UITouchPhaseCancelled: {
            if ( _touchView ) {
                [_touchView touchesCancelled:[event allTouches] withEvent:event];
                _touchView = nil;
                return;
            }
            break;
        }
        case UITouchPhaseEnded: {
            if ( _touchView ) {
                [_touchView touchesEnded:[event allTouches] withEvent:event];
                _touchView = nil;
                return;
            }
            break;
        }

        default: {
            break;
        }
    }

    [super sendEvent:event];
}

@end

步骤2:将ChunyuWindow实例分配给AppDelegate实例

步骤3:使用按钮为view实现touchesEnded:widthEvent:,例如:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded: touches withEvent: event];

    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView: _buttonsView]; // a subview contains buttons
    for (UIButton* button in _buttons) {
        if (CGRectContainsPoint(button.frame, point)) {
            [self onTabButtonClicked: button];
            break;
        }
    }    
}

步骤4:在我们关注的视图出现时,调用ChunyuWindowaddViewForTouchPriority,在视图控制器的viewDidAppear / viewDidDisappear / dealloc中,当视图消失或取消分配时,调用removeViewForTouchPriority,因此ChunyuWindow中的_touchView为NULL,与UIWindow相同,没有副作用。


您的标签很大。它们从{0,0}(按钮的左上角)开始,在按钮的整个宽度上延伸,并具有整个视图的高度。检查您的frame数据,然后重试。

同样,您可以选择使用UIButton属性titleLabel。也许您稍后要设置标题,它会进入该标签,而不是您自己的UILabel。这将解释为什么文本(属于按钮)将起作用,而标签将覆盖按钮的其余部分(不允许水龙头通过)的原因。

titleLabel是只读属性,但是您可以自定义它,就像自己的标签(可能是frame除外)一样,包括文本颜色,字体,阴影等。


有两件事可能会引起问题。

  • 您是否尝试使用setUserInteractionEnabled:NO作为标签。

  • 我认为可能有用的第二件事是,除了在按钮顶部添加标签之后,您还可以将标签发送回去(虽然可能,但不确定)

    [button sendSubviewToBack:label];

  • 请让我知道代码是否有效:)