【Kotlin/Android Studio】DatePicker/TimePickerDialogの実装方法!

この記事からわかること
- Android Studio/KotlinでDatePicker/TimePickerDialogの実装方法
- Widgetsでビューに配置する
- スピナーモードの変更方法
- スピナーモードのカレンダーをを非表示にするには?
- 選択された日時データをMainActivityに反映させるには?
- setOnDateChangedListenerとsetOnTimeChangedListenerの使い方
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
DatePickerDialogの実装方法
kotlinでButtonをタップした際に日付を入力できるDatePickerDialogを実装していきます。まずはDatePickerDialogFragment
クラスを用意します。ダイアログとして表示させたいのでDialogFragment
を継承して実装していきます。
import android.app.DatePickerDialog
import android.app.Dialog
import android.os.Bundle
import android.widget.DatePicker
import androidx.fragment.app.DialogFragment
import java.util.Calendar
// DialogFragment:onCreateDialogメソッド
// DatePickerDialog.OnDateSetListener:onDateSetメソッドの提供
class DatePickerFragment : DialogFragment(), DatePickerDialog.OnDateSetListener {
// ダイアログの生成
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// 現在の日付情報を取得
val c = Calendar.getInstance()
val year = c.get(Calendar.YEAR)
val month = c.get(Calendar.MONTH)
val day = c.get(Calendar.DAY_OF_MONTH)
// 取得した日付情報を元にダイアログを生成
return DatePickerDialog(requireContext(), this, year, month, day)
}
// 日付が選択されたときに呼び出されるコールバックメソッド
override fun onDateSet(view: DatePicker, year: Int, month: Int, day: Int) {
}
}
DatePickerDialog.OnDateSetListener
DatePickerDialog.OnDateSetListener
を継承することでDatePickerから日付を選択された時に呼び出されるコールバックメソッド(onDateSet)をオーバーライドできます。
onCreateDialogメソッド内
ダイアログを生成するonCreateDialog
メソッド内ではCalendar
クラスを使用して現在の日時情報を取得しています。
おすすめ記事:【Kotlin/Android Studio】現在の日付を取得する方法!DateTimeFormatterの使い方
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val c = Calendar.getInstance()
val year = c.get(Calendar.YEAR)
val month = c.get(Calendar.MONTH)
val day = c.get(Calendar.DAY_OF_MONTH)
return DatePickerDialog(requireContext(), this, year, month, day)
}
これでDatePickerDialogの準備ができたのであとはButtonのクリックイベントに合わせて表示させるだけです。
val button:Button = findViewById(R.id.button)
button.setOnClickListener {
val dialog = DatePickerFragment()
dialog.show(supportFragmentManager, "date")
}

TimePickerDialogの実装方法
同じ要領でTimePickerDialog
も実装していきます。DatePickerDialog
とほぼ同じなので説明は基本的には割愛します。
import android.app.Dialog
import android.app.TimePickerDialog
import android.os.Bundle
import android.widget.TimePicker
import androidx.fragment.app.DialogFragment
import java.util.Calendar
// DialogFragment:onCreateDialogメソッド
// TimePickerDialog.OnTimeSetListener:onTimeSetメソッドの提供
class TimePickerFragment : DialogFragment(), TimePickerDialog.OnTimeSetListener {
// ダイアログの生成
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// 現在の日付情報を取得
val c = Calendar.getInstance()
val hour = c.get(Calendar.HOUR_OF_DAY)
val minute = c.get(Calendar.MINUTE)
// 取得した日付情報を元にダイアログを生成
return TimePickerDialog(activity, this, hour, minute,false)
}
// 時間が選択されたときに呼び出されるコールバックメソッド
override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) {
}
}
TimePickerDialog(activity, this, hour, minute,false)
ここでTimePickerDialogを生成していますが、最後の引数はis24HourView
というパラメータで時間を24時間表示にするか12時間表示にするかのフラグです。
これでTimePickerDialogの準備ができたのであとはButtonのクリックイベントに合わせて表示させるだけです。
val button:Button = findViewById(R.id.button)
button.setOnClickListener {
val dialog = TimePickerFragment()
dialog.show(supportFragmentManager, "time")
}

選択された日時データをMainActivityのビューに反映させる
では実際に選択された日時情報をMainActivity側のビューに反映させてみたいと思います。まずはMainActivity側に「日時情報を受け取りTextViewに反映させるメソッド」を追加します。
// DatePickerFragmentから選択された日付を反映させる
fun updateDate(year: Int, month: Int, day: Int) {
val dateTextView:TextView = findViewById(R.id.date_text)
val formattedDate = String.format("%04d-%02d-%02d", year, month + 1, day)
dateTextView.text = formattedDate
}
続いてDatePickerFragmentクラスの日付が選択された時に呼ばれるonDateSet
メソッド内からMainActivity.ktのupdateDate
メソッドを呼び出します。(依存性とかは無視してます)
override fun onDateSet(view: DatePicker, year: Int, month: Int, day: Int) {
val activity = activity as MainActivity?
activity?.updateDate(year, month, day)
}
これで選択した際に日付情報が反映されるようになりました。

interfaceで依存性を軽減する
おすすめ記事:【Kotlin/Android Studio】FragmentのイベントをActivityで受け取る方法
先ほどの例だとDatePickerFragment
クラスがMainActivity
クラスの参照を持ってしまうのでinterface
を利用して依存性を解消していきたいと思います。まずはDatePickerFragment
クラス内にListener
インターフェースを実装しMainActivityに渡したいデータを引数で受け取るメソッドを用意します。さらにonAttach
メソッドをオーバーライドしてその中からcontext
をListenerへキャストします。
class DatePickerFragment : DialogFragment(), DatePickerDialog.OnDateSetListener {
interface Listener {
// MainActivityに渡したいデータを引数で受け取るメソッド
fun onDataset(year: Int, month: Int, day: Int)
}
// DatePickerFragmentクラス内参照用
private lateinit var listener: Listener
override fun onAttach(context: Context) {
super.onAttach(context)
// listenerプロパティにキャストしてセット
listener = context as? Listener
?: throw ClassCastException("The parent activity needs to implement OnDismissListener")
}
// 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
override fun onDateSet(view: DatePicker, year: Int, month: Int, day: Int) {
// val activity = activity as MainActivity?
// activity?.updateDate(year, month, day)
listener.onDataset(year,month,day)
}
}
オーバーライドしているonDateSet
メソッド内は上記のように修正します。MainActivity
クラス側ではDatePickerFragment.Listener
を継承させ、先ほどのupdateDate
メソッドを削除しonDataset
メソッドに入れ替えます。
class MainActivity : AppCompatActivity() ,DatePickerFragment.Listener {
// 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
override fun onDataset(year: Int, month: Int, day: Int) {
val dateTextView:TextView = findViewById(R.id.date_text)
val formattedDate = String.format("%04d-%02d-%02d", year, month + 1, day)
dateTextView.text = formattedDate
}
}
これでMainActivityの参照を持つことなくデータを受け渡すことができました。
Widgetsとして実装する
わざわざダイアログとして実装しなくてもWidgetsとしてビューに配置することも可能です。これによりPickerを自由な位置に配置したり、他のWidgetsと組み合わせたカスタムフラグメントとして実装することができるようになります。
DatePicker
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

TimePicker
<TimePicker
android:id="@+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

TimePicker
を24時間表示にしたい場合はレイアウトファイルからは指定できずコード側でsetIs24HourView
メソッドにtrue
を渡す必要があります。
timePicker.setIs24HourView(true)
スピナーモード
日付や時間を選択するモードとしてスピナーモードがあります。これを実装するにはdatePickerMode
ortimePickerMode
属性にspinner
を渡すだけです。
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:datePickerMode="spinner" />
<TimePicker
android:id="@+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:timePickerMode="spinner" />

スピナーモードのカレンダーを非表示にする
しかしDatePicker
ではスピナーモードにしてもカレンダーが表示されたままになってしまいます。これを非表示にするにはandroid:calendarViewShown="false"
属性を付与します。
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:datePickerMode="spinner"
android:calendarViewShown="false"/>

