Использование акселерометра (Core Motion, iOS, Xcode 4.2)

Уроки по созданию приложений для iOS (iPhone, iPad, iPod Touch) в Xcode (Objective-C)
Ответить
Аватара пользователя
Александр
Создатель сайта
Создатель сайта
Сообщения: 4574
Зарегистрирован: 27 сен 2009, 16:45

Использование акселерометра (Core Motion, iOS, Xcode 4.2)

Сообщение Александр »

Использование акселерометра (Core Motion, iOS, Xcode 4.2)
Objective-C 2.0, iPhone, iPod, iPad, iOS, Core Motion, CMAcceleration, CMAccelerometerData, UIAccelerometer


В этой статье я приведу пример работы с акселерометром в Xcode в режимах Portrait и Landscape.

Акселерометр есть на всех устройствах с iOS.
Начиная с iPhone 4 и iPod Touch 4 в устройствах появился гироскоп.
С появлением гироскопа, для разработчиков стал доступен новый способ работы с движением устройства.
На данный момент предыдущий способ хоть и работает на iOS 4 и 5, но является устаревшим. Использовать его имеет смысл только если вы создаёте приложение для версии iOS 3 и ранее.
Актуальный способ работает начиная с iOS 4.


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



Использование акселерометра

1. Создаем новый проект Single View Application
Называем его Ball.


2. Скрываем Status Bar
В Ball-Info.plist добавляем
Status bar is initially hidden: YES
Более подробно о скрытии Status Bar можно прочитать в статье Как скрыть Status Bar (iOS, Xcode 4.2).


3. Открываем ViewController.xib
Выбираем View, в его свойствах выбираем Status Bar: None.

Добавляем Label.
В лейбл будем выводить координаты, чтобы нагляднее понимать как работает акселерометр.
В свойствах лейбла можно увеличить количество линий до 7. За этот параметр отвечает свойство Lines.

4. Пишем код
Для начала надо связать Label с кодом. Я делаю это перетаскиванием линии с зажатой правой кнопкой мыши в код.

Получаю такой код в ViewController.h

Код: Выделить всё

#import <UIKit/UIKit.h> @interface ViewController : UIViewController {    IBOutlet UILabel *label;} @property (retain, nonatomic) IBOutlet UILabel *label; @end

Для того чтобы интерфейс приложения не менял ориентацию, в настройках приложения Targets - Supported Device Orientations оставляем только Portrait и пишем в ViewController.m

