Главная iPhone Mac OS X Форум О себе

Скроллинг при помощи акселерометра

Одной из самых “вкусных” функциональностей, которые предоставляет 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.

UITableViewController в Interface Builder

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

Теперь, когда подготовительная фаза завершена, мы можем приступить к знакомству с акселерометром. Но прежде немного теории.

Акселерометр — устройство, измеряющее проекцию силы тяжести. С его помощью можно также установить величину ускорения аппарата под действием внешних сил. Для получения информации о величине и направлении ускорения используются значения проекций на оси координат.

Оси координат в iPhone
* изображение взято с сайта http://www.forestgumpsays.com/?p=42

В состоянии покоя, когда устройство не двигается, величина замеряемого ускорения равна силе тяжести и принята за единицу. Соотношения величин проекций этой силы на оси координат дают нам углы поворота устройства в пространстве. Если iPhone находится в движении, то величину ускорения, с которым разгоняется аппарат, можно посредством дополнительных преобразований вычислить на основе значений проекций.

Заметьте, это именно ускорение, а не скорость движения устройства. То есть, если ваш iPhone начнет падать на землю, то проекции величины ускорения примут значение 0 по всем осям — ваш iPhone будет в невесомости :) А если вы поднимаетесь с должным ускорением на лифте вверх, то значение силы тяжести на время ускорения увеличится.

Чтобы было понятнее, пользуясь значением по оси Z можно определить в каком положении находится аппарат: лицом вверх (-1, при нулевых значениях по другим осям), в вертикальном положении (0) или лицом вниз (+1).

Проекция вектора силы тяжести на ось Z

Ось Y дает нам следующую информацию: iPhone находится в вертикальном положении (-1), лежит в горизонтальной плоскости (0) или находится в вертикальном положении, только вверх ногами (+1).

Проекция вектора силы тяжести на ось Y

И, наконец, ось X дает информацию о повороте аппарата влево (-1) или вправо (+1).

Проекция вектора силы тяжести на ось X

Для случая произвольной ориентации iPhone данные о величине ускорения распределяются по осям согласно проекции вектора ускорения.

Проекция вектора силы тяжести на оси координат в произвольном положении 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. Я немного залез вперед в своих объяснениях, так как еще не поднимал тему обработки уведомлений в своих постах, и дабы не отклоняться от темы статьи, предлагаю закончить сегодняшнее повествование и поставить здесь жирную точку.

Скачать архив проекта (8Кб)

Дополнительная информация:

Примеры кода:

Комментарий

Пингбэки

Ссылки по программированию для iPhone &laquo; KasBlog @kasblog.ru 2.11.2009 22:15
Раз: http://pyobjc.ru/2008/09/30/skrolling-pri-pomoshi-akselerometra/

Комментарии

Val 1.10.2008 12:54

Огромное спасибо!

ответить

Форма комментирования для «Скроллинг при помощи акселерометра»

Обязательное поле. Не больше 30 символов.

Обязательное поле

captcha image Пожалуйста, введите символы, которые вы видите на изображении

vjaako 19.05.2009 20:37

man, you have a balls! really!

ответить
.