あかんわ

覚えたことをブログに書くようにすれば多少はやる気が出るかと思ったんです

IntelliJ IDEAでScala on Android using sbt Part.3[ギャラリーアプリ]

こちらの記事を参考に、画像を順番に表示するだけの簡単なギャラリーアプリを書きました。

IDEIntelliJ IDEA CE 2016.2で、ビルドツールはsbt 0.13.12を使っています。
動作確認は、API level 23のAndroidエミュレータで実施しました。

目次

Scala on Androidでギャラリーアプリ

左右のスワイプで、SDカードに保存された画像を順番に表示するだけのアプリです。

ソースコードGitHubに置いてますので、動かしてみる場合はこちらの記事を参考にして下さい。

プロジェクトディレクトリの構成
~/PictureGallery
    |- build.sbt                               // sbtのビルド定義ファイル
    |- /project                                // sbt関連の設定ファイルを配置するディレクトリ
    |- /src                                    // アプリのモジュールを構成するディレクトリ
    |    |- /main                              // アプリのメインソースセットを配置するディレクトリ
    |    |    |- AndroidManifest.xml           // アプリに関する情報を記述するマニフェストファイル
    |    |    |- /libs                         // 外部ライブラリを配置するディレクトリ
    |    |    |- /res                          // リソースファイルを配置するディレクトリ
    |    |    |    |- /drawable-mdpi           // 中解像度の画像を配置するディレクトリ
    |    |    |    |- /drawable-hdpi           // 高解像度の画像を配置するディレクトリ
    |    |    |    |- /drawable-xhdpi          // 超高解像度の画像を配置するディレクトリ
    |    |    |    |    |- ic_launcher.png     // アプリのアイコン画像
    |    |    |    |- /drawable-xxhdpi         // 超超高解像度の画像を配置するディレクトリ
    |    |    |    |- /drawable-xxxhdpi        // 超超超高解像度の画像を配置するディレクトリ
    |    |    |    |- /layout                  // レイアウトの定義ファイルを配置するディレクトリ
    |    |    |    |    |- activity_main.xml   // 画面レイアウトの定義ファイル
    |    |    |    |- /values                  // 配色や文字の定義ファイルを配置するディレクトリ
    |    |    |                                
    |    |    |- /scala                        // Scalaのコードを配置するディレクトリ
    |    |         |- /com.b0npu.picturegallery          // アプリのパッケージディレクトリ
    |    |              |- PictureViewerActivity.scala   // アプリのメインファイル
    |    |              |- GalleryPagerAdapter.scala     // Viewの表示を管理するクラス
    |    |                                
    |    |- /test                              // テストコードを配置するディレクトリ
    |                             
    |- /target                                 // ビルドで生成された成果物の出力先ディレクトリ
主なソースコード

PictureViewerActivity.scalaviewGalleryPagerメソッドでViewPagerを生成して、レイアウトに設置しています。
ViewPagerに設置するImageViewの生成とImageViewへの画像の設置はAdapterクラスを記述したGalleryPagerAdapter.scalaで管理しています。
GalleryPagerAdapterクラスのaddPictureメソッドでは、直接配列に画像を格納してますので、画像の枚数が多くなると重くなるかもしれません。
アプリ名は、res/values/strings.xmlsapp_nameと、build.sbtname :=で定義しています。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.b0npu.picturegallery">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme">

        <activity android:name="com.b0npu.picturegallery.PictureViewerActivity">

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>
PictureViewerActivity.scala
package com.b0npu.picturegallery

import android.Manifest
import android.content.pm.PackageManager
import android.content.{ContentResolver, ContentUris, DialogInterface, Intent}
import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.provider.{BaseColumns, MediaStore, Settings}
import android.support.v4.app.ActivityCompat
import android.support.v4.content.PermissionChecker
import android.support.v4.view.ViewPager
import android.support.v7.app.{AlertDialog, AppCompatActivity}

class PictureViewerActivity extends AppCompatActivity with TypedFindView {

  /**
    * フィールドの定義
    *
    * requestPermissionsメソッドで権限を要求した際に
    * コールバックメソッドのonRequestPermissionsResultメソッドに渡す定数を定義
    * (自クラスで使うだけのフィールドはprivateにして明示的に非公開にしてます)
    */
  private val REQUEST_READ_STORAGE_PERMISSION_CODE: Int = 0x01

