2版 2024年11月22日 再生経過時間を表示する機能の追加
1版 2024年10月06日 registerForActivityResultにリファクタリング、ソースコードに不足があり追記など
初版 2021年07月09日
1.概要
1.1 ソースコードについて
最新のソースコードをGithubにて公開しています。
第2項以降の情報は、第1版当時の情報です。
1.2 第1版改訂にあたり
MediaPlayerで音楽プレイヤ―を作ってみました。正確には斎藤著「Androidアプリ開発の教科書Kotlin対応(2019年2月初版第2刷)の第12章の記事を元に、ネット上で音楽プレイヤのソースコードを見つけて、内部ストレージのDownLoadフォルダやGoogleドライブから音源を選び再生できるようにコピペし、改造したことがありました。最近少しKotlinが分かるようになったので、改めてMediaPlayerクラスについて勉強し、また併せてstartActivityForResultが非推奨になったのでregisterForActivityResultにリファクタリングしました。
(2024年10月、ソースコードをGithub上に公開しました。なお、build.gradle#Projectファイルとbulid.gradle#appファイルは、プロジェクトを作り直したため、ktsの形式になっています。)
1.3 第2版改訂にあたり
再生経過時間を表すcurrentPositionを表示できるように改修を行った。概要は「MediaPlayer音楽プレイヤ―にtimerを使ってcurrentPoisitionを表示してみた。」を参照してください。
2.開発環境(第1版当時)
Android Studio 4.2.2
Build #AI-202.7660.26.42.7486908, built on June 24, 2021
Runtime version: 11.0.8+10-b944.6842174 amd64
VM: OpenJDK 64-Bit Server VM by N/A
Windows 10 10.0
3.動作環境(第1版当時)
Androidの動作環境を示す代わりにBuildgradleを示します。
minSdkVersionを24としました。あまり古いSDKバージョンだとストレージの取り扱いが異なるようなので24未満は未対応としました。
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.testmediaplayer"
minSdkVersion 24
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
4.ソースコード(第1版当時)
4.1 レイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btnPlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="116dp"
android:enabled="false"
android:text="@string/play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/STOP"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.161"
app:layout_constraintStart_toEndOf="@+id/btnPlay"
app:layout_constraintTop_toTopOf="@+id/btnPlay" />
<Switch
android:id="@+id/swLoop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/loopOn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4.2 文字列ファイル
<resources>
<string name="app_name">TestMediaPlayer</string>
<string name="play">再生</string>
<string name="pause">一時停止</string>
<string name="STOP">停止</string>
<string name="loopOn">LoopOn</string>
</resources>
4.3 オプションメニューファイル
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/openSoundFolder"
app:showAsAction="never"
android:title="開く"/>
</menu>
4.4 プログラムコード
package com.example.testmediaplayer
import android.app.Activity
import android.content.Intent
import android.media.MediaPlayer
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Button
import android.widget.CompoundButton
import android.widget.Switch
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity
import java.io.IOException
import java.util.*
class MainActivity : AppCompatActivity() {
val mediaPlayer = MediaPlayer()
lateinit var btnPlay: Button
lateinit var btnStop: Button
lateinit var swLoopOn: Switch
val getContentSound =
registerForActivityResult(StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val resultIntent = it.data
val uri: Uri? = resultIntent?.data
mediaPlayer.apply {
stop()
reset()
}
if (uri != null) {
mediaPlayer.apply {
setDataSource(this@MainActivity, uri) // 音源を設定
//メディアソースの再生準備が整ったときに呼び出されるコールバックの登録する
setOnPreparedListener {
// 各ボタンをタップ可能に設定
btnPlay.setEnabled(true)
btnPlay.text = getString(R.string.play)
btnStop.setEnabled(true)
}
// 再生中にメディアソースの終端に到達したときに呼び出されるコールバックを登録
setOnCompletionListener {
// ループ設定がされていなければ
if (!isLooping()) {
// 再生ボタンのラベルを「再生」に設定
btnPlay.text = getString(R.string.play)
}
}
prepareAsync()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnPlay = findViewById<Button>(R.id.btnPlay)
btnStop = findViewById<Button>(R.id.btnStop)
swLoopOn = findViewById<Switch>(R.id.swLoop)
btnPlay.setOnClickListener {
if (!mediaPlayer.isPlaying) {
try {
mediaPlayer.start()
btnPlay.text = getString(R.string.pause)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
} else {
try {
// 再生を一時停止
mediaPlayer.pause()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
btnPlay.text = getString(R.string.play)
}
}
btnStop.setOnClickListener {
try {
mediaPlayer.stop()
mediaPlayer.prepare()
} catch (e: IllegalStateException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
btnPlay.text = getString(R.string.play)
}
// ループスイッチの状態が変化したときに、MediaPlayerのインスタンスに反映
swLoopOn.setOnCheckedChangeListener(
object : CompoundButton.OnCheckedChangeListener {
override fun onCheckedChanged(
buttonView: CompoundButton?,
isChecked: Boolean,
) {
mediaPlayer.isLooping = isChecked
}
}
)
}
// アプリを一時的に隠した時の処理
override fun onPause() {
super.onPause()
try {
mediaPlayer.pause()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// オプションメニュー用xmlファイルをインフレートする
menuInflater.inflate(R.menu.option_menu, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// 選択されたメニューのIDのR値による処理分岐
when (item.itemId) {
R.id.openSoundFolder -> {
// ファイルピッカーを呼び出す
val iSound = Intent(Intent.ACTION_OPEN_DOCUMENT)
iSound.type = "audio/mpeg"
iSound.putExtra(Intent.EXTRA_TITLE, "memo.mp3")
getContentSound.launch(iSound)
}
}
return super.onOptionsItemSelected(item)
}
}
以上、