タイマーを使って電子コンパスの動きを滑らかに見せる方法
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回といった感じです。
参考
- Flashゲーム講座&ASサンプル集【角度の計算について】
Flashは角度を-180〜180度で扱う(CoreLocationは0〜360度)ので多少読み替えが必要ですが基本的な考え方は参考になります。 - ラジアン
- タイミング図(Timing Diagram)