【Android】メトロノームアプリの作成【Kotlin】
北本です。
最近、Androidプログラミングの勉強を始めました。
Android StudioでKotlinを使ってのプログラミングに挑戦しており、『はじめてのAndroidプログラミング 第5版』(金田浩明著, SBクリエイティブ)を参考書にしています。
練習がてら非常に簡単なアプリを作成してみたので紹介します。最低限の機能のみを備えたメトロノームです。
仕様
画面は上掲画像のようなものとします。
入力欄にBPM値(一分間の拍数)をキー入力し、「START」ボタンをタップすると、再生状態になり指定したテンポでの拍に合わせて音が鳴ります。再生状態になるとボタンのテキストが「START」から「STOP」に変化し、それをタップすると再生状態から抜けます。
設定可能なBPM値は、1~300の整数とし、異常値が設定された状態で「START」ボタンをタップした場合は、エラーメッセージを表示し再生状態に遷移しないものとします。
再生状態中は、入力欄が使用不可となりBPM値の変更ができないものとします。
また、メトロノームは再生しながら楽器を弾くような使い方が想定されるため、再生状態中は操作せずに放置してもスリープ状態にならないようにします。
下準備
Android Studioで、Empty Activityを選択し、名前を「metronome」、言語を「Kotlin」としてプロジェクトを作成します。
非常に短い音声ファイルbeat.oggを用意し、app/resにrawフォルダを作成し、その中に配置します。
ビューバインディングを有効にするため、build.gradle (Module: metronome.app)のandroid{}内に以下のように追記し、Sync Nowで変更を適用します。
1 2 3 4 5 6 | android { // 中略 // buildFeatures{ viewBinding = true } } |
レイアウト
「BPM」のテキスト表示をTextViewとして、BPM値の入力欄をNumber (Signed)として、「START」ボタンをButtonとして配置します。idは以下のように設定することとします。
ビュー | id |
TextView | textViewBPM |
Number (Signed) | editTextBPM |
Button | buttonStart |
レイアウトの詳細については説明を割愛しますが、以下のようなxmlファイルになっています。
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <?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"> <TextView android:id="@+id/textViewBPM" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="BPM" android:textSize="34sp" app:layout_constraintBottom_toTopOf="@+id/buttonStart" app:layout_constraintEnd_toStartOf="@+id/editTextBPM" app:layout_constraintHorizontal_bias="0.521" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/editTextBPM" android:layout_width="129dp" android:layout_height="83dp" android:ems="10" android:inputType="numberSigned" android:maxLength="3" android:text="60" android:textSize="60sp" app:layout_constraintBottom_toBottomOf="@+id/textViewBPM" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/textViewBPM" app:layout_constraintTop_toTopOf="@+id/textViewBPM" /> <Button android:id="@+id/buttonStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="START" android:textSize="34sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/editTextBPM" /> </androidx.constraintlayout.widget.ConstraintLayout> |
実装
肝要となるのは、
・Handlerを使ったタイマー処理
・SoundPoolを使った音声再生
・FLAG_KEEP_SCREEN_ONのON/OFFによるアイドル状態での自動スリープの無効/有効化
です。
MainActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | package com.example.metronome import android.media.AudioAttributes import android.media.SoundPool import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.WindowManager import androidx.appcompat.app.AlertDialog import com.example.metronome.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var soundPool: SoundPool private val handler = Handler(Looper.getMainLooper()) private var isRunning: Boolean = false // 再生状態であるか private var interval: Long = 0 // 拍間のミリ秒数 private var soundResId = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.buttonStart.setOnClickListener{ onStartButtonTapped() } } override fun onResume() { super.onResume() // SoundPoolのインスタンスを生成しリソースを読み込み soundPool = SoundPool.Builder().run{ val audioAttributes = AudioAttributes.Builder().run{ setUsage(AudioAttributes.USAGE_ALARM) build() } setMaxStreams(1) setAudioAttributes(audioAttributes) build() } soundResId = soundPool.load(this, R.raw.beat,1) } override fun onPause(){ // アクティビティ非表示時はSoundPoolのリソースを解放 super.onPause() soundPool.release() } private fun onStartButtonTapped(){ if(isRunning){ stop(); } else{ start(); } } // 「START」ボタン押下時の処理 private fun start(){ var bpm: Long?; bpm = binding.editTextBPM.text.toString().toLongOrNull() if(bpm == null){ val dialog = AlertDialog.Builder(this) dialog.setMessage("BPM値の入力が正しくありません。") dialog.setPositiveButton("OK", null) dialog.show() return } bpm?.let{ if(it <= 0 || 300 < it){ val dialog = AlertDialog.Builder(this) dialog.setMessage("BPM値は1~300の範囲で入力してください。") dialog.setPositiveButton("OK", null) dialog.show() return } } interval = 60000 / bpm // 1分( = 60000ms) / BPMで拍間のミリ秒数を求める isRunning = true getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 再生中はスリープ状態にならないようにFLAG_KEEP_SCREEN_ONをONにする handler.post(runnable) // Handlerにコールバック関数を渡す binding.buttonStart.setText("STOP") binding.editTextBPM.isEnabled = false } // 「STOP」ボタン押下時の処理 private fun stop(){ isRunning = false getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 非再生時はスリープ状態になってもよいのでFLAG_KEEP_SCREEN_ONをOFFにする handler.removeCallbacks(runnable) // Handlerからコールバック関数を削除 binding.buttonStart.setText("START") binding.editTextBPM.isEnabled = true } // 音声を再生するコールバック関数 private val runnable = object: Runnable { override fun run(){ soundPool.play(soundResId, 1.0f, 1.0f, 100, 0, 1.0f) // SoundPoolでの音声再生 handler.postDelayed(this, interval) // intervalに設定したミリ秒後にコールバックを呼び出す } } } |
以下は、アプリをエミュレータで動作させた様子の動画です。
音声をONにしてご覧ください。