Почему UINavigationBar крадет события касания?
У меня есть пользовательский UIButton с UILabel, добавленный как subview. Кнопка выполняет заданный селектор только тогда, когда я касаюсь его около 15 точек ниже верхней границы. И когда я нажимаю выше этой области, ничего не происходит.
Я выяснил, что это не вызвано неправильным созданием кнопки и метки, потому что после того, как я сдвигаю кнопку ниже примерно на 15 пикселей, она работает правильно.
ОБНОВЛЕНИЕ Я забыл сказать, что кнопка, расположенная под UINavigationBar и 1/3 верхней части кнопки, не получает сенсорных событий.
Изображение было здесь
Вид с 4 кнопками расположен под навигационной панелью. И когда прикоснитесь к "Баскетболу" в верхней части, BackButton получит событие касания, а при касании "Фортепиано" сверху, тогда rightBarButton (если существует) получит прикосновение. Если не существует, ничего не произошло.
Я не нашел эту документацию в документах приложений.
Также я нашел эту тему, связанную с моей проблемой, но ответа тоже нет.
Ответы
Ответ 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