Pull to refresh

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

Reading time 4 min
Views 6.8K
Думаю, мало кто замечал, что физические координаты прикосновения пальца и их программное отображение в 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

После чего игроку, который играет черными, попадать в шахматные фигурки становится так же удобно, как и игроку, играющему белыми.
Tags:
Hubs:
+19
Comments 28
Comments Comments 28

Articles