Android(Kotlin)日付・時間選択ダイアログ

2020/9/3

はじめに

ユーザーに日付、時間を選択させたい場合、DatePickerDialogTimePickerDialog を利用するのが良いと思います。ただ、ドキュメントにもそれぞれの使い方が記載されてはいるんですが、実際に使えるレベルまで実装するとプラスアルファで色々あったので、備忘録として残しておきます。

実装内容

概要

  • DatePickerDialog を内部的に利用する DatePickerDialogFragment.kt
  • TimePickerDialog を内部的に利用する TimePickerDialogFragment.kt
  • それぞれを利用する処理のみ実装で FormFragment.kt

詳細

DatePickerDialogFragment.kt

常に現在日時が初期値となっているダイアログは使えないので、初期値を与えることができるようにインスタンス化メソッドを追加しています。

また、フラグメントから呼び出されることを想定した作りになっていますが、アクティビティから呼び出す場合は onAttach() を修正する必要があります。

ついでに、DatePickerDialogjava.util.Calendar の月数(実数 - 1)を前提に実装されていますが、使う側にそんな特有なルールを強制するのはイマイチなので(LocalDate 使いたいし)対応しています。

package com.example.myapplication.ui.datetime

import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.widget.DatePicker
import androidx.fragment.app.DialogFragment
import java.util.*

class DatePickerDialogFragment private constructor() : DialogFragment(), DatePickerDialog.OnDateSetListener {
    interface DatePickerDialogListener {
        fun onDateSet(year: Int, month: Int, dayOfMonth: Int)
    }

    companion object {
        private const val ARG_YEAR = "ARG_YEAR"
        private const val ARG_MONTH = "ARG_MONTH"
        private const val ARG_DAY_OF_MONTH = "ARG_DAY_OF_MONTH"

        @JvmStatic
        fun newInstance(year: Int, month: Int, dayOfMonth: Int): DatePickerDialogFragment {
            return DatePickerDialogFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_YEAR, year)
                    putInt(ARG_MONTH, month)
                    putInt(ARG_DAY_OF_MONTH, dayOfMonth)
                }
            }
        }
        @JvmStatic
        fun newInstance(): DatePickerDialogFragment {
            return DatePickerDialogFragment()
        }
    }

    private lateinit var listener: DatePickerDialogListener

    override fun onAttach(context: Context) {
        super.onAttach(context)
        when (parentFragment) {
            is DatePickerDialogListener -> listener = parentFragment as DatePickerDialogListener
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val calendar = Calendar.getInstance()
        var year = calendar.get(Calendar.YEAR)
        var month = calendar.get(Calendar.MONTH)
        var dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
        if (arguments != null) {
            year = requireArguments().getInt(ARG_YEAR)
            month = requireArguments().getInt(ARG_MONTH) - 1
            dayOfMonth = requireArguments().getInt(ARG_DAY_OF_MONTH)
        }
        return DatePickerDialog(requireContext(), this, year, month, dayOfMonth)
    }

    override fun onDateSet(view: DatePicker, year: Int, month: Int, dayOfMonth: Int) {
        listener.onDateSet(year, month + 1, dayOfMonth)
    }
}

TimePickerDialogFragment.kt

こちらもインスタンス化メソッド追加、フラグメントから呼び出し前提なのは、前述の DatePickerDialogFragment.kt と同様です。

package com.example.myapplication.ui.datetime

import android.app.Dialog
import android.app.TimePickerDialog
import android.content.Context
import android.os.Bundle
import android.text.format.DateFormat
import android.widget.TimePicker
import androidx.fragment.app.DialogFragment
import java.util.*

class TimePickerDialogFragment private constructor() : DialogFragment(), TimePickerDialog.OnTimeSetListener {
    interface TimePickerDialogListener {
        fun onTimeSet(hourOfDay: Int, minute: Int)
    }

    companion object {
        private const val ARG_HOUR_OF_DAY = "ARG_HOUR_OF_DAY"
        private const val ARG_MINUTE = "ARG_MINUTE"

        @JvmStatic
        fun newInstance(hourOfDay: Int, minute: Int): TimePickerDialogFragment {
            return TimePickerDialogFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_HOUR_OF_DAY, hourOfDay)
                    putInt(ARG_MINUTE, minute)
                }
            }
        }
        @JvmStatic
        fun newInstance(): TimePickerDialogFragment {
            return TimePickerDialogFragment()
        }
    }

    private lateinit var listener: TimePickerDialogListener

    override fun onAttach(context: Context) {
        super.onAttach(context)
        when (parentFragment) {
            is TimePickerDialogListener -> listener = parentFragment as TimePickerDialogListener
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val calendar = Calendar.getInstance()
        var hour = calendar.get(Calendar.HOUR_OF_DAY)
        var minute = calendar.get(Calendar.MINUTE)
        if (arguments != null) {
            hour = requireArguments().getInt(ARG_HOUR_OF_DAY)
            minute = requireArguments().getInt(ARG_MINUTE)
        }
        return TimePickerDialog(requireContext(), this, hour, minute, DateFormat.is24HourFormat(activity))
    }

    override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) {
        listener.onTimeSet(hourOfDay, minute)
    }
}

FormFragment.kt

今回の話と無関係な処理は省いて記載しています。

また、詳細は割愛しますが、LocalDateLocalTime はデフォルトで使えないことには注意です。

class FormFragment : Fragment(),
    DatePickerDialogFragment.DatePickerDialogListener, TimePickerDialogFragment.TimePickerDialogListener {

    private lateinit var dateEditText: EditText
    private lateinit var timeEditText: EditText

    private var date: LocalDate? = null
    private var time: LocalTime? = null

    fun showDatePickerDialog() {
        var fragment: DatePickerDialogFragment? = null
        if (date != null) {
            fragment = DatePickerDialogFragment.newInstance(date!!.year, date!!.monthValue, date!!.dayOfMonth)
        }
        if (fragment == null) {
            fragment = DatePickerDialogFragment.newInstance()
        }
        fragment.show(childFragmentManager, "DatePickerDialogFragment")
    }

    fun showTimePickerDialog() {
        var fragment: TimePickerDialogFragment? = null
        if (time != null) {
            fragment = TimePickerDialogFragment.newInstance(time!!.hour, time!!.minute)
        }
        if (fragment == null) {
            fragment = TimePickerDialogFragment.newInstance()
        }
        fragment.show(childFragmentManager, "TimePickerDialogFragment")
    }

    override fun onDateSet(year: Int, month: Int, dayOfMonth: Int) {
        date = LocalDate.of(year, month, dayOfMonth)
        dateEditText.setText("${"%04d".format(year)}/${"%02d".format(month)}/${"%02d".format(dayOfMonth)}")
    }

    override fun onTimeSet(hourOfDay: Int, minute: Int) {
        time = LocalTime.of(hourOfDay, minute)
        timeEditText.setText("${"%02d".format(hourOfDay)}:${"%02d".format(minute)}")
    }
}