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




0 件のコメント: