作成: 2020年09月22日
更新: 2021年02月01日
アンドロイドで好きな音楽のサビを流せるアラームアプリを作るで音楽の再生開始時間を設定する際にスライダーのほかに以下のようにダイアログ形式で設定したくなりました.
日付の設定には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.ktclass 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, "タグ")
}