Как стать автором
Поиск
Написать публикацию
Обновить

Отучаем сенсорный экран смещать координаты прикосновений на пару мм вверх

Время на прочтение4 мин
Количество просмотров7K
Думаю, мало кто замечал, что физические координаты прикосновения пальца и их программное отображение в iOS немного отличаются: iOS выдаёт точку, смещенную примерно на 1,5 мм вверх относительно реального прикосновения. Это сделано в интересах usability — точка, приближенная к ногтю, кажется более реалистичной, нежели лежащая ниже под подушечкой пальца. Кроме того, так лучше видно область экрана, куда нажимаешь.
Чтобы было понятнее, о чем речь, можно скачать любую рисовалку (например Bamboo Paper, приложение не моё, бесплатное), заблокировать автоповорот экрана, нарисовать небольшую горизонтальную линию, затем перевернуть устройство вверх ногами (обязательно при блокировке автоповорота) и попытаться продолжить нарисованную линию. Скорее всего продолженная линия окажется ниже первоначальной.

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

По моим тестам на iPad первого поколения сдвиг составил около 7 пикселей. Естественно, на других устройствах результат будет другим, необходимо тестировать.

Открытого программного интерфейса для управления этим сдвигом я в документации Apple не нашел, а потому пришлось искать обходные пути.

В случае рисовалок можно просто ввести поправку в логику методов touchesMoved и подобным:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
	//...
	CGPoint point = [[touches anyObject] locationInView:self];
	// Поправка имеет положительное значение, т.к. ось y направлена сверху вниз
	point.y = point.y + 7;
	//...
}

Однако это не всегда приемлемо, особенно при наличии некоторого объёма готового кода, который хочется повторно использовать без изменений.

В качестве радикального решения в первой версии этой статьи я показал пример с внесением поправки в абсолютно все вызовы locationInView и previousLocationInView у экземпляров UITouch:
#import "objc/runtime.h"
@interface UITouch (Adjusted)
-(CGPoint)adjustedLocationInView:(UIView *)view;
-(CGPoint)adjustedPreviousLocationInView:(UIView *)view;
@end
@implementation UITouch (Adjusted)
-(CGPoint)adjustedLocationInView:(UIView *)view{
    CGPoint point = [self adjustedLocationInView:view];
    point.y = point.y + 7;
    return point;
}
-(CGPoint)adjustedPreviousLocationInView:(UIView *)view{
    CGPoint point = [self adjustedPreviousLocationInView:view];
    point.y = point.y + 7;
    return point;
}
+(void)load{
    Class class = [UITouch class];
    Method locInViewMethod = class_getInstanceMethod(class, @selector(locationInView:));
    Method adjLocInViewMethod = class_getInstanceMethod(class, @selector(adjustedLocationInView:));
    method_exchangeImplementations(locInViewMethod, adjLocInViewMethod);
    Method prevLocInViewMethod = class_getInstanceMethod(class, @selector(previousLocationInView:));
    Method adjPrevLocInViewMethod = class_getInstanceMethod(class, @selector(adjustedPreviousLocationInView:));
    method_exchangeImplementations(prevLocInViewMethod, adjPrevLocInViewMethod); 
    NSLog(@"UITouch class is adjusted now.");
}
@end

Здесь с помощью функции рантайма method_exchangeImplementations происходит обмен реализаций дефолтовых методов UITouch на наши с поправкой. Обратите внимание, файл с созданной категорией не обязательно импортировать в какой-либо другой .h или .m файл. Достаточно только добавить его в проект, а метод +load вызовется автоматически, т.к. сообщение +load автоматически посылается каждому классу и категории при добавлении оных в рантайм.
Upd. Однако, как выяснилось в комментариях, начиная с iOS 5 Apple просит не использовать method_exchangeImplementations в приложениях. Спасибо wicharek за информацию. Поэтому для внесения поправки лучше использовать любой другой способ на свой вкус, например один из предложенных в комментариях.

Итак, после введения поправки рисование на view будет работать идентично при любой ориентации устройства. Однако это не решит проблемы с шахматными фигурками: при определении, какому view послать событие прикосновения, по-прежнему будет использоваться точка со сдвигом, и мы можем попасть не в ту фигурку.
Для определения того, какой view должен обработать прикосновение, используется метод hitTest:withEvent: у UIView, который работает следующим образом:
  • вызывается pointInside:withEvent: у self;
  • если рузультат NO, то hitTest:withEvent: возвращает nil, т.е. view на прикосновение не отвечает;
  • если результат YES, то метод рекурсивно посылает hitTest:withEvent: всем своим subview;
  • если один из subview вернул не-nil объект, то корневой hitTest:withEvent: возвращает этот объект;
  • если все subview вернули nil, либо view не имеет subview, то возвращается self;

Таким образом, для корректного попадания прикосновений в фигурки шахмат, достаточно переопределить hitTest:withEvent у корневого view, содержащего все subview, для которых нам надо ввести поправку:
@implementation ChessBoardView
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    point.y = point.y + 7;
    UIView *hitView = [super hitTest:point withEvent:event];
    return hitView;
}
@end

После чего игроку, который играет черными, попадать в шахматные фигурки становится так же удобно, как и игроку, играющему белыми.
Теги:
Хабы:
Всего голосов 27: ↑23 и ↓4+19
Комментарии28

Публикации

Ближайшие события