あかんわ

ブログで日誌でも書くようにすれば多少はやる気が出るかと思ったんです

IntelliJ IDEAでScala on Android using sbt Part.4[ビデオプレーヤー]

Scala on Androidの練習に、簡単な動画再生アプリを書きました。

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

目次

Scala on Androidでビデオプレーヤー

SDカードに保存された動画のサムネイルをGridViewで表示し、選択した動画を再生するアプリです。

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

プロジェクトディレクトリの構成
~/VideoPlayer
    |- 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          // 通常画面レイアウトの定義ファイル
    |    |    |    |    |- activity_videoplayer.xml   // 動画再生画面レイアウトの定義ファイル
    |    |    |    |- /values                  // 配色や文字の定義ファイルを配置するディレクトリ
    |    |    |                                
    |    |    |- /scala                        // Scalaのコードを配置するディレクトリ
    |    |         |- /com.b0npu.videoplayer             // アプリのパッケージディレクトリ
    |    |              |- MainActivity.scala            // アプリのメインファイル
    |    |              |- GridViewAdapter.scala         // Viewの表示を管理するクラス
    |    |              |- VideoPlayerActivity.scala     // 動画を再生するためのクラス
    |    |                                
    |    |- /test                              // テストコードを配置するディレクトリ
    |                             
    |- /target                                 // ビルドで生成された成果物の出力先ディレクトリ
主なソースコード

MainActivity.scalaviewVideoThumbnailメソッドでGridViewにSDカードに保存された動画ファイルのサムネイルを表示し、選択した動画をVideoPlayerActivity.scalaで再生します。
Adapterクラスを記述したGridViewAdapter.scalaでは、GridViewに設置する動画ファイルのサムネイルの生成と、格子状に並んだ各Viewへのサムネイルの設置を管理しています。
アプリ名は、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.videoplayer">
    <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=".MainActivity">

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

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

        <activity android:name=".VideoPlayerActivity"
                  android:label="@string/app_name">
        </activity>

    </application>

</manifest>
MainActivity.scala
package com.b0npu.videoplayer

import android.Manifest
import android.content.pm.PackageManager
import android.content.{DialogInterface, Intent}
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.support.v4.app.ActivityCompat
import android.support.v4.content.PermissionChecker
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.view.{Gravity, View}
import android.widget.{AdapterView, TextView}

/**
  * アプリ起動時の画面を表示するクラス
  *
  * アプリの画面を生成するonCreateメソッドでviewVideoThumbnailメソッドを呼び
  * SDカードに保存されている動画ファイル(mp4ファイル) のサムネイルをGridViewで表示する
  */
class MainActivity extends AppCompatActivity with TypedFindView {

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

  /**
    * アプリの画面を生成
    *
    * アプリを起動するとonCreateが呼ばれてActivityが初期化される
    * 動画の表示に必要なパーミッション(SDカードのデータの読み込み)を確認して
    * パーミッションが許可されていない場合はrequestReadStoragePermissionメソッドで
    * パーミッションの許可を要求する
    * パーミッションが許可されていればviewVideoThumbnailメソッドで
    * 動画ファイル(mp4ファイル)のサムネイルを表示する
    */
  override def onCreate(savedInstanceState: Bundle): Unit = {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
      != PackageManager.PERMISSION_GRANTED) {
      requestReadStoragePermission
    } else {
      viewVideoThumbnail
    }
  }

  /**
    * viewVideoThumbnailメソッドの定義
    *
    * SDカードの動画ファイル(mp4ファイル)を読み込んでGridViewにサムネイルを表示し
    * サムネイルを選択すると動画を再生する
    * GridViewへのサムネイルの配置はGridViewAdapter(BaseAdapterを継承したサブクラス)を
    * 使うのでGridViewにGridViewAdapterを設置してサムネイルを表示する
    */
  private def viewVideoThumbnail: Unit = {

    /* レイアウトに設置したgridViewのidを変数に格納する */
    val gridView = findView(TR.gridView)

    /* GridViewにGridViewAdapterを設置して画面にサムネイルを表示する */
    val gridViewAdapter = new GridViewAdapter(MainActivity.this)

    if (gridViewAdapter.getCount > 0) {
      /* SDカードに動画ファイルがあればサムネイルを表示する */
      gridView.setAdapter(gridViewAdapter)

    } else {
      /* SDカードに動画ファイルが無くて表示するサムネイルが無い場合は通知する */
      val textView = new TextView(MainActivity.this)
      textView.setGravity(Gravity.CENTER)
      textView.setTextSize(48)
      textView.setText("No Media File")
      setContentView(textView)
    }

    /* サムネイルを選択したら動画ファイル(mp4ファイル)をVideoPlayerActivityで再生する */
    gridView.setOnItemClickListener(new AdapterView.OnItemClickListener {

      override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
        /* インテントにVideoPlayerActivityクラスと動画のIDを指定してVideoPlayerの画面を開く */
        val videoPlayerIntent = new Intent(MainActivity.this, classOf[VideoPlayerActivity])
        videoPlayerIntent.putExtra("id", parent.getItemIdAtPosition(position))
        startActivity(videoPlayerIntent)
      }
    })

  }

 ・
 ・ ここから下はM Permissionsのためのコードが長々と続くだけなので省略します
 ・
