vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity5】【Android】【Mac】プラグインで端末のディレクトリにアクセスする 3

前回の続き…のつもりだったのですが、脇道に逸れてしまったお話。
そのまま消してしまうのは悔しいので書き留めておきます。

やったこと

  • Unityでボタンを押した時に、ネイティブ側で端末内にある画像をGridViewで表示する
  • GridViewの画像をタップしたときに、該当画像のパスを取得する

Activityを追加してボタン押下のタイミングで表示する

呼び出す側

特に難しいこともなく、ボタンを押したらプラグイン側のメソッドを呼ぶだけです。

CtrlMain.cs

〜省略〜
void OnGUI()
{
  if(GUI.Button(new Rect(300f, 20f, 200f, 200f), "GetData"))
  {
    _ctrPlugins.ShowImageView();
  }
  〜省略〜

CtrlPlugin.cs

〜省略〜
public void ShowImageView()
{
  // 画像Viewを表示する.
  using(AndroidJavaClass clsPlugin = new AndroidJavaClass(PLUGIN_CLASS_PATH))
  {
    clsPlugin.CallStatic("OpenGridView");
  }
}
〜省略〜

新しいActivityの追加

app > java > jp.controller の上で右クリック > New > Activity > Blank Activity で「ImageViewActivity」というクラスを追加します。

Activityを呼び出す

前々回のトーストの表示と同じく、UIをコントロールしているスレッドで処理を実行するため、「runOnUiThread」を使用します。

PluginConnector.java

package jp.plugincontroller;

import java.io.File;
import android.content.Intent;
import android.os.Environment;
import android.app.Activity;
import android.widget.Toast;
import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;

public class PluginConnector extends UnityPlayerActivity{
  public static void OpenGridView()
  {
    UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
      public void run() {
        // Activityを開く
        Intent intent = new Intent(UnityPlayer.currentActivity.getApplicationContext(), ImageViewActivity.class);
        UnityPlayer.currentActivity.startActivity(intent);
      }
    });
  }
  public static String GetDcimPath()
  {
    File filDcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
    // DCIMディレクトリのパスを返す.
    return (filDcimDir.getPath());
  }
  public static void ShowToast()
  {
    // 戻るボタン押下時にトーストを表示.
    final Activity activity = UnityPlayer.currentActivity;
    activity.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Toast.makeText(activity, "もう一度押すとアプリを終了します", Toast.LENGTH_SHORT).show();
      }
    });
  }
}

いつぞやと同じく、Intentを使って次のActivityを開きます。
ポイントとしては現在のActivityとして「UnityPlayer.currentActivity」を指定する点でしょうか。

ちなみに実はこれだけだとエラーが発生します。

エラー対策

ビルドしたアプリを実行すると、java.lang.ClassNotFoundExceptionが発生してクラッシュしてしまいました。

Android StudioAndroid Device Monitorを使ってエラーメッセージをよく読んでみると、AndroidManifest.xmlに呼びだそうとしているActivityを記述する必要がある、とありました。
なるほど〜。

早速Assets > Plugins > Android のAndroidManifest.xmlに追加してみたところ、ClassNotFoundExceptionは発生しなくなりました。

AndroidManifest.xml

〜省略〜
    < application
        android:theme="@android:style/Theme.NoTitleBar"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true" >
        < activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:label="@string/app_name" >
            < intent-filter >
                < action android:name="android.intent.action.MAIN" / >
                < category android:name="android.intent.category.LAUNCHER" / >
            < /intent-filter >
            < meta-data android:name="unityplayer.UnityActivity" android:value="true" / >
        < /activity >
        < activity
            android:name="jp.plugincontroller.ImageViewActivity" >
        < /activity >
    < /application >
〜省略〜

ただし、今度は「jp/plugincontroller/R$layout」でjava.lang.NoClassDefFoundErrorが発生するようになりました。

どうもプラグインとして作成したjarファイルには PluginController > app > src > res 以下のxmlファイルは含まれていないようです。
※gradleによるjarファイル作成で含めることができるファイルはjarファイルだけのようで、build.gradleをいじってみてもエラーが発生するだけでした。