  /**
    * アプリの画面を生成
    *
    * アプリを起動するとonCreateが呼ばれてActivityが初期化される
    * 画像の表示に必要なパーミッション(SDカードのデータの読み込み)を確認して
    * パーミッションが許可されていない場合はrequestReadStoragePermissionメソッドで
    * パーミッションの許可を要求する
    * パーミッションが許可されていればviewGalleryPagerメソッドで画像を表示する
    */
  override def onCreate(savedInstanceState: Bundle): Unit = {
    super.onCreate(savedInstanceState)

    if (PermissionChecker.checkSelfPermission(PictureViewerActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
      != PackageManager.PERMISSION_GRANTED) {
      requestReadStoragePermission
    } else {
      viewGalleryPager
    }
  }

  /** viewGalleryPagerメソッドの定義
    *
    * SDカードの画像を読み込んでViewPagerに配置したImageViewに表示する
    * ImageViewへの画像の配置はGalleryPagerAdapter(PagerAdapterを継承したサブクラス)を
    * 使うので画像を格納したGalleryPagerAdapterをViewPagerにセットして画像を表示する
    */
  private def viewGalleryPager: Unit = {

    /* ViewPagerはインスタンスの生成の代わりにレイアウトXMLに記述する方法でも良い */
    val galleryPager = new ViewPager(PictureViewerActivity.this)
    val galleryPagerAdapter = new GalleryPagerAdapter(PictureViewerActivity.this)

    /* SDカードの画像データのURIに問い合わせをして検索結果をCursorに格納する */
    val imageMediaStoreUri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    val mediaContentResolver: ContentResolver = getContentResolver
    val pictureCursor: Cursor = mediaContentResolver.query(imageMediaStoreUri, null, null, null, null)

    /* Cursorに格納した画像データの検索結果の先頭から順に画像を取得しGalleryPagerAdapterに格納する */
    pictureCursor.moveToFirst
    do {
      /* 画像データのURIとIDから画像(ビットマップ画像)を取得する */
      val pictureId = pictureCursor.getLong(pictureCursor.getColumnIndex(BaseColumns._ID)).asInstanceOf[Int]
      val bmpImageUri: Uri = ContentUris.withAppendedId(imageMediaStoreUri, pictureId)
      val bmpImage: Bitmap = MediaStore.Images.Media.getBitmap(mediaContentResolver, bmpImageUri)

      galleryPagerAdapter.addPicture(bmpImage)

    } while (pictureCursor.moveToNext)

    /* 画像を配置したViewPagerをレイアウトに設置して画面に画像を表示する */
    galleryPager.setAdapter(galleryPagerAdapter)
    setContentView(galleryPager)
  }

  /**
    * openSettingsメソッドの定義
    *
    * インテントを使ってアプリの設定画面を開く
    */
  private def openSettings: Unit = {

    val appSettingsIntent: Intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
    val appPackageUri: Uri = Uri.fromParts("package", getPackageName, null)

    /* インテントにアプリのURIを指定してアプリ情報の画面を開く */
    appSettingsIntent.setData(appPackageUri)
    startActivity(appSettingsIntent)
  }

  /**
    * requestReadStoragePermissionメソッドの定義
    *
    * READ_EXTERNAL_STORAGEのパーミッションの許可(権限取得)を要求する
    * shouldShowRequestPermissionRationaleメソッドを使って
    * 以前にパーミッションの許可を拒否されたことがあるか確認し
    * 拒否されたことがある場合はパーミッションの許可が必要な理由を
    * ダイアログに表示してからパーミッションの許可を要求する
    */
  private def requestReadStoragePermission: Unit = {

    /* パーミッションの許可を拒否されたことがあるか確認する */
    if (ActivityCompat.shouldShowRequestPermissionRationale(PictureViewerActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {

      /* パーミッションの許可を拒否されたことがあれば許可が必要な理由を説明してから許可を要求する */
      new AlertDialog.Builder(PictureViewerActivity.this)
        .setTitle("パーミッションの追加説明")
        .setMessage("このアプリで画像を表示するにはパーミッションが必要です")
        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener {

          override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
            /* パーミッションの許可を要求 */
            ActivityCompat.requestPermissions(
              PictureViewerActivity.this,
              Array[String](Manifest.permission.READ_EXTERNAL_STORAGE),
              REQUEST_READ_STORAGE_PERMISSION_CODE
            )
          }
        })
        .create
        .show

    } else {
      /* 初回要求時か「今後は確認しない」を選択されている場合のパーミッションの許可の要求 */
      ActivityCompat.requestPermissions(
        PictureViewerActivity.this,
        Array[String](Manifest.permission.READ_EXTERNAL_STORAGE),
        REQUEST_READ_STORAGE_PERMISSION_CODE
      )
    }
  }

  /**
    * onRequestPermissionsResultメソッドをオーバーライド
    *
    * このメソッドはrequestPermissionsメソッドのコールバックメソッドで
    * requestPermissionsメソッドでパーミッションの許可を要求した結果を取得する
    * 引数のrequestCodeで要求されたパーミッションを区別し
    * grantResultの要素でパーミッションの許可・不許可を確認する
    */
  override def onRequestPermissionsResult(requestCode: Int, permissions: Array[_root_.java.lang.String], grantResults: Array[Int]): Unit = {

    /* 要求されたパーミッションによって対応が変わるので何のパーミッションか確認する */
    requestCode match {

      case REQUEST_READ_STORAGE_PERMISSION_CODE ⇒
        /* パーミッションの要求が拒否されていた場合はダイアログに表示する */
        if (grantResults.length != 1 || grantResults(0) != PackageManager.PERMISSION_GRANTED) {

          /* 「今後は確認しない」が選択されていなければ再度パーミッションの許可を要求する */
          if (ActivityCompat.shouldShowRequestPermissionRationale(PictureViewerActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {

            new AlertDialog.Builder(PictureViewerActivity.this)
              .setTitle("パーミッション取得エラー")
              .setMessage("画像の表示に必要なパーミッションが取得できませんでした")
              .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener {

                override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
                  requestReadStoragePermission
                }
              })
              .create
              .show

          } else {
            /* 「今後は確認しない」を選択されている場合はアプリの設定画面を開く */
            new AlertDialog.Builder(PictureViewerActivity.this)
              .setTitle("パーミッション取得エラー")
              .setMessage("今後は許可しないが選択されました!!アプリ設定>権限を確認してください(権限をON/OFFすることで状態はリセットされます)")
              .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener {

                override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
                  /* アプリの設定画面を開いて手動で許可してもらう */
                  openSettings
                }
              })
              .create
              .show
          }

        } else {
          /* パーミッションが許可された場合は画像を表示する */
          viewGalleryPager
        }
    }
  }
}
GalleryPagerAdapter.scala
package com.b0npu.picturegallery

