Почему UINavigationBar крадет сенсорные события?
У меня есть пользовательский UIButton с UILabel, добавленный в качестве подпредставления. Кнопка выполняет данный селектор, только когда я касаюсь его примерно на 15 пунктов ниже верхней границы. И когда я нажимаю на эту область, ничего не происходит.
Я обнаружил, что это не вызвано неправильным созданием кнопки и метки, потому что после того, как я переместил кнопку ниже примерно на 15 пикселей, она работает правильно.
ОБНОВЛЕНИЕ Я забыл сказать, что кнопка, расположенная под UINavigationBar и 1/3 верхней части кнопки, не получает сенсорные события.
Вид с 4 кнопками находится под панелью навигации. А когда коснитесь "Баскетбола" в верхней части, BackButton получит событие касания, а когда коснитесь "Piano" в верхней части, тогда rightBarButton (если существует) получит прикосновение. Если не существует, ничего не случилось.
Я не нашел эту документированную функцию в документации по приложениям.
Также я нашел эту тему связанной с моей проблемой, но ответа тоже нет.
11 ответов
Я узнал ответ здесь(Apple Developer Forum).
Кит из службы технической поддержки Apple, 18 мая 2010 г. (iPhone OS 3):
Я рекомендую вам избегать сенсорного интерфейса в такой непосредственной близости от навигационной панели или панели инструментов. Эти области, как правило, известны как "факторы спада", облегчающие пользователям выполнение сенсорных событий на кнопках без затруднений выполнения точных касаний. Это также относится к UIButtons, например.
Но если вы хотите захватить событие касания до того, как панель навигации или панель инструментов его получат, вы можете создать подкласс 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.
Теперь, когда я касаюсь области под панелью навигации, ничего не происходит.
Мои кнопки по-прежнему не получают сенсорные события, потому что я не знаю, как отправить это событие (и изменить координаты) на мой вид с помощью кнопок.
Я заметил, что если вы установите userInteractionEnabled в положение OFF, панель навигации больше не "крадет" касания.
Таким образом, вы должны создать подкласс UBavigationBar и в 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, вы можете найти здесь.
Подкласс UINavigationBar и добавьте этот метод. Это приведет к тому, что касания будут проходить, если они не касаются подпредставления (например, кнопки).
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
return v == self? nil: v;
}
Решение для меня было следующее:
Первое: добавьте в ваше приложение (не важно, где вы вводите этот код) расширение для 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
прослушивается.
Я просто хотел поделиться другой перспективой решения этой проблемы. Это не является проблемой по своему замыслу, но она предназначена для того, чтобы помочь пользователю вернуться назад или перемещаться. Но нам нужно плотно расположить вещи в навигационной панели или под ней, и все выглядит грустно.
Сначала давайте посмотрим на код.
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 точек, где находится навигационная панель.
Посмотрите на экран, чтобы быть ясным.
Таким образом, существует механизм, который будет использовать соседнюю логику для второго прохода хит-теста. Если мы сможем узнать его второй проход, а затем вызвать супер с первой контрольной точкой попадания. Работа выполнена.
Приведенный выше код делает это точно.
Дайте версию расширения согласно Барту Уайтли. Нет необходимости в подклассе.
@implementation UINavigationBar(Xxxxxx)
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
return v == self ? nil: v;
}
@end
Расширение решения Александра:
Шаг 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
"s addViewForTouchPriority
когда появится вид, который нас интересует, и позвоните removeViewForTouchPriority
когда представление исчезает или удаляется, в viewDidAppear/viewDidDisappear/dealloc из ViewControllers, поэтому _touchView в ChunyuWindow
NULL, и это так же, как UIWindow
, не имея побочных эффектов.
Ваши ярлыки огромны. Они начинаются в {0,0}
(левый верхний угол кнопки), растяните на всю ширину кнопки и получите высоту всего вида. Проверьте свои frame
данные и попробуйте еще раз.
Кроме того, у вас есть возможность использования UIButton
имущество titleLabel
, Может быть, вы устанавливаете заголовок позже, и он входит в этот ярлык, а не в ваш собственный UILabel
, Это объяснило бы, почему текст (принадлежащий кнопке) будет работать, в то время как метка будет покрывать остальную часть кнопки (не позволяя касаниям проходить).
titleLabel
это свойство только для чтения, но вы можете настроить его так же, как свой собственный ярлык (за исключением, возможно, frame
) включая цвет текста, шрифт, тень и т. д.
У меня сработало следующее:
self.navigationController?.isNavigationBarHidden = true
Это решило мою проблему..
Я добавил hitTest: withEvent: code в мой подкласс 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];
}
Альтернативное решение, которое сработало для меня, основано на ответе Александра:
self.navigationController?.barHideOnTapGestureRecognizer.enabled = false
Вместо переопределения UIWindow
, вы можете просто отключить распознаватель жестов, отвечающий за UINavigationBar
,
Есть 2 вещи, которые могут вызывать проблемы.
Ты пробовал
setUserInteractionEnabled:NO
для этикетки.Во-вторых, я думаю, что это может сработать, кроме того, что после добавления ярлыка в верхней части кнопки вы можете отправить ярлык назад (хотя это может сработать, но не уверен)
[button sendSubviewToBack:label];
Пожалуйста, дайте мне знать, если код работает:)