AndroidでSpinner形式の DatePickerDialog
を表示する方法を解説します。
本記事のサンプルコードについては こちら に置いてあります。
コード
テーマ
テーマを下記のように設定します。
android:datePickerStyle
に <item name="android:datePickerMode">spinner</item>
を子要素に持つスタイルを設定します。
themes.xml
<resources>
<!-- 省略 -->
<!-- DatePickerDialog -->
<!--
parentはアプリ全体のテーマに合わせて、
"Theme.MaterialComponents.Light.Dialog" や "Theme.AppCompat.Light.Dialog"
にする。
-->
<style name="DatePickerDialog" parent="Theme.MaterialComponents.Light.Dialog" />
<style name="DatePickerDialog.Spinner">
<item name="android:datePickerStyle">@style/DatePickerDialogSpinnerDatePickerStyle</item>
</style>
<style name="DatePickerDialogSpinnerDatePickerStyle">
<item name="android:datePickerMode">spinner</item>
</style>
</resources>
DatePickerDialogの表示
DatePickerDialogの第2引数にさきほどのテーマを指定します。
val datePickerDialog = DatePickerDialog(
context,
R.style.DatePickerDialog_Spinner, // 上記のテーマを指定する
{ view, year, month, dayOfMonth ->
// DatePickerDialogでOKを押した際の処理
},
2020, // 初期表示の年 2020年
10, // 初期表示の月 11月
26 // 初期表示の日 26日
)
datePickerDialog.show()
Android 7.0のバグの回避
Android 7.0にはバグがあり、上記のようにSpinner表示をしてもCalendar表示のDatePickerDialogが表示されてしまいます。
これを回避するために、Android標準の android.app.DatePickerDialog
の代わりに、下記のDatePickerDialogを使用してください。
package your.package.name // 適当なパッケージ名に書き換えてください
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.widget.DatePicker
import java.lang.reflect.Field
/**
* Android 7.0でspinner表示ができるDatePickerDialog
*/
class DatePickerDialog : android.app.DatePickerDialog {
constructor(
context: Context,
callback: OnDateSetListener?,
year: Int,
monthOfYear: Int,
dayOfMonth: Int
) : super(context, callback, year, monthOfYear, dayOfMonth) {
fixSpinner(context, year, monthOfYear, dayOfMonth)
}
constructor(
context: Context,
theme: Int,
callback: OnDateSetListener?,
year: Int,
monthOfYear: Int,
dayOfMonth: Int
) : super(context, theme, callback, year, monthOfYear, dayOfMonth) {
fixSpinner(context, year, monthOfYear, dayOfMonth)
}
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
private fun fixSpinner(context: Context, year: Int, month: Int, dayOfMonth: Int) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N) {
try {
// android:datePickerModeを取得
val modeSpinner = 2
val styleableClass = Class.forName("com.android.internal.R\$styleable")
val datePickerStyleableField = styleableClass.getField("DatePicker")
val datePickerStyleable = datePickerStyleableField[null] as IntArray
val a = context.obtainStyledAttributes(
null, datePickerStyleable, android.R.attr.datePickerStyle, 0
)
val datePickerModeStyleableField =
styleableClass.getField("DatePicker_datePickerMode")
val datePickerModeStyleable = datePickerModeStyleableField.getInt(null)
val mode = a.getInt(datePickerModeStyleable, modeSpinner)
a.recycle()
if (mode == modeSpinner) {
val datePicker = findField(
android.app.DatePickerDialog::class.java,
DatePicker::class.java,
"mDatePicker"
)!!.get(this) as DatePicker
val delegateClass = Class.forName("android.widget.DatePickerSpinnerDelegate")
val delegateField = findField(
DatePicker::class.java, delegateClass, "mDelegate"
)!!
var delegate = delegateField[datePicker]
val spinnerDelegateClass =
Class.forName("android.widget.DatePickerSpinnerDelegate")
// Android 7.0ではdatePickerModeがなぜか無視され、デリゲートはDatePickerClockDelegate
// となってしまっている
if (delegate.javaClass != spinnerDelegateClass) {
delegateField[datePicker] = null // DatePickerClockDelegateを削除
datePicker.removeAllViews() // DatePickerClockDelegate viewを削除
val createSpinnerUIDelegate = DatePicker::class.java.getDeclaredMethod(
"createSpinnerUIDelegate",
Context::class.java,
AttributeSet::class.java,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
createSpinnerUIDelegate.isAccessible = true
// createSpinnerUIDelegateメソッドを通してDatePickerSpinnerDelegateを生成する
delegate = createSpinnerUIDelegate.invoke(
datePicker,
context,
null,
android.R.attr.datePickerStyle,
0
)
delegateField[datePicker] = delegate // DatePicker.mDelegateをspinnerに設定
datePicker.calendarViewShown = false
// DatePickerデリゲートを再度生成
datePicker.init(year, month, dayOfMonth, this)
}
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
companion object {
private fun findField(
objectClass: Class<*>,
fieldClass: Class<*>,
expectedName: String
): Field? {
try {
val field = objectClass.getDeclaredField(expectedName)
field.isAccessible = true
return field
} catch (e: NoSuchFieldException) {
// nop
}
// 見つからない場合は検索
for (searchField in objectClass.declaredFields) {
if (searchField.type == fieldClass) {
searchField.isAccessible = true
return searchField
}
}
return null
}
}
}