Help us understand the problem. What is going on with this article?

AndroidでSpinner形式のDatePickerDialogを表示する

AndroidでSpinner形式の DatePickerDialog を表示する方法を解説します。

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を使用してください。

Calendar形式の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
        }
    }
}

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away