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