Почему верхняя часть моего UISegmentedControl не поддерживается?
Пока я играл на своем телефоне, я заметил, что мой UISegmentedControl не очень отзывчив. Было бы две или более попыток сделать мой входной регистр. Поэтому я решил запустить свое приложение в Simulator, чтобы более точно определить, что было не так. Нажимая десятки раз с помощью мыши, я решил, что верхние 25% UISegmentedControl не отвечают (часть выделена красным цветом с помощью Photoshop на скриншоте ниже). Я не знаю ни одного невидимого UIView, который мог бы его блокировать. Вы знаете, как сделать весь контроль доступным?
![uinavigationbar uisegmentedcontrol]()
self.segmentedControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Uno", @"Dos", nil]];
self.segmentedControl.selectedSegmentIndex = 0;
[self.segmentedControl addTarget:self action:@selector(segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
self.segmentedControl.height = 32.0;
self.segmentedControl.width = 310.0;
self.segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
self.segmentedControl.tintColor = [UIColor colorWithWhite:0.9 alpha:1.0];
self.segmentedControl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
UIView* toolbar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, HEADER_HEIGHT)];
toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(
toolbar.bounds.origin.x,
toolbar.bounds.origin.y,
// * 2 for enough slack when iPad rotates
toolbar.bounds.size.width * 2,
toolbar.bounds.size.height
);
gradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor whiteColor] CGColor],
(id)[[UIColor
colorWithWhite:0.8
alpha:1.0
] CGColor
],
nil
];
[toolbar.layer insertSublayer:gradient atIndex:0];
toolbar.backgroundColor = [UIColor navigationBarShadowColor];
[toolbar addSubview:self.segmentedControl];
UIView* border = [[UIView alloc] initWithFrame:CGRectMake(0, HEADER_HEIGHT - 1, toolbar.width, 1)];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
border.backgroundColor = [UIColor colorWithWhite:0.7 alpha:1.0];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[toolbar addSubview:border];
[self.segmentedControl centerInParent];
self.tableView.tableHeaderView = toolbar;
http://scs.veetle.com/soget/session-thumbnails/5363e222d2e10/86a8dd984fcaddee339dd881544ecac7/5363e222d2e10_86a8dd984fcaddee339dd881544ecac7_20140509171623_536d6fd78f503_68_896x672.jpg
Ответы
Ответ 1
Как уже написано в других ответах, UINavigationBar захватывает штрихи, сделанные рядом с навигационной панелью, но не потому, что у нее есть некоторые подвидности, расширенные по краям: это не причина.
Если вы зарегистрируете всю иерархию представлений, вы увидите, что UINavigationBar не распространяется на определенные ребра.
Причина, по которой он получает штрихи, - это другое:
в UIKit существует много "особых случаев", и это один из них.
Когда вы нажимаете на экран, запускается процесс под названием "hit testing". Начиная с первого UIWindow, всем представлениям предлагается ответить на два "вопроса": точка, занятая вашими границами? каковы субподзоры, которые должны получать событие касания?
на эти вопросы отвечают эти два метода:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
Хорошо, теперь мы можем продолжить.
После крана, UIApplicationMain запускает процесс тестирования ударов. Тест атаки начинается с основного UIWindow (и выполняется, например, даже в окне строки состояния и в окне просмотра предупреждений) и проходит через все подпункты.
Этот процесс выполняется 3 раза:
- два раза начиная с UIWindow
- один раз, начиная с _UIApplicationHandleEvent
Если вы нажмете на панели навигации, вы увидите, что hitTest
в UIWindow вернет UINavigationBar (все три раза)
Если вы нажмете на область под панелью навигации, вы увидите что-то странное:
- первые два hitTest вернут ваш UISegmentedControl
- последний hitTest вернет UINavigationBar
зачем это?
Если вы swizzle и подкласс UIView, переопределяя hitTest, вы увидите, что первые два раза точка прослушивания верна. В третий раз что-то меняет точку, делая что-то вроде point - 15
(или аналогичного числа)
После многого поиска я нашел, где это происходит:
UIWindow имеет (частный) метод, называемый
-(CGPoint)warpPoint:(CGPoint)point;
отлаживая его, я увидел, что этот метод изменяет отмеченную точку, если она находится непосредственно под строкой состояния.
Отлаживая больше, я увидел, что вызовы стека, которые делают это возможным, составляют всего 3:
[UINavigationBar, _isChargeEnabled]
[UINavigationBar, isEnabled]
[UINavigationBar, _isAlphaHittableAndHasAlphaHittableAncestors]
Итак, в конце этот метод warpPoint
проверяет, включен ли UINavigationBar и hittable, если да, он "искажает" точку. Точка деформируется из числа пикселей от 0 до 15, и этот "деформация" увеличивается, когда вы приближаетесь к панели навигации.
Теперь, когда вы знаете, что происходит за кулисами, вы должны знать, как его избежать (если хотите).
Вы не можете просто переопределить warpPoint:
, если приложение должно перейти в AppStore: это частный метод, и ваше приложение будет отклонено.
Вам нужно найти другую систему (например, предлагаемую, переопределяя sendEvent, но я не уверен, что она будет работать)
Поскольку этот вопрос интересен, я буду думать о юридическом решении завтра и обновить этот ответ (одной хорошей отправной точкой может быть подклассификация UINavigationBar, переопределяющая hitTest и pointInside, возвращающая nil/false, если, учитывая одно и то же событие по нескольким вызовам, но я должен проверить, работает ли это завтра)
ИЗМЕНИТЬ
Хорошо, я пробовал много решений, но не просто найти законный и стабильный.
Я описал фактическое поведение системы, которое может варьироваться в разных версиях (hitTest называется более или менее 3 раз, warpPoint деформирует точку около 15 пикселей, что может изменить ecc ecc).
Наиболее стабильным, очевидно, является незаконное переопределение warpPoint:
в подклассе UIWindow:
-(CGPoint)warpPoint:(CGPoint)point;
{
return point;
}
однако, я обнаружил, что такой метод (в подклассе UIWindow) достаточно стабилен и делает трюк:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// this method is not safe if you tap the screen two times at the same x position and y position different for 16px, because it moves the point
if (self.lastPoint.x == point.x)
{
// the points are on the same vertical line
if ((0 < (self.lastPoint.y - point.y)) && ((self.lastPoint.y - point.y) < 16) )
{
// there is a differenc of ~15px in the y position?
// if so, the point has been changed
point.y = self.lastPoint.y;
}
}
self.lastPoint = point;
return [super hitTest:point withEvent:event];
}
Этот метод записывает последнюю точку, и если последующее нажатие имеет один и тот же x, а y - для max 16px, то использует предыдущую точку.
Я много тестировал, и он кажется стабильным.
Если вы хотите, вы можете добавить дополнительные элементы управления, чтобы включить это поведение только в определенных контроллерах или только в определенной части окна, ecc ecc.
Если я найду другое решение, я обновлю сообщение
Ответ 2
Я считаю, что проблема в том, что кнопки в UINavigationBar имеют большую, чем обычно, зону касания. См. Этот SO пост. Вы также можете найти много дискуссий по этому вопросу с помощью поиска в области поиска "UINavigationBar".
В качестве возможного решения вы можете поместить сегментированный элемент управления в навигационную панель, но вы бы знали лучше, чем я, если это подходит для ваших случаев использования или нет.
Ответ 3
Я придумал альтернативное решение, которое для меня кажется более безопасным, чем LombaX. Он использует тот факт, что оба события входят с одной и той же меткой времени, чтобы отклонить последующее событие.
@interface RFNavigationBar ()
@property (nonatomic, assign) NSTimeInterval lastOutOfBoundsEventTimestamp;
@end
@implementation RFNavigationBar
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// [rfillion 2014-03-28]
// UIApplication/UIWindow/UINavigationBar conspire against us. There a band under the UINavigationBar for which the bar will return
// subviews instead of nil (to make those tap targets larger, one would assume). We don't want that. To do this, it seems to end up
// calling -hitTest twice. Once with a value out of bounds which is easy to check for. But then it calls it again with an altered point
// value that is actually within bounds. The UIEvent it passes to both seem to be the same. However, we can't just compare UIEvent pointers
// because it looks like these get reused and you end up rejecting valid touches if you just keep around the last bad touch UIEvent. So
// instead we keep around the timestamp of the last bad event, and try to avoid processing any events whose timestamp isn't larger.
if (point.y > self.bounds.size.height)
{
self.lastOutOfBoundsEventTimestamp = event.timestamp;
return nil;
}
if (event.timestamp <= self.lastOutOfBoundsEventTimestamp + 0.001)
{
return nil;
}
return [super hitTest:point withEvent:event];
}
@end
Ответ 4
Возможно, вам захочется проверить, какое представление записывает штрихи. Попробуйте этот метод -
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
[touch locationInView:self.view];
if([touch.view isKindOfClass:[UISegmentedControl class]])
{
NSLog(@"This is UISegment");
}
else if([touch.view isKindOfClass:[UITabBar class]])
{
NSLog(@"This is UITabBar");
} else if(...other views...) {
...
}
}
Как только вы поймете это, вы, возможно, сможете сузить свою проблему.
Ответ 5
Похоже, вы используете расширение категории для установки ширины/высоты на представлениях, а также центрируете их в своем родителе. Возможно, здесь есть скрытая проблема - можете ли вы реорганизовать, чтобы сделать свой макет без этой категории?
Я скопировал ваш код в чистый проект и запустил его в UITableViewController viewDidLoad - он отлично работает, и у меня нет мертвых точек, как вы сообщаете. Мне пришлось немного изменить код, так как у меня нет того же расширения категории, которое вы используете.
Кроме того, если вы используете этот код в viewDidLoad, вы должны убедиться, что ваше представление имеет определенный размер (вы получаете доступ к вашему view.width). Если вы создаете свой UITableViewController программно (vs from nib/storyboard), тогда кадр может быть CGRectZero. Шахта была загружена из наконечника, чтобы кадр был установлен.
Я бы также попытался временно удалить ваш пограничный вид, чтобы увидеть, является ли он виновником.
Ответ 6
Я рекомендую вам избегать использования сенсорного интерфейса в такой непосредственной близости от навигационной панели или панели инструментов. Эти области обычно известны как "факторы отслоения", что облегчает пользователям возможность совершать сенсорные события на кнопках без сложностей с точными касаниями. Это также относится к UIButton, например.
Но если вы хотите захватить событие касания до того, как панель навигации или панель инструментов его получит, вы можете подклассифицировать UIWindow и переопределить: - (void) событие sendEvent: (UIEvent *);
Ответ 7
Простым способом отладки является попытка использовать DCIntrospect в вашем проекте. Это очень простая в использовании/реализующая библиотека, которая позволяет узнать, какие виды видны там, где в симуляторе ветерок.
- Установите библиотеку и настройте ее
- Запустите приложение в симуляторе и перейдите к экрану с проблемой
- Нажмите клавишу пробела на клавиатуре (клавиатура компьютера, а не симулятор
клавиатуры)
- Нажмите на область 25% и посмотрите, что подсвечивается.
Если выделенное не является сегментированным контроллером представления, это представление может быть тем, что скрывает событие касания.
Ответ 8
Создайте протокол для UINavigationBar: (добавьте новый файл и вставьте ниже кода)
/******** file: UINavigationBar+BelowSpace.h*******/
"UINavigationBar+BelowSpace.h"
#import <Foundation/Foundation.h>
@interface UINavigationBar (BelowSpace)
@end
/*******- file: UINavigationBar+BelowSpace.m*******/
#import "UINavigationBar+BelowSpace.h"
@implementation UINavigationBar (BelowSpace)
-(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];
}
@end
Надеюсь на эту помощь ^ ^
Ответ 9
Попробуйте это
self.navigationController!.navigationBar.userInteractionEnabled = false;