IntelliJ IDEAでScala on Android using sbt Part.4[ビデオプレーヤー]
Scala on Androidの練習に、簡単な動画再生アプリを書きました。
IDEはIntelliJ 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.scala
のviewVideoThumbnail
メソッドでGridView
にSDカードに保存された動画ファイルのサムネイルを表示し、選択した動画をVideoPlayerActivity.scala
で再生します。
Adapterクラスを記述したGridViewAdapter.scala
では、GridView
に設置する動画ファイルのサムネイルの生成と、格子状に並んだ各View
へのサムネイルの設置を管理しています。
アプリ名は、res/values/strings.xmls
のapp_name
と、build.sbt
のname :=
で定義しています。
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の実行結果
うまくいけば、選択した動画の再生と一時停止ができます。
参考記事
ScalaやAndroidに関しては、こちらの記事を参考にさせていただきました。
- Scalacheat - Scala Documentation
- Scala tryメモ(Hishidama's Scala try Memo)
- Grid View | Android Developers
- BaseAdapter | Android Developers
- VideoView | Android Developers
動画再生アプリの作成に関しては、こちらの記事を参考にさせていただきました。
- Androidコンポーネント初級編#4 : GridViewの使いかた | Developers.IO
- adapterクラスを使う時 - 素人のアンドロイドアプリ開発日記
- VideoViewで動画を再生する « Tech Booster
開発環境
- OSX 10.11.6 El Capitan
- IDE: InteiijJ IDEA Community Edition 2016.2
- Java Development Kit: Java SE Development Kit 8u101
- Android SDK Tools: android-sdk_r24.4.1-macosx
- Android Virtual Device: Android 6.0(Google APIs) API level 23 - Scala 2.11.8
- ビルドツール: sbt 0.13.12