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
    }
}

0 件のコメント: