Скроллинг при помощи акселерометра
Одной из самых “вкусных” функциональностей, которые предоставляет iPhone/iPod Touch, безусловно является акселерометр. Его реализация в устройствах от Apple не первая в мире — попытки внедрения модуля определения положения аппарата были и раньше, например, в телефонах от Sony Ericsson и Nokia, но действительно удачное применение удалось найти именно Apple, сделав акселерометр одной из главных отличительных черт своих телефонов и плееров.
В этот раз начну свою статью необычно — с видео, в котором продемонстрирую в действии приложение, созданием которого мы займемся в этом посте.
Как и большинство классов, с которыми нам довелось познакомиться, API акселерометра прост до безобразия. Для его использования в своих программах нужно быть знакомым всего с двумя классами и одним делегатом. Но прежде чем приступить к знакомству с ними, создайте новый проект на основе шаблона “Window based application” в XCode. Я назвал его “accelerometer”.
Хочу заранее предупредить, что без сертификата разработчика попробовать в действии это приложение вам не удастся, как и любые примеры с сайта developer.apple.com, работающие с информацией о положении аппарата в пространстве, так как в симуляторе нет возможности эмулировать вращение устройства. Но информации в статье достаточно, чтобы составить представление о функционировании акселерометра, знания о работе с которым пригодятся вам в будущем и позволят писать приложения, использующие его функциональность, уже сейчас.
Первым делом нам потребуется создать в нашей программе Table View и заполнить его буквами алфавита. Для этих целей воспользуемся готовым объектом UITableViewController, который немного упростит нам дальнейшую работу с таблицей. Для тех, кто еще не был знаком с UITableView, отправляю к статье Вездесущий UITableView, а остальные откройте MainWindow.xib и добавьте на форму объект UITableViewController.
dataSource и delegate для нашей таблицы автоматически установлены на объект UITableViewController. Чтобы описать методы, ответственные за наполнение таблицы данными, измените класс UITableViewController на tableViewController, сгенерируйте файлы класса и включите их в состав проекта.
Чтобы наша таблица отображалась в окне приложение, добавьте outlet переменную viewController класса tableViewController в класс accelerometerAppDelegate, и свяжите ее с объектом tableViewController на форме через Interface Builder. А в методе applicationDidFinishLaunching: добавьте следующую строчку:
[window addSubview:viewController.view];
Специально не описываю подробности всего процесса, так как уже неоднократно освещал эти вопросы в предыдущих статьях. В случае появления трудностей взгляните в исходный код проекта, приложенного в конце статьи.
В accelerometerAppDelegate.h объявите переменную list класса NSMutableArray, в которую мы поместим сгенерированный нами алфавит. В этом нам поможет метод stringWithFormat класса NSString, в котором мы будем формировать символы по кодам из нижней части ACSII таблицы. Код поместим во все тот же applicationDidFinishLaunching:.
list = [[NSMutableArray alloc] init];
for (int i = 65; i < 65+26; i++) {
[list addObject:[NSString stringWithFormat:@"%c", i]];
}
Не забудьте освободить переменную list в dealloc.
Нам осталось сформировать код, ответственный за заполнение таблицы данными. Подробно останавливаться на нем не буду, лишь приведу исходный текст tableViewController.m.
#import "tableViewController.h"
#import "accelerometerAppDelegate.h"
@implementation tableViewController
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
accelerometerAppDelegate *appDelegate = (accelerometerAppDelegate *)[[UIApplication sharedApplication] delegate];
return [appDelegate.list count];
}
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];
}
accelerometerAppDelegate *appDelegate = (accelerometerAppDelegate *)[[UIApplication sharedApplication] delegate];
cell.text = (NSString *)[appDelegate.list objectAtIndex:indexPath.row];
return cell;
}
@end
Теперь, когда подготовительная фаза завершена, мы можем приступить к знакомству с акселерометром. Но прежде немного теории.
Акселерометр — устройство, измеряющее проекцию силы тяжести. С его помощью можно также установить величину ускорения аппарата под действием внешних сил. Для получения информации о величине и направлении ускорения используются значения проекций на оси координат.
* изображение взято с сайта http://www.forestgumpsays.com/?p=42
В состоянии покоя, когда устройство не двигается, величина замеряемого ускорения равна силе тяжести и принята за единицу. Соотношения величин проекций этой силы на оси координат дают нам углы поворота устройства в пространстве. Если iPhone находится в движении, то величину ускорения, с которым разгоняется аппарат, можно посредством дополнительных преобразований вычислить на основе значений проекций.
Заметьте, это именно ускорение, а не скорость движения устройства. То есть, если ваш iPhone начнет падать на землю, то проекции величины ускорения примут значение 0 по всем осям — ваш iPhone будет в невесомости :) А если вы поднимаетесь с должным ускорением на лифте вверх, то значение силы тяжести на время ускорения увеличится.
Чтобы было понятнее, пользуясь значением по оси Z можно определить в каком положении находится аппарат: лицом вверх (-1, при нулевых значениях по другим осям), в вертикальном положении (0) или лицом вниз (+1).
Ось Y дает нам следующую информацию: iPhone находится в вертикальном положении (-1), лежит в горизонтальной плоскости (0) или находится в вертикальном положении, только вверх ногами (+1).
И, наконец, ось X дает информацию о повороте аппарата влево (-1) или вправо (+1).
Для случая произвольной ориентации iPhone данные о величине ускорения распределяются по осям согласно проекции вектора ускорения.
Всю эту информацию аккумулирует объект класса UIAcceleration, возвращающий данные по всем осям, а также временной маркер, позволяющий определить относительное время замера указанных величин. Напрямую подступиться к данным этого класса нельзя, эту информацию можно получить только через делегат UIAccelerometerDelegate, предоставляющего для реализации один единственный метод accelerometer:didAccelerate:, в который возвращается объект класса UIAcceleration. Назначение делегата и инициализация вызовов метода accelerometer:didAccelerate: происходит при помощи класса UIAccelerometer. Пришло время написать код, реализующий описанный алгоритм.
В метод applicationDidFinishLaunching: добавьте следующие строки.
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:0.5];
[[UIAccelerometer sharedAccelerometer] setDelegate:self];
Первая назначает интервал в секундах, с которым будут посылаться данные от акселерометра в класс-делегат, назначенный в следующей строке. В заголовочном файле класса accelerometerAppDelegate укажите протокол UIAccelerometerDelegate.
@interface accelerometerAppDelegate : NSObject <UIApplicationDelegate, UIAccelerometerDelegate>
Нам осталось только добавить реализацию метода в класс accelerometerAppDelegate.
— (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
}
Но что же мы должны в нем написать, чтобы реализовать прокрутку списка? Возьмем за точку отсчета положение нашего устройства в пространстве, при котором угол между задней стенкой и землей составляет 45 градусов. В этом случае проекции силы тяжести на ось Y будет составлять -0.7. Если мы отклоняем аппарат чуть ближе к вертикальному положению, то примем, что при достижении угла в 30 градусов от вертикали мы должны перелистнуть список к концу. И наоборот, при достижении угла в 30 и менее градусов от горизонтального положения, мы должны перелистнуть список к началу.
В первом случае абсолютная величина проекции силы тяжести на ось Y, направленная вдоль аппарата, станет равна 0.86. Те, кто не понял откуда взялось это значение, вспоминаем геометрию и вычисление проекции на ось координат вектора единичной длины. Во втором случае это же значение равно 0.5. Для реализации прокрутки мы воспользуемся методом scrollToRowAtIndexPath:atScrollPosition:animated: класса UITableView.
— (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
double absY = fabs(acceleration.y);
if (absY <= 0.5) {
// Прокрутка к началу списка
[viewController.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
} else if (absY >= 0.86) {
// Прокрутка к концу списка
[viewController.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([list count] — 1) inSection:0]
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
}
Мы будем работать с абсолютными значениями проекции силы тяжести на ось Y, чтобы не запутаться в операциях сравнения отрицательных величин. scrollToRowAtIndexPath:atScrollPosition:animated: использует для указания координат конечной точки прокрутки объект NSIndexPath, точнее категорию класса NSIndexPath, приведенную для использования в UITableView. Мы указываем строку (row) и номер секции (section), куда нужно переместиться. Не забываем, что нумерация начинается с нуля. Параметр atScrollPosition позволяет указать где должна отображаться строка с указанной координатой после прокрутки к ее положению: UITableViewScrollPositionTop — в верхней части таблицы, UITableViewScrollPositionBottom — в нижней части таблицы. И, конечно же, прокрутка должна быть анимирована, чтобы мы увидели весь процесс.
Можно немного улучшить наше приложение. Как вы помните, описанный выше метод вызывается каждые 0.5 секунды. А это значит, что при нахождении аппарата в горизонтальном или вертикальном положении прокрутка списка будет вызываться постоянно, не смотря на то, что мы уже находимся в крайнем положении. Предлагаю сравнивать текущее положение с требуемым, и только в случае разницы осуществлять прокрутку.
В определении положения в таблице нам поможет метод indexPathsForVisibleRows класса UITableView, возвращающий список координат видимых ячеек. Нам потребуется лишь сравнить координаты первой и последней ячейки.
— (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
double absY = fabs(acceleration.y);
if (absY <= 0.5) {
if ([[[viewController.tableView indexPathsForVisibleRows] objectAtIndex:0] row] > 0) {
// Прокрутка к началу списка
[viewController.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
}
} else if (absY >= 0.86) {
if ([[[viewController.tableView indexPathsForVisibleRows] lastObject] row] < ([list count] — 1)) {
// Прокрутка к концу списка
[viewController.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([list count] — 1) inSection:0]
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
}
}
Как вы поняли, по показаниям акселерометра можно определить направление силы тяжести, и, как следствие, ориентацию устройства. Если вам нужно всего лишь определить положение аппарата, то есть более простой путь — воспользоваться информацией, которую предоставляет класс UIDevice.
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
Метод orientation возвращает одно из нескольких возможных значений:
- UIDeviceOrientationPortrait и UIDeviceOrientationPortraitUpsideDown — вертикальное и вертикально-перевернутое положение
- UIDeviceOrientationLandscapeLeft и UIDeviceOrientationLandscapeRight — горизонтальное положение в двух вариантах
- UIDeviceOrientationFaceUp и UIDeviceOrientationFaceDown — лицом вверх и лицом вниз
Если же вы хотите, как и выше, получать информацию о смене положения аппарата, то следует воспользоваться следующим кодом.
— (void)applicationDidFinishLaunching:(UIApplication *)application {
// ...
// Инициализация
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didRotate:)
name:@"UIDeviceOrientationDidChangeNotification"
object:nil];
}
}
— (void) didRotate:(NSNotification *)notification
{
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
// Здесь ваш код
}
Метод beginGeneratingDeviceOrientationNotifications своим запуском начинает генерировать уведомления UIDeviceOrientationDidChangeNotification, которые информируют о смене положения iPhone. Эти уведомления мы можем перехватывать и обрабатывать самостоятельно, что мы и делаем в методе didRotate. Я немного залез вперед в своих объяснениях, так как еще не поднимал тему обработки уведомлений в своих постах, и дабы не отклоняться от темы статьи, предлагаю закончить сегодняшнее повествование и поставить здесь жирную точку.
Дополнительная информация:Примеры кода:
Комментарии
Огромное спасибо!
Форма комментирования для «Скроллинг при помощи акселерометра»
man, you have a balls! really!