A Day In The Life

とあるプログラマの備忘録

タイマーを使って電子コンパスの動きを滑らかに見せる方法

iPhone 3GS電子コンパスは角度が変わったときに値を取得することができます。AR系アプリや地図アプリなんかでコンパスの値と連動させてUI部品の位置を変更する場合、角度が変わるタイミングでUI部品を動かすとどうしても動きが「カク」っとなってぎこちない感じになってしまいます。
そこでこの「ぎこちない」動きをタイマーを使って滑らかにする方法を紹介します。

サンプルプログラムの概要

方位磁石っぽい画像を使った簡単なコンパスアプリです。端末の向いている方角によって方位磁石画像が回転します。
コンパスアプリの画面イメージ
ポイントはCLLocationManagerのlocationManager:didUpdateHeading:メソッドのタイミングで画像の回転を行わず、NSTimerを使って一定時間経過ごとに少しづつ画像の角度を変化させるところです。タイミング図で表すと下のようになります。
タイミング図

ヘッダのコード

compassViewが回転させる対象の画像です。correctedDirectionに現在の画像の角度、rawDirectionにコンパスから取得した角度を保持します(correctedは「補正された」rawは「生の」とか「そのままの」という意味です)。

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>

@interface CompassViewController : UIViewController <CLLocationManagerDelegate> {
  UIImageView *compassView;
  UILabel *rawDirectionLabel;
  UILabel *correctedDirectionLabel;
  CLLocationManager *manager;
  CLLocationDirection rawDirection;
  CLLocationDirection correctedDirection;
}

@property (nonatomic, retain) IBOutlet UIImageView *compassView;
@property (nonatomic, retain) IBOutlet UILabel *rawDirectionLabel;
@property (nonatomic, retain) IBOutlet UILabel *correctedDirectionLabel;

@end

実装部分のコード

16ミリ秒ごと(60FPS)にonScheduleメソッドが呼ばれるようにタイマを設定します。locationManager:didUpdateHeading:メソッドでは画像の回転を行わずにonScheduleメソッドで行います。
なお画像を回転させるときに角度をマイナスにしているのは端末の傾き(角度)と画像の回転角度が逆になるためです。

#import "CompassViewController.h"
#define kDirectionFilterFactor 0.1

@implementation CompassViewController

@synthesize compassView;
@synthesize rawDirectionLabel;
@synthesize correctedDirectionLabel;

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
  [super viewDidLoad];
  manager = [[CLLocationManager alloc] init];
  manager.headingFilter = kCLHeadingFilterNone;
  manager.delegate = self;
  [manager startUpdatingHeading];
  [NSTimer scheduledTimerWithTimeInterval:1 / 60.0f
                                   target:self
                                 selector:@selector(onSchedule)
                                 userInfo:nil
                                  repeats:YES];
}

- (void)dealloc {
  [manager stopUpdatingHeading];
  [manager release];
  [compassView release];
  [rawDirectionLabel release];
  [correctedDirectionLabel release];
  [super dealloc];
}

- (void)onSchedule {
  double sub = rawDirection - correctedDirection;
  // 0度をまたいだ時の角度差の計算
  if (sub < -180) {
    sub += 360;
  }
  if (180 < sub) {
    sub -= 360;
  }
  // 目的の角度に徐々に近づいていく
  correctedDirection = sub * kDirectionFilterFactor + correctedDirection;
  // 0 <= Direction < 360の範囲に収まるように修正する
  if (360 <= correctedDirection) {
    correctedDirection -= 360;
  }
  if (correctedDirection < 0) {
    correctedDirection += 360;
  }
  // 画像を回転させる(引数はラジアン角を受けるので変換して渡す)
  compassView.transform = CGAffineTransformMakeRotation(M_PI * - correctedDirection / 180);
  correctedDirectionLabel.text = [NSString stringWithFormat:@"%f", correctedDirection];
}

- (void)locationManager:(CLLocationManager *)manager
         didUpdateHeading:(CLHeading *)newHeading {
  compassDirection = newHeading.magneticHeading;
  compassDirectionLabel.text = [NSString stringWithFormat:@"%f", compassDirection];
}

- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager {
  return YES;
}

@end

kDirectionFilterFactorの値を調節することで滑らかさが変わります。電子コンパスが指し示す角度に一定時間ごとに近づいていくようなイメージです。0.1にすると10回かけてコンパスの指す角度に移動します。0.01だと100回、0.001だと1000回、0.5だと2回といった感じです。

参考

関連アプリ

この記事で紹介した方法を実装したアプリ「COMPASS des COMPASS」をリリースしました。無料です。