为什么UINavigationBar会窃取触摸事件?

时间:2012-01-31 12:58:32

标签: iphone objective-c ios4 uibutton uilabel

我有一个自定义的UIButton,UILabel被添加为子视图。按钮仅在我触摸顶部边界约15个点时执行给定选择器。当我在该区域上方点击时,没有任何反应。

我发现它并非由于按钮和标签的错误创建造成的,因为在我将按钮向下移动约15 px后,它可以正常工作。

更新我忘了说UINavigationBar下面的按钮和按钮上半部分的1/3没有触摸事件。

Image was here

带有4个按钮的视图位于NavigationBar下方。当触摸顶部的“篮球”时,BackButton会触摸事件,当触摸顶部的“Piano”时,则右边的BarButton(如果存在)触摸。如果不存在,则什么也没发生。

我在App文档中找不到这个记录的功能。

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

12 个答案:

答案 0 :(得分:27)

我注意到如果你将userInteractionEnabled设置为OFF,导航栏就不会再“窃取”这些触摸了。

所以你必须继承你的UINavigationBar并在你的CustomNavigationBar中这样做:

-(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的信息,您可以找到here

答案 1 :(得分:16)

我找到了答案here(Apple开发者论坛)。

Keith于2010年5月18日在Apple Developer Technical Support上发布(iPhone OS 3):

  

我建议您避免在导航栏或工具栏附近使用触敏UI。这些区域通常被称为“slop因子”,使得用户更容易在按钮上执行触摸事件而不会难以执行精确触摸。例如,对于UIButton也是如此。

     

但是如果你想在导航栏或工具栏接收之前捕获触摸事件,你可以继承UIWindow并覆盖:       - (void)sendEvent:(UIEvent *)event;

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

CustomWindow.h

@interface CustomWindow: UIWindow 
@end

CustomWindow.m

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

现在当我触摸导航栏下的区域时,没有任何反应。

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

答案 2 :(得分:3)

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

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

答案 3 :(得分:2)

我的解决方案如下:

第一: 添加您的应用程序(无论您输入此代码的位置)都是UINavigationBar的扩展名,如下所示: 以下代码只会在点击navigationBar时发送包含点和事件的通知。

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中添加以下行来收听此通知:

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

然后,您需要在视图控制器中创建方法tapNavigationBar,如下所示:

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中的观察结果:

deinit {
    NotificationCenter.default.removeObserver(self)
}

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

答案 4 :(得分:1)

我只是想分享另一个解决这个问题的前景。这不是设计上的问题,但它旨在帮助用户返回或导航。但我们需要将东西紧紧地放在导航栏中或下方,事情看起来很悲伤。

首先让我们看一下代码。

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

所以这是一种机制,它将使用附近的逻辑来进行第二次测试。如果我们可以知道它的第二次传球然后用第一次击中测试点调用超级。任务完成。

上面的代码完全正确。

答案 5 :(得分:0)

有两件事可能会导致问题。

  1. 您是否尝试setUserInteractionEnabled:NO作为标签。

  2. 我认为可能有用的第二件事是除了那个,在按钮顶部添加标签之后你可以将标签发送回去(它可能有效,但不确定)

    [button sendSubviewToBack:label];

  3. 如果代码有效,请告诉我。)

答案 6 :(得分:0)

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

此外,您还可以选择使用UIButton属性 titleLabel 。也许你以后设置标题,它会进入这个标签,而不是你自己的UILabel。这可以解释为什么文本(属于按钮)可以工作,而标签将覆盖按钮的其余部分(不让水龙头通过)。

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

答案 7 :(得分:0)

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

步骤1.子类UIWindow

@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步:使用按钮为touchesEnded:widthEvent:实施view,例如:

- (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步:当我们关注的视图出现时调用ChunyuWindow的{​​{1}},并在ViewControllers的viewDidAppear / viewDidDisappear / dealloc中,当视图消失或dealloc时调用addViewForTouchPriority,因此removeViewForTouchPriority中的_touchView为NULL,与ChunyuWindow相同,没有副作用。

答案 8 :(得分:0)

这解决了我的问题..

我在我的navbar子类中添加了hitTest:withEvent:代码..

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

答案 9 :(得分:0)

基于Alexandar提供的答案,另一种对我有用的解决方案:

self.navigationController?.barHideOnTapGestureRecognizer.enabled = false

您可以停用负责&#34; slop区域的手势识别器,而不是覆盖UIWindow。在UINavigationBar

答案 10 :(得分:0)

Give a extension version according to Bart Whiteley. No need to subclass.

@implementation UINavigationBar(Xxxxxx)

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

@end

答案 11 :(得分:0)

以下对我有用:

self.navigationController?.isNavigationBarHidden = true