Androidアプリでミリ秒単位で時間を入力するダイアログ

作成: 2020年09月22日

更新: 2020年09月22日

カテゴリー:

概要

アンドロイドで好きな音楽のサビを流せるアラームアプリを作るで音楽の再生開始時間を設定する際にスライダーのほかに以下のようにダイアログ形式で設定したくなりました.

dialog.png

日付の設定にはDatePickerDialogが自国の設定にはTimePickerDialogがあるが今回のようにミリ秒単位で設定したい場合はNumberPickerを使用してカスタムダイアログを作成する必要がある.

作成方法

まずダイアログとして表示するxmlファイル,number_picker_dialog.xmlをres/layoutディレクトリに作成する.

number_picker_dialog.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:id="@+id/numberPickerDialogLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <NumberPicker android:id="@+id/minutesPicker" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@+id/colon_between_minutes_and_seconds" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/colon_between_minutes_and_seconds" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical" android:text=":" android:textSize="36sp" app:layout_constraintStart_toEndOf="@+id/minutesPicker" app:layout_constraintEnd_toStartOf="@+id/secondsPicker" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@+id/minutesPicker"/> <NumberPicker android:id="@+id/secondsPicker" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toEndOf="@+id/colon_between_minutes_and_seconds" app:layout_constraintEnd_toStartOf="@+id/dot_between_seconds_and_millis" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/dot_between_seconds_and_millis" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical" android:text="." android:textSize="36sp" app:layout_constraintStart_toEndOf="@+id/secondsPicker" app:layout_constraintEnd_toStartOf="@+id/millisPicker" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@+id/secondsPicker"/> <NumberPicker android:id="@+id/millisPicker" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toEndOf="@+id/dot_between_seconds_and_millis" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

ContraintLayoutでNumberPickerとTextView(コロン:とドット.)を交互に置いてる.

次にnumber_picker_dialog.xmlをダイアログとして表示するためDialogFragmentを継承したNumberPickerDialogクラスを作成する.

NumberPickerDialog.kt
class NumberPickerDialog : DialogFragment() { private var initMinutes: Int? = null private var initSeconds: Int? = null private var initMillis: Int? = null lateinit var alarm: Alarm lateinit var viewContext: Context lateinit var listAdapter: AlarmRecyclerAdapter private var durationMilli: Int? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return activity?.let { val builder = AlertDialog.Builder(it) val inflater = requireActivity().layoutInflater val numberPickerDialog = inflater.inflate(R.layout.number_picker_dialog, null) val minutesPicker = numberPickerDialog.findViewById<NumberPicker>(R.id.minutesPicker) val secondsPicker = numberPickerDialog.findViewById<NumberPicker>(R.id.secondsPicker) val millisPicker = numberPickerDialog.findViewById<NumberPicker>(R.id.millisPicker) val dialogLimits = calcDialogLimits(initMinutes ?: 0, initSeconds ?: 0) minutesPicker.minValue = 0 minutesPicker.maxValue = dialogLimits["maxMinutes"] ?: 99 secondsPicker.minValue = 0 secondsPicker.maxValue = dialogLimits["maxSeconds"] ?: 59 millisPicker.minValue = 0 millisPicker.maxValue = dialogLimits["maxMillis"] ?: 999 minutesPicker.value = initMinutes ?: 0 secondsPicker.value = initSeconds ?: 0 millisPicker.value = initMillis ?: 0 minutesPicker.setOnValueChangedListener { picker, oldValue, newValue -> val soundStartTimeMillis = newValue * 60 * 1000 + secondsPicker.value * 1000 + millisPicker.value val soundStartTimeText = String.format( "%02d:%02d.%03d", newValue, secondsPicker.value, millisPicker.value ) AlarmActionsCreator.changeSoundStartTime( this.alarm.id, soundStartTimeMillis, soundStartTimeText, this.viewContext ) secondsPicker.maxValue = calcDialogLimits(newValue, secondsPicker.value)["maxSeconds"] ?: 59 this.listAdapter.notifyDataSetChanged() } secondsPicker.setOnValueChangedListener { picker, oldValue, newValue -> val soundStartTimeMillis = minutesPicker.value * 60 * 1000 + newValue * 1000 + millisPicker.value val soundStartTimeText = String.format( "%02d:%02d.%03d", minutesPicker.value, newValue, millisPicker.value ) AlarmActionsCreator.changeSoundStartTime( this.alarm.id, soundStartTimeMillis, soundStartTimeText, this.viewContext ) millisPicker.maxValue = calcDialogLimits(minutesPicker.value, newValue)["maxMillis"] ?: 999 this.listAdapter.notifyDataSetChanged() } millisPicker.setOnValueChangedListener { picker, oldValue, newValue -> val soundStartTimeMillis = minutesPicker.value * 60 * 1000 + secondsPicker.value * 1000 + newValue val soundStartTimeText = String.format( "%02d:%02d.%03d", minutesPicker.value, secondsPicker.value, newValue ) AlarmActionsCreator.changeSoundStartTime( this.alarm.id, soundStartTimeMillis, soundStartTimeText, this.viewContext ) this.listAdapter.notifyDataSetChanged() } builder.setView(numberPickerDialog) builder.create() } ?: throw IllegalStateException("Activity cannot be null") } private fun calcDialogLimits(minutes: Int, seconds: Int): Map<String, Int> { val maxMinutes = this.durationMilli?.div(60 * 1000) ?: 99 val maxSeconds = if (minutes == maxMinutes) { this.durationMilli?.div(1000)?.rem(60) ?: 59 } else { 59 } val maxMillis = if (minutes == maxMinutes && seconds == maxSeconds) { this.durationMilli?.rem(1000) ?: 999 } else { 999 } return mapOf( "maxMinutes" to maxMinutes, "maxSeconds" to maxSeconds, "maxMillis" to maxMillis ) } fun setDialogInit( alarm: Alarm, initMinutes: Int, initSeconds: Int, initMillis: Int, viewContext: Context, listAdapter: AlarmRecyclerAdapter ) { this.alarm = alarm this.initMinutes = initMinutes this.initSeconds = initSeconds this.initMillis = initMillis val retriever = MediaMetadataRetriever() if (alarm.isDefaultSound) { val assetFileDescriptor = viewContext.assets.openFd("default/${alarm.soundFileName}") retriever.setDataSource( assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.length ) } else { retriever.setDataSource( viewContext, Uri.parse(alarm.soundFileUri) ) } val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) retriever.release() val durationMilli = Integer.parseInt(duration) this.durationMilli = durationMilli this.viewContext = viewContext this.listAdapter = listAdapter } }

ダイアログを表示する際にonCreateDialogが呼ばれるのでそこにそれぞれのNumberPickerの値が変わったときの挙動をNumberPicker.setOnValueChangedListenerで記述している.今回はアラームアプリだったので音楽ファイルの読み込みなどを書いているので使用する場合は適宜変更が必要.

setOnValueChangedListenerで必要な要素,今回の場合はアラームの情報alarmやcontextなどはダイアログを表示する前にsetDialogInitを呼び出してこのクラスのフィールドに保存している.

ダイアログを表示するときはActivityのボタンなどから以下のように呼び出す.

button.setOnClickListener {
    val dialog = NumberPickerDialog()
    dialog.setDialogInit(/**NumberPickerの処理に必要な要素**/)
    dialog.show(supportFragmentManager, "タグ")
}