Код: Выделить всё

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {    // Return YES for supported orientations    return (interfaceOrientation == UIInterfaceOrientationPortrait);}
Более подробно о ориентации интерфейса можно прочитать в статье Ориентация экрана (Portrait, Landscape, iOS, Xcode 4.2).



На следующем этапе есть разные варианты дальнейшей работы с акселерометром.

Вариант 1

Устаревший в iOS 5 способ работы с акселерометром.
Не рекомендуется использовать в iOS 5.0, хоть он там и работает.

Для устройств с iOS 3 и ранних версий, для работы с акселерометром нужно использовать метод accelerometer:didAccelerate:
Почитать в официальной документации https://developer.apple.com/library/ios ... egate.html

Пример использования этого метода:
ViewController.h

Код: Выделить всё

#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UIAccelerometerDelegate>  {    IBOutlet UILabel *label;}@property (retain, nonatomic) IBOutlet UILabel *label; @end
Необходимо добавить <UIAccelerometerDelegate>

ViewController.m

Код: Выделить всё

- (void)viewDidLoad{    [super viewDidLoad];        UIAccelerometer *accel = [UIAccelerometer sharedAccelerometer];    accel.delegate = self;    accel.updateInterval = 1.0f / 60.0f;} -(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {    label.text = [NSString stringWithFormat:                      @"Accelerometer \nx:%f, y:%f, z:%f",                      acceleration.x,                      acceleration.y,                      acceleration.z];}
Используйте этот способ только если вы делаете приложения для устрйоств с iOS 3.



Вариант 2

Использование Core Motion.
Начиная с iOS 4 стало возможно использовать фреймворк Core Motion.
Core Motion позволяет приложению получать данные о движениях устройства с помощью акселерометра и гироскопа.

Подробнее о Core Motion можно прочитать в документации http://developer.apple.com/library/ios/ ... index.html
https://developer.apple.com/library/ios ... vents.html

Для работы с Core Motion framework необходимо сначала добавить этот фреймворк в проект.
Это можно сделать зайдя в свойства проекта Build Phases - Link Binary With Libraries

Изображение

Нажать на значек плюса и добавить CoreMotion.framework

Изображение


С Core Motion можно работать двумя способами:
- С запуском акселерометра с заданным обработчиком (startAccelerometerUpdatesToQueue).
- Периодически опрашивая акселерометр.

Способ 1
Запуск акселерометра с заданным обработчиком.

В этом способе используются блоки.
Блоки в Objective-C - это функции, описанные специальным образом.
Подробнее о Блоках в Objective-C можно прочитать в документации https://developer.apple.com/library/mac ... ction.html


В ViewController.h добавляем недостающие строки

Код: Выделить всё

#import <UIKit/UIKit.h>#import <CoreMotion/CoreMotion.h> @interface ViewController : UIViewController {    UILabel  *label;    CMMotionManager *motionManager;} @property (retain, nonatomic) IBOutlet UILabel *label;@property (retain, nonatomic) CMMotionManager *motionManager; @end
В ViewController.m добавляем

Код: Выделить всё

@synthesize motionManager;// ...- (void)viewDidLoad{    [super viewDidLoad];        motionManager = [[CMMotionManager alloc] init];    NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //очередь        if(motionManager.accelerometerAvailable) {        motionManager.accelerometerUpdateInterval = 1.0f / 30.0f; // обновление 30 раз в секунду         [motionManager startAccelerometerUpdatesToQueue:queue withHandler:         ^(CMAccelerometerData *accelerometerData, NSError *error) {             NSString *str;             if(error) {                 [motionManager stopAccelerometerUpdates];                 str = [NSString stringWithFormat:@"Accelerometer error: %@", error];             } else {                 str = [NSString stringWithFormat:                        @"Accelerometer \nx:%f \ny:%f \nz:%f",                        accelerometerData.acceleration.x,                        accelerometerData.acceleration.y,                        accelerometerData.acceleration.z];             }             [label performSelectorOnMainThread:@selector(setText:) withObject:str waitUntilDone:YES];         }];    } else {        label.text = @"No accelerometer";    }    }
При каждом изменении значения акселерометра, motionManager передает в качестве аргумента (CMAccelerometerData *accelerometerData, NSError *error).
UIlabel доступен для изменения только в главном потоке, поэтому используем performSelectorOnMainThread. Более подробно в документации http://developer.apple.com/library/mac/ ... rence.html


Способ 2
Опрашиваем акселерометр по таймеру.

Код ViewController.h

Код: Выделить всё

#import <UIKit/UIKit.h>#import <CoreMotion/CoreMotion.h> @interface ViewController : UIViewController {    IBOutlet UILabel *label;    CMMotionManager *motionManager;    NSTimer *updateTimer;} @property (retain, nonatomic) IBOutlet UILabel *label;@property (retain, nonatomic) CMMotionManager *motionManager;@property (retain, nonatomic) NSTimer *updateTimer; @end
В ViewController.m добавляем

Код: Выделить всё

@synthesize motionManager;@synthesize updateTimer;// ...- (void)viewDidLoad{    [super viewDidLoad];        motionManager = [[CMMotionManager alloc]init];        if (motionManager.accelerometerAvailable) {        motionManager.accelerometerUpdateInterval = 1.0f / 30.0f;        [motionManager startAccelerometerUpdates];    } else {        label.text = @"No accelerometer";    }        updateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f / 30.0f                                                   target:self                                                   selector:@selector(accelUpdate)                                                   userInfo:nil                                                   repeats:YES];} -(void)accelUpdate{    if(motionManager.accelerometerAvailable) {        CMAccelerometerData *accelerometerData = motionManager.accelerometerData;        label.text = [NSString stringWithFormat:                      @"Accelerometer \nx:%f \ny:%f \nz:%f",                      accelerometerData.acceleration.x,                      accelerometerData.acceleration.y,                      accelerometerData.acceleration.z];     }    }
Можно запустить приложение и проверить его работу. Если запускать в симуляторе, то вы не сможете протестировать работу акселерометра. Запустите приложение на своем устройстве. О том как запустить приложение без сертификата разработчика можно прочитать в статье Запуск приложения на iPhone без сертификата (Xcode 4.2).

Изображение

В лейбле вы должны будете увидеть значения акселерометра по трем осям.



Добавление мяча

Для оформления приложения я создал 2 изображения для Retina дисплея (640x960).

grass.jpg

Изображение


soccer-ball.png

Изображение


Можете использовать эту графику, а можете создать свою.


Для обычных дисплеев (320x480) можно использовать изображения в 2 раза меньше.
Чтобы для обычных и retina дисплеев использовались разные изображения, нужно добавлять в проект изображения 2х форматов с разными именами ImageName.png и ImageName@2x.png. Для retina нужно добавить в имя @2x.
Подходящее под экран устройства изображение будет использовано автоматически.
Если использовать изображения для Retina дисплея, то на устройствах с обычными экранами не будет заметно разницы, но нагрузка на процессор будет сильнее. В этой статье я буду использовать только изображения для Retina дисплеев.



Использование акселерометра в режиме Portrait

Мы будем изменять уже выше созданный проект.
Я выбрал вариант с использованием Core Motion с периодическим опрашиванием акселерометра (Способ 2).

1. Добавляем изображения в проект
Для этого перетаскиваем их в Xcode, в папку Supporting Files.
Ставим галочку Copy items into destination group's folder (if needed).


2. Открываем ViewController.xib
Добавляем Image View, растягиваем его на весь View.
В поле Image выбираем grass.jpg.
Устанавливаем Mode: Aspect Fit.

Добавляем еще один Image View, но уже не на весь экран, а размером 64x64.
Image: soccer-ball.png
Mode: Aspect Fit.

Можно сделать лейбл поверх других элементов. Для этого выберите лейбл и в меню Editor - Arrange - Send Forward.

Должно получиться примерно такое:

Изображение

Трава специально сделана не на весь экран, чтобы мы могли отличить приложение в портретном режиме от приложения в ландшафтном режиме.


3. Связываем мяч в Image View с кодом

Я буду использовать способ с Core Motion с опросом акселерометра по таймеру

ViewController.h

Код: Выделить всё

#import <UIKit/UIKit.h>#import <CoreMotion/CoreMotion.h> @interface ViewController : UIViewController {    IBOutlet UILabel *label;    CMMotionManager *motionManager;    NSTimer *updateTimer;    IBOutlet UIImageView *ball;} @property (retain, nonatomic) IBOutlet UILabel *label;@property (retain, nonatomic) CMMotionManager *motionManager;@property (retain, nonatomic) NSTimer *updateTimer;@property (retain, nonatomic) IBOutlet UIImageView *ball; @end

ViewController.m

Код: Выделить всё

#import "ViewController.h" @implementation ViewController@synthesize label;@synthesize motionManager;@synthesize updateTimer;@synthesize ball; //... - (void)viewDidLoad{    [super viewDidLoad];        motionManager = [[CMMotionManager alloc]init];        if (motionManager.accelerometerAvailable) {        motionManager.accelerometerUpdateInterval = 1.0f / 60.0f;        [motionManager startAccelerometerUpdates];    } else {        label.text = @"No accelerometer";    }        updateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f / 60.0f                                                   target:self                                                 selector:@selector(accelUpdate)                                                 userInfo:nil                                                  repeats:YES];} -(void)accelUpdate{    if(motionManager.accelerometerAvailable) {        CMAccelerometerData *accelerometerData = motionManager.accelerometerData;        label.text = [NSString stringWithFormat:                      @"Accelerometer \nx:%f \ny:%f \nz:%f",                      accelerometerData.acceleration.x,                      accelerometerData.acceleration.y,                      accelerometerData.acceleration.z];                        CGPoint delta; // координаты положение мяча        // умножаем значении акселерометра на 10, чтобы мяч двигался быстрее        delta.x = accelerometerData.acceleration.x * 10;        delta.y = accelerometerData.acceleration.y * 10;                // задаем мячу новые координаты        ball.center = CGPointMake(ball.center.x + delta.x, ball.center.y - delta.y);                        // делаем границы, чтобы мяч не скрылся за пределы экрана        if (ball.center.x < 0) {            // слева            ball.center = CGPointMake(0, ball.center.y);        } else if (ball.center.x > 320) {            // справа            ball.center = CGPointMake(320, ball.center.y);        }        if (ball.center.y < 0) {            // сверху            ball.center = CGPointMake(ball.center.x, 0);        } else if (ball.center.y > 480) {            // снизу            ball.center = CGPointMake(ball.center.x, 480);        }                   }    }
В данном приложении нам не нужно значение оси Z.
Можете запустить и проверить работу приложения.

Расчет идет от цента мяча, поэтому он на половину скрывается. Чтобы это исправить, нужно сделать дополнительные вычисления.

ball.image.size.width - позволяет узнать оригинальный размер изображения в Image View
ball.bounds.size.width - позволяет узнать размер самого Image View.

Изменяем код:

Код: Выделить всё

    // делаем границы, чтобы мяч не скрылся за пределы экрана    // половина ширины изображения с мячом    int ballHalfWidth = ball.bounds.size.width/2;        if (ball.center.x < 0+ballHalfWidth) {        // слева        ball.center = CGPointMake(0+ballHalfWidth, ball.center.y);    } else if (ball.center.x > 320-ballHalfWidth) {        // справа        ball.center = CGPointMake(320-ballHalfWidth, ball.center.y);    }    if (ball.center.y < 0+ballHalfWidth) {        // сверху        ball.center = CGPointMake(ball.center.x, 0+ballHalfWidth);    } else if (ball.center.y > 480-ballHalfWidth) {        // снизу        ball.center = CGPointMake(ball.center.x, 480-ballHalfWidth);    }
Запустим и проверим приложение.

Изображение

Мяч двигается не реалистично, но это не было целью.


Использование акселерометра в режиме Landscape

Переделываем приложение из портретной ориентации в ландшафтную.

1. Открываем свойства проекта
Targets - Summary
Выбираем только ландшафтную ориентацию Landscape Right.


2. Открываем Ball-Info.plist
Добавляем строку
Key: Initial interface orientation
Value: Landscape (right home button)


3. Открываем ViewController.xib
Выбираем View. В свойствах выбираем
Size: iPhone/iPod touch Full screen
Orientation: Landscape
Можно поменять расположение объектов.


4. Открываем ViewController.m
Изменяем код на

Код: Выделить всё

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {    // Return YES for supported orientations    return (interfaceOrientation == UIInterfaceOrientationLandscapeRight);}
Теперь приложение будет работать в ландшафтном режиме. Можно запустить и проверить. Мяч будет вести себя не так как при портретной ориентации. Это связано с тем, что значения акселерометра всегда возвращаются относительно портретного расположения интерфейса, а координаты считаются в зависимости от ориентации интерфейса.

Меняем часть кода, отвечающую за передвижение мяча и определяющую границы и добавим вывод текущих координат положения центра мяча

Код: Выделить всё

        label.text = [NSString stringWithFormat:                      @"Accelerometer \nx:%f \ny:%f \nz:%f \nBall \nx:%f \ny:%f",                      accelerometerData.acceleration.x,                      accelerometerData.acceleration.y,                      accelerometerData.acceleration.z,                      ball.center.x,                      ball.center.y];                        CGPoint delta; // координаты положение мяча        // умножаем значении акселерометра на 10, чтобы мяч двигался быстрее        delta.x = accelerometerData.acceleration.x * 10;        delta.y = accelerometerData.acceleration.y * 10;                ball.center = CGPointMake(ball.center.x - delta.y, ball.center.y - delta.x);        // делаем границы, чтобы мяч не скрылся за пределы экрана        // половина ширины изображения с мячом        int ballHalfWidth = ball.bounds.size.width/2;                if (ball.center.x < 0+ballHalfWidth) {            // слева            ball.center = CGPointMake(0+ballHalfWidth, ball.center.y);        } else if (ball.center.x > 480-ballHalfWidth) {           // справа           ball.center = CGPointMake(480-ballHalfWidth, ball.center.y);        }        if (ball.center.y < 0+ballHalfWidth) {            // сверху            ball.center = CGPointMake(ball.center.x, 0+ballHalfWidth);        } else if (ball.center.y > 320-ballHalfWidth) {            // снизу            ball.center = CGPointMake(ball.center.x, 320-ballHalfWidth);        }
Изображение

Теперь акселерометр работает корректно.
Ответить