GridViewAdapter.scala
package com.b0npu.videoplayer

import android.content.Context
import android.database.Cursor
import android.graphics.{Bitmap, BitmapFactory}
import android.net.Uri
import android.provider.{BaseColumns, MediaStore}
import android.util.Log
import android.view.{View, ViewGroup}
import android.widget.{BaseAdapter, ImageView}

/**
  * 動画ファイルのサムネイルをGridViewに表示するためのAdapterクラス(BaseAdapterのサブクラス)
  *
  * GridViewを表示するActivityの情報(Context)を引数で受取り格子状にViewを表示する
  * 格子状に表示したViewにはImageViewを配置し動画ファイル(mp4ファイル)のサムネイルを表示する
  */
class GridViewAdapter(context: Context) extends BaseAdapter {

  /**
    * フィールドの定義
    *
    * コンストラクタの引数(Context)を格納する定数を定義
    * GridViewに表示するサムネイルを取得するために必要な変数も定義する
    * (自クラスで使うだけのフィールドはprivateにして明示的に非公開にしてます)
    */
  private val videoThumbnailContext: Context = context

  /* 動画ファイルのIDとファイル名を格納する配列を定義 */
  private var videoFileIdArray: Array[Long] = Array.empty
  private var videoFileNameArray: Array[String] = Array.empty

  /* SDカードの動画ファイルのURIに問い合わせをして検索結果をCursorに格納する */
  private val videoMediaStoreUri: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
  private val videoThumbnailContextResolver = videoThumbnailContext.getContentResolver
  private val videoFileCursor: Cursor = videoThumbnailContextResolver.query(videoMediaStoreUri, null, null, null, null)
  /* 動画ファイルが無かった場合にFATAL EXCEPTIONで異常終了したりするので例外処理の中でCursorの中身を取得する */
  try {
    /* Cursorに格納した動画ファイルの検索結果の先頭から順に動画ファイルのIDとファイル名を取得し配列に格納する */
    videoFileCursor.moveToFirst
    do {
      /* 動画ファイルのIDとファイル名を取得する */
      val videoFileId = videoFileCursor.getLong(videoFileCursor.getColumnIndex(BaseColumns._ID))
      val videoFileName = videoFileCursor.getString(videoFileCursor.getColumnIndex(MediaStore.MediaColumns.TITLE))

      videoFileIdArray :+= videoFileId
      videoFileNameArray :+= videoFileName

    } while (videoFileCursor.moveToNext)

  } catch {
    case e: Exception ⇒ Log.v("Error", s"$e")
  }

