8
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】MediaPlayerで音楽プレイヤ―を作ってみた。(Kotlin版)

Last updated at Posted at 2021-07-09

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を表示してみた。」を参照してください。

TestMediaPlayer03_002_01.png  Picker.png

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未満は未対応としました。

build.gradle#Project
// 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
}
bulid.gradle#app
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 レイアウトファイル

Activity_main.xml
<?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 文字列ファイル

Strings.xml
<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 オプションメニューファイル

option_menu.xml
<?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 プログラムコード

MainActivity.kt
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)
    }
}

以上、

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?