またその他にも背景色の変更や日付選択の範囲など色々カスタマイズできるので詳細は公式サイトを参考にしてください。
DatePickerDialogでスピナーモードにする
DatePickerDialog
側でスピナーモードにしたい場合はDatePickerDialog
の第二引数にandroid.R.style.Theme_Holo_Dialog
を渡すことで実装できました。
val dialog = DatePickerDialog(requireContext(),
android.R.style.Theme_Holo_Dialog,
this,
year,
month,
day)
参考文献:Teratail:Kotlin Android 日付や時刻選択をスピナー型で
Widgetsで定義したPickerの変更通知を受け取る
ではPickerを保持するカスタムフラグメントを実装する流れを見ていきます。
- カスタムフラグメント用レイアウトの作成
- カスタムフラグメント用クラスの作成
- イベントリスナーを設定
- アクティビティ側から変更通知とデータを受け取り反映
細かいフラグメントの実装方法については以下の記事を参考にしてください。
1.カスタムフラグメント用レイアウトの作成
まずは実際に表示させるFragmentレイアウトを作成していきます。ここでは特別なことはしてませんので割愛します。
<?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/frameLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:clickable="true"
tools:context=".CustomDatePickerFragment">
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:datePickerMode="spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TimePicker
android:id="@+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:timePickerMode="spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/datePicker" />
<Button
android:id="@+id/done_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="決定"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timePicker" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.カスタムフラグメント用クラスの作成
続いて今回の肝となるFragmentを管理するクラスを仕上げていきます。Listener
を利用したMainActivityへのデータの受け渡しはこちらと一緒です。
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.DatePicker
import android.widget.TimePicker
class CustomDatePickerFragment : Fragment() {
interface Listener {
fun onDataset(year: Int, month: Int, day: Int)
fun onTimeset(hour: Int, minutes: Int)
}
private lateinit var listener: CustomDatePickerFragment.Listener
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? CustomDatePickerFragment.Listener
?: throw ClassCastException("The parent activity needs to implement OnDismissListener")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_custom_date_picker, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val doneButton: Button = view.findViewById(R.id.done_button)
doneButton.setOnClickListener {
parentFragmentManager.apply {
popBackStack()
popBackStack()
}
}
val datePicker: DatePicker = view.findViewById(R.id.datePicker)
// DatePickerの値が変化したときに呼び出されるリスナーを設定
datePicker.setOnDateChangedListener { datePicker, year, month, day ->
listener.onDataset(year,month,day)
}
val timePicker: TimePicker = view.findViewById(R.id.timePicker)
// TimePickerの値が変化したときに呼び出されるリスナーを設定
timePicker.setOnTimeChangedListener { timePicker, hour, minutes ->
listener.onTimeset(hour,minutes)
}
}
}
3.イベントリスナーを設定
ここで重要なのは以下の部分です。それぞれのWidgetsから呼び出しているsetOnXXXXChangedListener
がデータの変更を検知して呼び出されるコールバックメソッドです。この中に変更時に処理させたいことを実装します。
datePicker.setOnDateChangedListener { datePicker, year, month, day ->
listener.onDataset(year,month,day)
}
timePicker.setOnTimeChangedListener { timePicker, hour, minutes ->
listener.onTimeset(hour,minutes)
}
4.アクティビティ側から変更通知とデータを受け取り反映
ここも特に変わったことはしてません。
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
class MainActivity : AppCompatActivity() ,CustomDatePickerFragment.Listener{
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button:Button = findViewById(R.id.button)
button.setOnClickListener {
supportFragmentManager.beginTransaction().apply {
add(R.id.main_frame, CustomDatePickerFragment())
addToBackStack(null)
commit()
}
}
}
override fun onDataset(year: Int, month: Int, day: Int) {
val dateTextView:TextView = findViewById(R.id.date_text)
val formattedDate = String.format("%04d-%02d-%02d", year, month + 1, day)
dateTextView.text = formattedDate
}
override fun onTimeset(hour: Int, minutes:Int) {
val timeTextView:TextView = findViewById(R.id.time_text)
val formattedTime = String.format("%02d:%02d", hour, minutes)
timeTextView.text = formattedTime
}
}
これで独自で実装したPickerの値の変化を検知してアクティビティに反映させることができるようになりました。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。