Android Tips #27 PhotoSphere Viewer を作ってみる

2012.12.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

PhotoSphere とは?

PhotoSphere は GoogleMap のストリートビューのような、自分が球体の中に居るような感覚で閲覧することができる写真のことです。 Nexus7には背面カメラがないので残念ながら撮影できませんが、今後国内でも発売されるであろう Nexus4 や Nexus10 といった Android 4.2 のカメラアプリに搭載された新機能!です。また PhotoSphere の画像は XMP (アドビシステムズからオープン・ソースライセンスで提供されている XML 形式で構成されるメタデータフォーマット) が埋め込まれた JPEG 画像になっています。
↓こんな感じで撮影するそうです。撮影に時間が必要そうですが、ペタペタ貼っていく感じで楽しそうですね!

[swf]http://www.youtube.com/watch?feature=player_embedded&v=0poff-mHQ4Q, 800, 600[/swf]

今回は GooglePlayServices に新たに追加された PhotoSphere API を利用して、PhotoSphere Viewer を作ってみたいと思います!

PhotoSphere Viewer の作りかた

1. GooglePlayServices ライブラリをインポートする

まずは最新の GooglePlayServices ライブラリをインポートします。インポート方法はこちらにまとめてあるので参照してください。Google API Key の取得は不要です!

2. プロジェクトを作成する

次に新しい Android プロジェクトを作成します。プロジェクトを作成したら先ほどワークスペースにインポートした GooglePlayServices ライブラリプロジェクトをライブラリとして設定します。

photosphere01

AndroidManifest.xml の編集は特に必要ありません。また、レイアウトも不要です。

2. Activity をつくる

次に Activity の実装に入ります。まずは PanoramaClient クラスをインスタンス化しましょう。コンストラクタ引数は以下のとおりです。

第一引数 Context getApplicationContext() を渡す
第二引数 ConnectionCallbacks GooglePlayService の接続に成功したときのコールバック
第三引数 OnConnectionFailedListener GooglePlayService の接続に失敗したときのコールバック

あとは onStart() のタイミングで PanoramaClient#connect() で GooglePlayServices に接続、 ConnectionCallbacks#onConnected() のときに PanoramaClient#loadPanoramaInfo() を呼びます。loadPanoramaInfo() に渡す第一引数は OnPanoramaInfoLoadedListener で、画像の読み込みが完了したときに呼ばれるコールバックです。渡される ConnectionResult の isSuccess() で成功したか失敗したか判別できます。成功した場合は、渡される Intent を使って startActivity() します。また第二引数には Uri を渡します。ここではリソース内の raw フォルダ内の画像を参照しています。JPEG画像が読み込めますが、PhotoSphereの指定の形式で保存されている必要があります。どの画像でも見れるわけではないのでご注意ください。
ここまでで実装完了です!

MainActivity.java

package jp.classmethod.android.sample.photosphere;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
import com.google.android.gms.panorama.PanoramaClient;
import com.google.android.gms.panorama.PanoramaClient.OnPanoramaInfoLoadedListener;

/**
 * MainActivity.
 */
public class MainActivity extends Activity {
    
    /** LogCat出力用タグ. */
    private static final String TAG = MainActivity.class.getSimpleName();

    /** {@link PanoramaClient}. */
    private PanoramaClient mPanoramaClient;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPanoramaClient = new PanoramaClient(getApplicationContext(), connectionCallbacks, onConnectionFailedListener);
    }
    
    @Override
    protected void onStart() {
        super.onStart();
        mPanoramaClient.connect();
    }
    
    /** GooglePlayServices への接続に成功したときのコールバック. */
    private ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks() {
        @Override
        public void onConnected() {
            Log.i(TAG, "onConnected.");
            Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.panorama);
            mPanoramaClient.loadPanoramaInfo(onPanoramaInfoLoadedListener, uri);
        }
        @Override
        public void onDisconnected() {
            Log.i(TAG, "onDisconnected.");
        }
    };
    
    /** GooglePlayServices への接続に失敗したときのコールバック. */
    private OnConnectionFailedListener onConnectionFailedListener = new OnConnectionFailedListener() {
        @Override
        public void onConnectionFailed(ConnectionResult result) {
            Log.e(TAG, "onConnectionFailed.");
        }
    };

    /** PhotoSphere 画像の読み込みが完了したときのコールバック. */
    private OnPanoramaInfoLoadedListener onPanoramaInfoLoadedListener = new OnPanoramaInfoLoadedListener() {
        @Override
        public void onPanoramaInfoLoaded(ConnectionResult result, Intent viewerIntent) {
            if (result.isSuccess()) {
                Log.i(TAG, "found viewerIntent: " + viewerIntent);
                if (viewerIntent != null) {
                    startActivity(viewerIntent);
                }
            } else {
                Log.e(TAG, "error: " + result);
            }
        }
    };
}

実行結果

photosphere02

公式のサンプルソースとほぼ同じ実装になってますが、ご了承くださいw

せっかくなので、ついでにパノラマ画像の中のXMPデータを覗いてみました。

<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about=""
        xmlns:GPano="http://ns.google.com/photos/1.0/panorama/"
      GPano:UsePanoramaViewer="True"
      GPano:ProjectionType="equirectangular"
      GPano:PoseHeadingDegrees="350.0"
      GPano:CroppedAreaLeftPixels="0"
      GPano:FullPanoWidthPixels="4000"
      GPano:CroppedAreaTopPixels="0"
      GPano:FirstPhotoDate="2012-11-07T21:03:13.465Z"
      GPano:CroppedAreaImageHeightPixels="2000"
      GPano:FullPanoHeightPixels="2000"
      GPano:SourcePhotosCount="50"
      GPano:CroppedAreaImageWidthPixels="4000"
      GPano:LastPhotoDate="2012-11-07T21:04:10.897Z"
      GPano:LargestValidInteriorRectLeft="0"
      GPano:LargestValidInteriorRectTop="0"
      GPano:LargestValidInteriorRectWidth="4000"
      GPano:LargestValidInteriorRectHeight="2000"/>
  </rdf:RDF>
</x:xmpmeta>

これを普通のJPEGに埋め込めば、パノラマ画像として見れるかも? CG 画像とかイラストとかに埋め込んだら面白そうですね!

まとめ

PhotoSphere という新しい機能みたいになっていますが、同じ閲覧方法はこれまで GoogleMaps のストリートビューで見ていたのであまり新鮮味が沸かないかも知れません。ですがその写真が自分で撮れる!というところはかなり違いますよね!何より、早く撮影してみたい…。
今回のサンプルで読み込んだ PhotoSphere な画像 (パノラマ画像) は公式のサンプルソースに入っていたサンプルファイルでした。現在は国内で Android 4.2 が搭載され、かつ外側カメラが付いている端末は存在しないので、 PhoroSphere で撮影した画像は海外で対応端末をゲットした人が Web で公開してくれるのを探すしかないような気がします。
ちなみに PhotoSphere な画像は GoogleMap と Google+ で共有できます。まだ海外で撮影された画像しかほとんど共有されていませんが、今後国内でも 4.2 以上の端末が普及すれば、もっと身近な場所や景色なんかも見れそうですね。楽しみにしておきましょう!