---
# System prepended metadata

title: TimePickerDialog
tags: [View, Android, Kotlin]

---

###### tags: `Android` `Kotlin` `View`
# TimePickerDialog
當想要讓使用者選擇時間，就會用到TimePicker，其中官方有提供它的Dialog
叫做TimePickerDialog，但僅提供基本的設定與取得時間，這對於想要客製化的開發者來說
實在不太方便，因此自製一個CustomTimePickerDialog，這篇文章應用於預約老師課程的情境

## CustomTimePickerDialog類別
```kotlin=
class CustomTimePickerDialog(context: Context, listener: OnTimeSetListener,
                             hour: Int, minute: Int, is24hour: Boolean)
    : TimePickerDialog(context, listener, hour, minute, is24hour) {

    private var mTimePickerListener: TimePickerListener? = null
    private var hour: Int? = null
    private var min: Int? = null

    interface TimePickerListener {

        fun onTimeChanged(view: TimePicker?, hourOfDay: Int, minute: Int)

        fun onPositiveClick(dialog: DialogInterface?, which: Int)

        fun onNegativeClick(dialog: DialogInterface?, which: Int)
    }

    // 當picker按鈕被點擊後調用
    override fun onClick(dialog: DialogInterface?, which: Int) {
        setDialogStatus(false)
        when (which) {
            DialogInterface.BUTTON_POSITIVE -> mTimePickerListener?.onPositiveClick(dialog, which)
            DialogInterface.BUTTON_NEGATIVE -> mTimePickerListener?.onNegativeClick(dialog, which)
        }
    }

    // 當picker的時間被選擇後調用
    override fun onTimeChanged(view: TimePicker?, hourOfDay: Int, minute: Int) {
        hour = hourOfDay
        min = minute
        mTimePickerListener?.onTimeChanged(view, hourOfDay, minute)
    }

    // 執行父類的onClick，僅影響要不要執行onTimeSet()
    fun superOnClick(dialog: DialogInterface?, which: Int) {
        super.onClick(dialog, which)
    }

    // 設定自定義監聽
    fun setTimePickerListener(l: TimePickerListener) {
        mTimePickerListener = l
    }

    // 我們想在Dialog按鈕被按下後，決定Dialog會不會消失，就需要用此方法，否則只要按下按鈕就一定會dismiss
    fun setDialogStatus(isDismiss: Boolean) {
        val field = this.javaClass.superclass?.superclass?.superclass?.getDeclaredField("mShowing")
        field?.isAccessible = true
        field?.set(this, isDismiss) //keep: false, dismiss: true
    }

    fun getHour(): Int? = hour

    fun getMinute(): Int? = min
}
```

## 使用方式
```kotlin=
//val time = arrayListOf<Array<Int>>() 存放老師上課星期與時段 ex:arrayOf(1, 12) 即星期日的12點(星期為1~7,1為日,7為六)
//val reserveCalendar = Calendar.getInstance() 放入選定的日期
val onTimeSetListener = TimePickerDialog.OnTimeSetListener { _, _, m -> } 
val timePickerDialog = CustomTimePickerDialog(mActivity, onTimeSetListener, 12, 0, true)

timePickerDialog.setTimePickerListener(object : CustomTimePickerDialog.TimePickerListener {

    var canReserve = false
    override fun onTimeChanged(view: TimePicker?, hourOfDay: Int, minute: Int) {
        // Revise minute to 0 or 30 only
        val hour = if (minute > 30) hourOfDay + 1 else hourOfDay
        val min = if (minute in 1..30) 30 else 0

        if (!(minute == 30 || minute == 0)) {
            timePickerDialog.onTimeChanged(view, hour, min)
            return
        }

        // Init selected calendar (means get first time of selected calendar)
        reserveCalendar.set(Calendar.HOUR_OF_DAY, 0)
        reserveCalendar.set(Calendar.MINUTE, 0)
        reserveCalendar.set(Calendar.SECOND, 0)
        Log.e("Selected Date", reserveCalendar.time.toString())

        // Get week of selected date and this week's workHour
        val week = reserveCalendar.get(Calendar.DAY_OF_WEEK)
        val workHour = arrayListOf<Int>()
        for (i in time)
            if (i[0] == week)
                workHour.add(i[1])

        // Validate reservation
        val now = Calendar.getInstance()
        reserveCalendar.timeInMillis = reserveCalendar.timeInMillis + hour * 3600000 + minute * 60000
        Log.e("Reserve", reserveCalendar.time.toString())

        canReserve = when {
            (now >= reserveCalendar) -> false
            (min == 30 && workHour.contains(hour + 1) && workHour.contains(hour)) -> true
            (min == 0 && workHour.contains(hour)) -> true
            else -> false
        }
    }

    override fun onPositiveClick(dialog: DialogInterface?, which: Int) {
        if (canReserve) {
            val hour = timePickerDialog.getHour()
            val min = timePickerDialog.getMinute()

            tv_time.text = String.format("%02d : %02d", hour, min)
            timePickerDialog.setDialogStatus(true)
        } else
            Toast.makeText(mActivity, "無法預約此時段", Toast.LENGTH_SHORT).show()
    }

    override fun onNegativeClick(dialog: DialogInterface?, which: Int) {
        timePickerDialog.setDialogStatus(true)
    }
})

timePickerDialog.show()
```