  /**
    * getViewメソッドをオーバーライド
    *
    * このメソッドはGridViewの格子状の各ViewにImageViewを表示するメソッドで
    * 引数のconvertViewに表示可能なViewが無ければImageViewを生成して表示する
    * アプリの起動時等で表示可能なconvertViewが無ければ
    * 動画ファイル(mp4ファイル)のサムネイル(ビットマップ画像)を作成して
    * ImageViewに設置しGridViewに配置する
    */
  override def getView(position: Int, convertView: View, parent: ViewGroup): View = {

    val imageView: ImageView = new ImageView(videoPlayerContext)

    if (convertView == null) {

      /* 選択された動画ファイルのサムネイルを取得して適当な大きさにする */
      val thumbnailBitmap: Bitmap = MediaStore.Video.Thumbnails.getThumbnail(
        videoThumbnailContextResolver,
        videoFileIdArray(position),
        MediaStore.Video.Thumbnails.MINI_KIND,
        new BitmapFactory.Options
      )
      val resizeThumbnail: Bitmap = Bitmap.createScaledBitmap(thumbnailBitmap, 320, 180, true)

      /* サムネイルをImageViewに設置してGridViewに渡す */
      imageView.setImageBitmap(resizeThumbnail)
      imageView

    } else {
      /* 表示できるconvertViewがあればconvertViewをGridViewに渡す */
      convertView
    }
  }

  /**
    * getItemメソッドをオーバーライド
    *
    * このメソッドはGridViewのpositionにあるItemを取得するメソッドで
    * positionの位置にあるサムネイルの動画ファイル名を取得する
    */
  override def getItem(position: Int): AnyRef = {
    videoFileNameArray(position)
  }

  /**
    * getItemIdメソッドをオーバーライド
    *
    * このメソッドはGridViewのpositionにあるItemのIdを取得するメソッドで
    * positionの位置にあるサムネイルの動画ファイルのIdを取得する
    */
  override def getItemId(position: Int): Long = {
    videoFileIdArray(position)
  }

  /**
    * getCountメソッドをオーバーライド
    *
    * このメソッドはGridViewに配置されたViewの数を取得するメソッドで
    * videoFileIdArrayに格納した動画ファイルの数を取得する
    */
  override def getCount: Int = {
    videoFileIdArray.length
  }

}
VideoPlayerActivity.scala
package com.b0npu.videoplayer

import android.content.Intent
import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.support.v7.app.AppCompatActivity
import android.widget.{MediaController, VideoView}

/**
  * ビデオプレーヤーの画面を表示するクラス
  *
  * アプリの画面を生成するonCreateメソッドでMainActivityからIntentを受取り
  * GridViewで選択された動画ファイル(mp4ファイル)を再生する
  */
class VideoPlayerActivity extends AppCompatActivity with TypedFindView {

  /**
    * アプリの画面を生成
    *
    * アプリを起動するとonCreateが呼ばれてActivityが初期化される
    * 選択された動画ファイル(mp4ファイル)のIDをIntentから取得し
    * VideoViewで動画を再生する
    */
  override def onCreate(savedInstanceState: Bundle): Unit = {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_videoplayer)

    /* レイアウトに設置したvideoViewのidを取得してVideoViewにコントローラを配置する */
    val videoView: VideoView = findView(TR.videoView)
    videoView.setMediaController(new MediaController(VideoPlayerActivity.this))

    /* Intentから動画ファイルのIdを取得し再生する動画ファイルのURIをVideoViewに渡す */
    val videoFileIntent: Intent = getIntent
    val videoFileId = videoFileIntent.getLongExtra("id", 0)
    val videoFileUri = Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoFileId.toString)
    videoView.setVideoURI(videoFileUri)

    /* VideoViewが動画ファイルの読込みを終えたら動画を再生する */
    videoView.setOnPreparedListener(new OnPreparedListener {
      override def onPrepared(mediaPlayer: MediaPlayer): Unit = {
        mediaPlayer.start
      }
    })
  }
}
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.videoplayer.MainActivity">

    <GridView
            android:id="@+id/gridView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:numColumns="auto_fit"
            android:verticalSpacing="10dp"
            android:horizontalSpacing="10dp"
            android:stretchMode="columnWidth"
            android:gravity="center"  />

</RelativeLayout>
activity_videoplayer.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <VideoView android:id="@+id/videoView"
               android:layout_centerVertical="true"
               android:layout_width="match_parent"
               android:layout_height="match_parent"/>

</RelativeLayout>
res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Video Player</string>
</resources>
build.sbt
・
・
・
name := "VideoPlayer"
・
・
・
Run/Debugの実行結果

うまくいけば、選択した動画の再生と一時停止ができます。
f:id:b0npu:20161106121602g:plain

参考記事

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

動画再生アプリの作成に関しては、こちらの記事を参考にさせていただきました。

開発環境

関連記事