【问题标题】:Why does UINavigationBar steal touch events?为什么 UINavigationBar 会窃取触摸事件?
【发布时间】:2012-02-23 04:38:54
【问题描述】:

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

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

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

Image was here

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

我没有在应用文档中找到这个记录在案的功能。

我也找到了与我的问题相关的this主题,但也没有答案。

【问题讨论】:

标签: iphone objective-c ios4 uibutton uilabel


【解决方案1】:

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

所以你必须继承你的 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

【讨论】:

  • 禁用用户交互似乎在我的设备上工作。我正在使用 iPhone 5 和 iOS 6.1.2 进行测试。不过,子类化不是必需的;您可以在外部禁用用户交互。
  • 非常好的解决方案,因为我无法禁用用户交互(有一个按钮),谢谢。
  • 这个解决方案对我造成了一个错误,有时当导航返回时,只有导航栏动画返回,但视图没有被删除或变黑。
  • 在模拟器和设备上与 iOS 9.3 完美配合。我创建了一个 UINavigationBar 子类并直接在 IB 中替换了默认类。
【解决方案2】:

我找到了答案here(Apple Developer Forum)。

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

我建议您避免在靠近导航栏或工具栏的位置使用触敏 UI。这些区域通常被称为“倾斜因素”,使用户更容易在按钮上执行触摸事件,而不会遇到执行精确触摸的困难。例如,UIButton 也是如此。

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

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

CustomWindow.h

@interface CustomWindow: UIWindow 
@end

自定义窗口.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。

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

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

【讨论】:

  • 你知道这里最奇怪的是什么吗?在 iPad 中,应该标记为 NO 的原点不是 64,而是 44...
  • 也许是这样。我没有在 iPad 上检查过。是的,这很奇怪。
  • 这甚至发生在包含视图控制器的边界之外 (my question)。我已将此归档为雷达 19504573。
  • Apple 链接无效。是否有另一个指向有效 Apple 文档的链接可以提供此建议?我在 HIG 中没有找到它。
  • @David 它适用于拥有开发者帐户的人。 Apple Developer Technical Support 将引用归功于 Keith
【解决方案3】:

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

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

【讨论】:

  • 值得注意的是,这会破坏导航栏中的任何非自定义UIBarButtonitems,例如。后退按钮。
  • - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; if ([hitView isKindOfClass:[UIControl class]]) { return hitView; } 其他 { 返回零; } } @mxcl
【解决方案4】:

我的解决方案如下:

首先: 在您的应用程序中(无论您在何处输入此代码)添加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 被点击时获取通知。

【讨论】:

    【解决方案5】:

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

    首先让我们看一下代码。

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

    看一下屏幕就清楚了。

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

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

    【讨论】:

    • 如果命中测试由于某种原因被调用了奇数次,那么它就会中断,不是吗?
    【解决方案6】:

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

    1. 你试过setUserInteractionEnabled:NO作为标签吗?

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

      [button sendSubviewToBack:label];

    如果代码有效,请告诉我:)

    【讨论】:

    • 谢谢,但代码的工作方式相同,标签文本上方的区域没有被触及...但在标签文本下它工作正常。
    【解决方案7】:

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

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

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

    【讨论】:

    • 谢谢,但我发现这不是创建按钮和标签的问题。现在我使用 titleLabel 代替我的标签。
    【解决方案8】:

    这解决了我的问题..

    我在导航栏子类中添加了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】:

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

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

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

      第四步:当我们关心的view出现时调用ChunyuWindowaddViewForTouchPriority,当view消失或dealloc时调用removeViewForTouchPriority,在ViewControllers的viewDidAppear/viewDidDisappear/dealloc中,所以ChunyuWindow中的_touchView为NULL,和UIWindow一样,没有副作用。

      【讨论】:

        【解决方案10】:

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

        self.navigationController?.barHideOnTapGestureRecognizer.enabled = false
        

        您可以禁用负责UINavigationBar 上“倾斜区域”的手势识别器,而不是覆盖UIWindow

        【讨论】:

        • 看起来很有希望但无法正常工作(ios10,在设备上测试)
        • 救世主,最简单的解决方案,为此苦苦挣扎了一整天,谢谢克里斯。
        • 是的,很遗憾不能在 iOS 14.3 上运行
        【解决方案11】:

        根据 Bart Whiteley 给出一个扩展版本。无需子类化。

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

        【讨论】:

        【解决方案12】:

        以下内容对我有用:

        self.navigationController?.isNavigationBarHidden = true
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-21
          • 2011-06-05
          • 1970-01-01
          相关资源
          最近更新 更多