import android.content.Context
import android.graphics.Bitmap
import android.support.v4.view.PagerAdapter
import android.view.{View, ViewGroup}
import android.widget.ImageView

/**
  * 画像をViewPagerに表示するためのAdapterクラス(PagerAdapterのサブクラス)
  *
  * ViewPagerを表示するActivityの情報(Context)を引数で受取りViewPagerのPageを生成する
  * ViewPagerのPageにはImageViewを配置しaddPictureメソッドで受取った画像を表示する
  */
class GalleryPagerAdapter(context: Context) extends PagerAdapter {

  /**
    * フィールドの定義
    *
    * コンストラクタの引数(Context)を格納する定数を定義
    * addPictureメソッドで受取った画像(ビットマップ画像)を格納する配列も定義
    * (自クラスで使うだけのフィールドはprivateにして明示的に非公開にしてます)
    */
  private val galleryContext: Context = context
  private var galleryArray: Array[Bitmap] = Array.empty

  /**
    * addPictureメソッドの定義
    *
    * 引数に画像(ビットマップ画像)を受取り配列に格納する
    * TODO: 画像を直接配列に格納しているので画像の枚数が多くなると重くなります
    */
  def addPicture(bitmap: Bitmap): Unit = {
    galleryArray :+= bitmap
  }

  /**
    * instantiateItemメソッドをオーバーライド
    *
    * このメソッドはViewPagerにPageを追加するメソッドで
    * 引数のcontainer(ViewGroup)のpositionの場所にViewを表示する
    * containerに追加したImageViewにaddPictureメソッドで
    * galleryArrayに格納した画像(ビットマップ画像)を配置して
    * ViewPagerに画像を表示する
    */
  override def instantiateItem(container: ViewGroup, position: Int): AnyRef = {

    val bitmapPicture: Bitmap = galleryArray(position)
    val imageView: ImageView = new ImageView(galleryContext)

    container.addView(imageView)
    imageView.setImageBitmap(bitmapPicture)
    imageView
  }

  /**
    * destroyItemメソッドをオーバーライド
    *
    * このメソッドはViewPagerからPageを削除するメソッドで
    * 引数のcontainer(ViewGroup)のpositionの場所にあるobj(View等のObject)を削除する
    */
  override def destroyItem(container: ViewGroup, position: Int, obj: Object): Unit = {
    container.removeView(obj.asInstanceOf[View])
  }

  /**
    * getCountメソッドをオーバーライド
    *
    * このメソッドはViewPagerに追加するViewの数を取得する
    * galleryArrayに格納した画像(SDカードに保存されている全画像)の数を取得する
    */
  override def getCount: Int = {
    galleryArray.length
  }

  /**
    * isViewFromObjectメソッドをオーバーライド
    *
    * このメソッドはViewPagerのPageにViewがあるか確認するメソッドで
    * instantiateItemメソッドで追加されたItem(Object)がViewであればTrueになる
    */
  override def isViewFromObject(view: View, obj: Object): Boolean = {
    view == obj
  }

}
activity_main.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="com.b0npu.picturegallery.PictureViewerActivity">
</RelativeLayout>
res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">PictureGallery</string>
</resources>
build.sbt
・
・
・
name := "PictureGallery"
・
・
・
Run/Debugの実行結果

うまくいけば、画像が表示されて少し嬉しい気持ちになります。
f:id:b0npu:20161010124137g:plain

参考記事

ScalaAndroidに関しては、こちらの記事を参考にさせていただきました。

ギャラリーアプリの作成に関しては、こちらの記事を参考にさせていただきました。

開発環境

関連記事