2012-01-31 2 views
16

У меня есть пользовательский UIButton с UILabel, добавленный как subview. Кнопка выполняет заданный селектор только тогда, когда я касаюсь его около 15 точек ниже верхней границы. И когда я нажимаю выше этой области, ничего не происходит.Почему UINavigationBar крадет события касания?

Я выяснил, что это не вызвано неправильным созданием кнопки и метки, потому что после того, как я сдвигаю кнопку ниже примерно на 15 пикселей, она работает правильно.

ОБНОВЛЕНИЕ Я забыл сказать, что кнопка, расположенная под UINavigationBar и 1/3 верхней части кнопки, не получает событий касания.

Image was here

Вид с 4-мя кнопками находится под Панель навигации. И когда прикоснитесь к «Баскетболу» в верхней части, BackButton получит событие касания, а при касании «Фортепиано» сверху, тогда rightBarButton (если существует) получит прикосновение. Если не существует, ничего не произошло.

Я не нашел эту документацию в приложениях.

Также я нашел this тему, связанную с моей проблемой, но ответа тоже нет.

+0

Ответ от @nonamelive в этой ссылке решает проблему на всех ОС (включая iOS 7.0/7.1): http://stackoverflow.com/questions/7806557/touch-events-within-8-pixels-of-nav -bar-not-called – strange

ответ

16

я узнал ответ here (Apple Форум разработчиков).

Кейт в компании Apple для разработчиков технической поддержки, 18 мая 2010 года (iPhone OS 3):

Я рекомендую вам избежать сенсорного интерфейса в такой непосредственной близости от навигационной панели или панели инструментов.Эти области обычно известны как «факторы отслоения», что облегчает пользователям возможность совершать сенсорные события на кнопках без сложностей с точными касаниями. Это также относится к UIButton, например.

Но если вы хотите захватить событие касания до того, как панель навигации или панель инструментов его получит, вы можете подклассировать UIWindow и переопределить: - (void) событие sendEvent: (UIEvent *);

Также я узнал, что, когда я касаюсь области под 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.

Теперь, когда я касаюсь области под навигационной панелью, ничего не происходит.

Мои кнопки по-прежнему не получают событий касания, потому что я не знаю, как отправить это событие (и изменить координаты) на мой взгляд с помощью кнопок.

+1

Знаете ли вы, что здесь самое странное? То, что в iPad вместо 64, источник, который должен быть помечен как NO, равен 44 ... –

+1

Возможно, это так. Я не проверял iPad. И да, это странно. – Alexander

+0

Это происходит даже за пределами границ контроллера содержимого ([мой вопрос] (http://stackoverflow.com/questions/27992243/tap-region-issues-with-uinavigationcontroller-in-vc-containment/)). Я зарегистрировал это как радар 19504573. –

0

Есть 2 вещи, которые могут вызвать проблемы.

  1. Вы попробовали setUserInteractionEnabled:NO для этикетки.

  2. Вторая вещь я думаю, мог бы работать вне кроме того, после добавления метки на верхней части кнопки вы можете отправить ярлык на спине (это могло бы работать, не уверен, хотя)

    [button sendSubviewToBack:label];

Пожалуйста, дайте мне знать, если код работает :)

+0

Спасибо, но код работает одинаково, область над текстом надписи не попадает ... но под текстом подписи она работает нормально. – Alexander

0

Ваши ярлыки огромны. Они начинаются с {0,0} (левый верхний угол кнопки), распространяются по всей ширине кнопки и имеют высоту всего изображения. Проверьте данные frame и повторите попытку.

Также у вас есть возможность использовать UIButtontitleLabel. Возможно, вы назначили название позже, и оно попадает на этот ярлык, а не на ваш собственный UILabel. Это объясняет, почему текст (принадлежащий кнопке) будет работать, в то время как метка будет охватывать остальную часть кнопки (не пропуская краны).

titleLabel является свойством только для чтения, но вы можете настроить его так же, как ваш собственный ярлык (за исключением, возможно, frame), включая цвет текста, шрифт, тени и т.д.

+0

Спасибо, но я узнал, что это не проблема кнопки создания и метки. И теперь я использую titleLabel вместо своего ярлыка. – Alexander

27

Я заметил, что если вы установите 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.

+1

Это работает только на симуляторе –

+1

Отключение взаимодействия с пользователем, похоже, работает на устройстве для меня. Я тестирую iPhone 5 и iOS 6.1.2. Однако подкласс не нужен; вы можете просто отключить внешнее взаимодействие пользователя. – jtbandes

+0

очень приятное решение, так как я не могу отключить взаимодействие с пользователем (была кнопка), спасибо. –

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 Instance

Шаг 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 «s addViewForTouchPriority когда появляется вид мы заботимся о том, и вызвать removeViewForTouchPriority, когда представление исчезает или dealloc, в viewDidAppear/viewDidDisappear/dealloc из ViewControllers, так _touchView в ChunyuWindow является NULL, и это то же самое как UIWindow, не имеющий побочных эффектов.

0

Это решило мою проблему ..

Я добавил hitTest:withEvent: код на NavBar подкласс ..

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

Подкласса UINavigationBar и добавить этот метод. Это приведет к тому, что краны будут пропущены, если они не постукивают подвью (например, кнопка).

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

Чистый и умный способ. Это работает для меня –

+0

В частности, это сломает любые нестандартные 'UIBarButtonitem' в вашем навигаторе, например. кнопку назад. – mxcl

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) 
} 

}

Вы могли бы понять, почему я делаю вторую обработку сенсорной. Рецепт решения.

Хит-тест вызывается дважды для вызова. В первый раз сообщается о фактической точке в окне. Все идет хорошо. На втором проходе это происходит.

Если система видит навигационную панель, а точка попадания составляет около 9 пикселей на стороне Y, она пытается уменьшить это постепенно до уровня ниже 44 пунктов, где находится панель навигации.

Посмотрите на экран, чтобы быть чистым.

enter image description here

Так Theres механизм, который будет использовать близлежащий логику для второго прохода Трассировка. Если мы сможем узнать его второй проход, а затем позвоним супер с первой тестовой точкой. Работа выполнена.

Приведенный выше код делает это точно.

+0

, если по какой-то причине критерий удара называется нечетным числом раз, то он сломается, не так ли? –

0

Альтернативное решения, которое работало на меня, на основе ответа, предоставленной Александар:

self.navigationController?.barHideOnTapGestureRecognizer.enabled = false 

Вместо перекрывая UIWindow, вы можете просто отключить распознавани жеста, ответственные за «область отстойной» на UINavigationBar ,

+2

выглядел многообещающим, но не работающим (ios10, проверенный на устройстве) –

+0

Спаситель, простейшее решение, изо всех сил боролся за этот день, спасибо Крису. – skkrish

+0

Также не работает в симуляторе iOS 9. – Greg

0

Дайте дополнительную версию в соответствии с Бартом Уайтли. Нет необходимости в подклассе.

@implementation UINavigationBar(Xxxxxx) 

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

@end 
+0

Небезопасно добавлять методы категорий с тем же именем, что и существующие методы для объекта. (см. https://stackoverflow.com/a/5272612/42484) – Greg

0

Решение для меня было следующим:

Первое: Добавить в приложении (не имеет значения, где вы вводите этот код) расширение для 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.

Смежные вопросы