###### 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等問題還有待研究