Почему UINavigationBar крадет события касания? - программирование
Подтвердить что ты не робот

Почему UINavigationBar крадет события касания?

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

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

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

Изображение было здесь

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

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

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

4b9b3361

Ответ 1

Я узнал ответ здесь (Форум разработчиков Apple).

Кейт в технической поддержке Apple Developer, 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.

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

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

Ответ 2

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

Ответ 3

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

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

Ответ 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)
}

}

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

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

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

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

введите описание изображения здесь

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

Этот код делает это точно.

Ответ 6

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

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

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

    [button sendSubviewToBack:label];

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

Ответ 7

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

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

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

Ответ 8

Расширение решения Alexander:

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

Ответ 9

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

Я добавил 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];
    }

Ответ 10

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

self.navigationController?.barHideOnTapGestureRecognizer.enabled = false

Вместо того, чтобы переопределять UIWindow, вы можете просто отключить распознаватель жестов, ответственный за "область slop" на UINavigationBar.

Ответ 11

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

@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