# 遇到的坑
在實作上遇到不少坑，在這個部分做一下紀錄，有些問題也是還理解原因，但暫時先避開

## 按下確定後執行想做的事情
### 方法一 : 設定監聽事件
在Dialog中，我們可以設定監聽讓按鈕做想做的事情，但是這在TimePickerDialog這個子類沒辦法，猜測是因為與原本的onClick衝突，也可能是我監聽用錯，等有空再研究，以下是有問題的code，即便重設onClick還是無法執行，但是按鈕文字是有修改的
```kotlin=
val picker = TimePickerDialog(mActivity, { view, hourOfDay, minute -> }, 12, 0, true)
picker.setButton(TimePickerDialog.BUTTON_POSITIVE, "預約", object : DialogInterface.OnClickListener {
                    override fun onClick(dialog: DialogInterface?, which: Int) {
                        Log.e("debug", "點擊")
                    }
                })
picker.show()
```

### 方法二 : 重設監聽事件
方法二與一不同的地方在於它是在Dialog show()之後，取得該Dialog的按鈕並重設監聽事件，此方法一定要在show之後才能get，否則會崩潰
使用後的結果是可以執行監聽，但由於重設監聽，所以原本的監聽無法調用，故OnTimeSetListener會失效，所以要另外想方法取得時間
```kotlin=
val picker = TimePickerDialog(mActivity, { view, hourOfDay, minute -> }, 12, 0, true)
picker.show()
picker.getButton(TimePickerDialog.BUTTON_POSITIVE).setOnClickListener {
    Log.e("debug", "點擊")
}
```

## 按下確定後讓Dialog不消失
這是屬於Dialog的問題，在點擊按鈕後Dialog最終都會調用Dismiss()，即便覆寫onClick方法還是會調用，爬了一些文章後，找出了兩種方式：
1. 第一種是重設監聽，但這在TimePickerDialog中並不好，因為它會無法按照週期正常工作
2. 第二種是反射法，也就是CustomTimePickerDialog中，使用的setDialogStatus方法，原理是取得這個類別的父層級參數mShowing，並將它做修改，因為Dialog類最終都是依靠這個參數，去判斷是否要讓Dialog消失，使用時要注意mShowing是在哪個層級，否則會無法取得，在TimePickerDialog中，就要三層才能取到
```kotlin=
fun setDialogStatus(isDismiss: Boolean) {
        val field = this.javaClass.superclass?.superclass?.superclass?.getDeclaredField("mShowing")
        field?.isAccessible = true
        field?.set(this, isDismiss) //keep: false, dismiss: true
    }
```

## (未處理)設定時間的間隔
這個問題我並沒有完美的處理好，網路上的文章一直都有人在問，但沒有好的答案
而我想到的方式是更新時間，當使用者點擊時間後，強制更改成我們要的時間
例如:我希望分鐘只有0跟30，那就在使用者點擊其他分鐘後，強制改成0跟30
這個可以用 TimePickerDialog 的 updateTime() 做到，它的好處是Picker的畫面跟著更新
但我在onTimeChanged的時候用卻出現奇怪的問題，設定1545應該要變成1600，但卻變成2300
可能是工作週期哪裡衝到，所以我最後用了 TimePickerDialog 的 onTimeChanged() 再設一次
但缺點就是畫面沒有更新，而設定間隔、畫面更新、updateTime等問題還有待研究