2017年2月18日土曜日
CMMotionManagerのサンプル(2)
AppStoreに載せているPanViewerは、パノラマ写真をiPhone/iPadを水平方向に回転させて見るアプリです。
このアプリはCMMotionManagerをからGyroデータ取得し、デバイスの回転角度に応じて画像を移動させています。
その方法について紹介ですが、今回は前回のサンプルの次の問題点に対処します。
・画像移動がギクシャクする。
・画像が画面からはみ出すとその部分が空白になる。
前者についてはanimationを利用して画像移動をスムーズにします。
→ func moveImageLayer 参照
後者については同じ画像をセットしたUIImageViewを3つ並べ、空白が生じないようにします。
→ func setImageViewSize 参照
また、移動後の位置が画面からはみ出す場合に位置調整を行います。
→ func newPosition 参照
今回のサンプルアプリのプロジェクトはここからダウンロードできます。
以下、前回との変更点だけ載せます。
//画像を表示するUIImageView。画像が画面からはみ出した時に画像を繋げて表示するために3つ使う。
@IBOutlet weak var imageViewL: UIImageView? //Left
@IBOutlet weak var imageViewC: UIImageView? //Center
@IBOutlet weak var imageViewR: UIImageView? //Right
//3つのUIImageViewのsuperview。
//画像移動はimageViewContainerのlayerを用いて行う。
//位置判定が容易になるよう、frame.originは(0,0)とし、サイズはimageViewCと同じにする。
@IBOutlet weak var imageViewContainer: UIView?
//中央のUIImageViewの左右の端が画面に入ると画像外の部分が空白になる。
//その空白を埋めるために左右にもUIImageViewを置き、同じ画像をセットする。
func setImage(imageName: String) {
if let image = UIImage(named: imageName) {
if self.imageViewC!.image != image {
self.imageViewL!.image = image; //左側
self.imageViewC!.image = image; //中央
self.imageViewR!.image = image; //右側
}
}
}
//3つのUIImageViewとimageViewContainerが画面にフィットするようにサイズ、位置を設定する。
func setImageViewSize(viewSize: CGSize) {
self.adjX = 0
//originX: 3つのUIImageViewのorigin.xを設定するために用いる。
var originX: CGFloat = 0;
//3つのimageView及びimageViewContainerのサイズを設定する。
for imageView in [self.imageViewL!, self.imageViewC!, self.imageViewR!] {
if let image = imageView.image {
//imageViewが画面にフィットするようにサイズを設定する。
imageView.frame.size.width = image.size.width * viewSize.height / image.size.height
imageView.frame.size.height = viewSize.height
imageView.frame.origin.y = 0;
//origin.xを-imageView.frame.size.width, 0, imageView.frame.size.widthの順にセット。
imageView.frame.origin.x = originX - imageView.frame.size.width;
originX += imageView.frame.size.width;
//imageViewCに合わせてimageViewContainerのサイズを設定。
if imageView == self.imageViewC {
self.imageViewContainer!.frame.size = self.imageViewC!.frame.size
self.imageViewContainer!.layer.position.x = 0
}
}
}
}
//self.imageViewContainer.layerの位置を変えることで画像を水平方向に移動する。
//画像移動をスムーズにするためanimationを使用する。
func moveImageLayer() {
let (from, to) = self.newPosition()
let anim: CABasicAnimation = CABasicAnimation(keyPath: "position")
anim.fromValue = from
anim.toValue = to
anim.duration = self.interval
//animation終了後に初期位置に戻るので、layer.positionを移動後の位置にしておく。
self.imageViewContainer!.layer.position = to
self.imageViewContainer!.layer.add(anim, forKey:"move-layer")
}
//デバイスの回転から画像の表示位置を計算する。
func newPosition() -> (CGPoint, CGPoint) {
var from = self.imageViewContainer!.layer.position
//deviceMotionのquaternionの値から回転角を求め、水平方向の移動量を計算する。
if let deviceMotion = self.motionManager?.deviceMotion {
let attitude: CMAttitude = deviceMotion.attitude
let q: CMQuaternion = attitude.quaternion
//angleはimageの視野角。360、180など。iPhoneで撮影したパノラマ写真なら180。
let tx = (CGFloat)(atan2(q.y, q.x) / Double.pi * (360 / angle))
let w = self.imageViewC!.frame.size.width
var x = w * tx + adjX;
//初期状態ではadjX=0。この場合、画像表示位置と計算上の位置が一致するようにadjXを設定する。
if adjX == 0 {
adjX = from.x - x
x = from.x
}
let maxX = w / 2
var posAdjusted = false
//mageViewCの移動後、画面に欠ける部分ができる場合画面を覆う位置に調整する。
//atan2の値はpiから-pi、またはその逆へ非連続に変化する。
//視野角により画像のwidthより大きな変化(180度の場合はwidth*2前後)になるため、whileで適正位置に移動するまでループする。
if x > maxX {
//mageViewCが画面の右端から画像が消える位置に移動する場合。
while(x > maxX) {
adjX -= w
x -= w
}
posAdjusted = true
} else if x < -maxX {
//mageViewCが画面の左端から画像が消える位置に移動する場合。
while(x < -maxX) {
adjX += w
x += w
}
posAdjusted = true
}
if posAdjusted {
//位置調整が行われた場合、layerの移動量がmaxX以上になったら調整後の位置に合わせてlayerのanimation開始位置も調整する。
//少々いい加減な判断だが、よほど激しく回転させなけばチラつきは発生しない。
let dx = x - self.imageViewContainer!.layer.position.x
if dx > maxX {
from.x += w;
} else if dx < -maxX {
from.x -= w;
}
//位置調整時はanimationを使用しない。
self.imageViewContainer!.layer.position.x = from.x
}
return (from, CGPoint(x:x, y:from.y))
}
return (from, from)
}
2017年2月16日木曜日
CMMotionManagerのサンプル(1)
AppStoreに載せているPanViewerは、パノラマ写真をiPhone/iPadを水平方向に回転させて見るアプリです。
このアプリはCMMotionManagerをからGyroデータ取得し、デバイスの回転角度に応じて画像を移動させています。その方法について紹介します。
今回はロジックを単純にし、肝心な部分をわかりやすくしています。
ポイントは次の3点です。
・CMMotionManagerからdeviceMotionのデータを取得する。
・attitude.quaternionのx、yの値からatan2関数でデバイスの水平方向の角度を得る。
・画像を角度に応じて水平方向に移動させる。
正直なところ、attitude.quaternionの値とatan2関数を使う方法は試行錯誤の結果たどり着いたものです。このアプリではデバイスを自分の前に構えて体を回転させ、それに応じて画像を動かすことを意図しています。attitude.rollでは体は動かさず手で回転させた場合は近い動作になりますが、体を回転させた場合は期待通りになりません。また、回転軸がデバイスの縦軸のため、横にした場合はattitude.pitchの方が近い動作になり、対応困難になります。
このサンプルでは次の問題があります。
・画像移動がギクシャクする。
・画像が画面からはみ出すとその部分が空白になる。
これらについては次回のサンプルで対処します。
サンプルアプリのプロジェクトはここからダウンロードできます。
以下はプロジェクト中のViewControllerのコード(一部を省略)です。このサンプルでは画像移動に関するコードがあるのはこのクラスだけです。
//CoreMotionをインポート
import UIKit
import CoreMotion
//このサンプルでは表示画面はViewControllerだけ
class ViewController: UIViewController {
//CMMotionManagerのインスタンスをセットする。
var motionManager: CMMotionManager?
//画像を表示するUIImageView。画像の左右の端が画面内にある時に画像を繋げて表示するために3つ使う。
@IBOutlet weak var imageView: UIImageView? //Center
//3つのUIImageViewのsuperview。
//画像移動はimageViewContainerのlayerを用いて行う。
//位置判定が容易になるよう、frame.originは(0,0)とし、サイズはimageViewと同じにする。
@IBOutlet weak var imageViewContainer: UIView?
//motionManagerの値から計算した位置を画像の画像位置に合わせるための調整値。
var adjX: CGFloat = 0
//画像視野角。iPhoneで撮影したパノラマ写真の場合は概ね180度。
let angle = 180.0
//motionManagerから位置情報を取得するタイマーイベント間隔。
let interval = 0.1
//viewWillAppearで画像設定
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.imageView!.frame = self.view.frame
self.imageViewContainer!.frame = self.view.frame
self.setImage(imageName: "ashinoko.jpg");
self.setImageViewSize(viewSize: self.view.frame.size)
}
//画面表示後にmoveImageを呼ぶ。moveImageはタイマーイベントでループ実行する。
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
moveImage()
}
//デバイス回転時に縦、横の画面サイズに合わせてImageViewのサイズを変更する。
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
self.setImageViewSize(viewSize: size)
}
//motionManagerをスタートさせる。
func startMotionManger() {
if self.motionManager == nil {
self.motionManager = CMMotionManager()
}
self.motionManager!.startDeviceMotionUpdates()
}
//motionManagerをストップさせる。
func stopMotionManger() {
NSObject.cancelPreviousPerformRequests(withTarget:self);
if let motionManager = self.motionManager {
motionManager.stopDeviceMotionUpdates()
}
self.motionManager = nil;
}
//imageViewに画像をセットする。
func setImage(imageName: String) {
if let image = UIImage(named: imageName) {
if self.imageView!.image != image {
self.imageView!.image = image;
}
}
}
//3つのUIImageViewとimageViewContainerが画面にフィットするようにサイズ、位置を設定する。
func setImageViewSize(viewSize: CGSize) {
self.adjX = 0
//3つのimageView及びimageViewContainerのサイズを設定する。
if let imageView = self.imageView {
if let image = imageView.image {
//imageViewが画面にフィットするようにサイズを設定する。
imageView.frame.size.width = image.size.width * viewSize.height / image.size.height
imageView.frame.size.height = viewSize.height
//imageViewに合わせてimageViewContainerのサイズを設定。
self.imageViewContainer!.frame.size = imageView.frame.size
self.imageViewContainer!.layer.position.x = 0
}
}
}
//タイマーイベントで実行し、デバイスの回転に応じて画像を水平方向に移動する。
func moveImage() {
if self.motionManager != nil {
self.moveImageLayer()
} else {
self.startMotionManger();
}
self.perform(#selector(moveImage), with: nil, afterDelay: interval)
}
//self.imageViewContainer.layerの位置を変えることで画像を水平方向に移動する。
//animationなし。ロジックはシンプル。
func moveImageLayer() {
let to = self.newPosition()
self.imageViewContainer!.layer.position = to
}
func newPosition() -> CGPoint {
let from = self.imageViewContainer!.layer.position
//deviceMotionのquaternionの値から回転角を求め、水平方向の移動量を計算する。
if let deviceMotion = self.motionManager?.deviceMotion {
let attitude: CMAttitude = deviceMotion.attitude
let q: CMQuaternion = attitude.quaternion
//angleはimageの視野角。360、180など。iPhoneで撮影したパノラマ写真なら180。
let tx = (CGFloat)(atan2(q.y, q.x) / Double.pi * (360 / angle))
let w = self.imageView!.frame.size.width
var x = w * tx + adjX;
//初期状態ではadjX=0。この場合、画像表示位置と計算上の位置が一致するようにadjXを設定する。
if adjX == 0 {
adjX = from.x - x
x = from.x
}
return CGPoint(x:x, y:from.y)
}
return from
}
}
登録:
投稿 (Atom)