試しにres以下のファイルをすべて Assets > Plugins > Android に入れると、res > values > style.xml で、 以下のようなエラーが出たため削除した他はとりあえず動作するようになりました。

stderr[
/Users/masanori/workspace/AndroidPluginTest/Temp/StagingArea/android-libraries/app-release/res/values/values.xml:15: error: Error retrieving parent for item: No resource found that matches the given name 'Theme.AppCompat.Light.DarkActionBar'.

※上記のようにすると、動作はするもののビルド時に以下のアラートが表示されるようになりました。
プラグインのファイルを作るときに、「./gradlew aR」でaarを作り、jarやres以下のファイルと置き換えると良さそうですが、style.xmlのエラーが発生するため未解決です。
android-support-v7-appcompat.jar」をライブラリとして追加すれば良いかな、とやってみたのですが解決はせず…。

OBSOLETE - Providing Android resources in Assets/Plugins/Android/res is deprecated, please move your resources to an Android Library. See "Building Plugins for Android" section of the Manual.

画像をサムネ化して一覧表示する

※ほぼ参考サイトからのコピペなので特に載せる必要はないとは思いつつ、画像をタップした時にパスを取得する辺りで戸惑ったため、その辺りを中心に書きます。

準備

以下を参考に、GridViewに画像を表示するためのArrayAdapterを作成します。

画像を表示して、画像タップ時にパスを取得する

activity_image_view.xml

< ?xml version="1.0" encoding="utf-8"? >
< RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="jp.plugincontroller.ImageViewActivity" >
    < GridView android:id="@+id/gridView1"
        android:background="#ffffff"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:numColumns="3"
        android:verticalSpacing="20dp"
        android:horizontalSpacing="20dp" >
    < /GridView >
< /RelativeLayout >

ImageViewActivity.java

package jp.plugincontroller;

import android.app.Activity;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.GridView;
import android.net.Uri;
import android.database.Cursor;

import java.util.ArrayList;

public class ImageViewActivity extends Activity {

  private ArrayList _aryImgPath;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_image_view);

    ArrayList list = load();
    BitmapAdapter adapter = new BitmapAdapter(
        getApplicationContext(), R.layout.list_item,list);

    GridView gridView = (GridView) findViewById(R.id.gridView1);
    gridView.setAdapter(adapter);

    // 画像タップ時のイベント追加.
    gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      public void onItemClick(AdapterView parent, View v, int position, long id) {
        // タップされた画像のパスを取得する.
        System.out.println("Path " + _aryImgPath.get(position));
      }
    });
  }
  private ArrayList load() {
    ArrayList list = new ArrayList();
    _aryImgPath = new ArrayList();

    Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

    cursor.moveToFirst();
    for (int i = 0; i < cursor.getCount(); i++) {
      long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
      // 画像のパスを追加.
      _aryImgPath.add(cursor.getString(cursor.getColumnIndex("_data")));
      list.add(
          /* 画像をサムネ化してリストに追加 */
          MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null)
      );
      cursor.moveToNext();
    }
    return list;
  }
〜省略〜
  • 「cursor.getString(cursor.getColumnIndex("_data"))」とすることで、GridViewのArrayListに画像を追加するタイミングで該当画像のパスを取得できます。
    これを配列に突っ込んでやれば、クリックイベントが発生したタイミングで「int position」を使ってパスが取得できます。
  • 参考サイトを見ると、Cursorにデータを格納する際、「managedQuery」を使っているものもありましたが、現在Deprecatedになったようで、「getContentResolver().query」を使用しています。

f:id:mslGt:20150502120703p:plain

最後に

表示の様子を見れば、それなりにやりたいことはできている気がします(Unity側にパスを渡す、画像タップ後にGridViewを閉じる処理が必要ですが)。

ただ、単純に端末内にある画像を一覧表示して、タップしてパスを取得したい、というだけならIntentを使ってギャラリーを呼び出すという方法があり、 そちらの方が簡単で標準に沿ったものができるということに上記の実装で散々悩んだ後に気づきました...。

まぁ今回の方法も、これはこれで状況によっては有用だと思いますので、必要になったらこの記事を掘り起こしてみようかと思います。

参考サイト

GridViewで画像を表示する

Unityから新しくActivityを開く

Plugin