Chuyện là hôm nay mình code 1 cái Range time picker ban đầu tính sẽ sử dụng thư viện luôn cho lẹ, tuy nhiên thời gian cũng thư thả nên mình cũng thử tự tạo bằng code của mình.
Và kết quả như này:
Nhìn cũng khá là ổn áp ấy chứ, nhưng cuối cùng không đúng yêu cầu của ứng dụng nên không dùng nữa =)))
Oke giờ mình sẽ Step by step cách làm:
1. Dựng layout y như hình ở trên:
Tạo file layout_rang_time_picker.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="24dp" android:background="@drawable/bg_picker"> <TextView android:id="@+id/btnStartTime" android:layout_width="0dp" android:layout_height="wrap_content" android:drawableTop="@drawable/ic_start_time_24dp" android:gravity="center" android:padding="8dp" android:text="Start Time" android:textColor="@color/white" android:textSize="15sp" android:textStyle="bold" app:layout_constraintEnd_toStartOf="@+id/centerView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <View android:id="@+id/centerView" android:layout_width="0.5dp" android:layout_height="0dp" android:background="@color/white" app:layout_constraintBottom_toBottomOf="@+id/btnStartTime" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/btnEndTime" android:layout_width="0dp" android:layout_height="wrap_content" android:drawableTop="@drawable/ic_end_time_24dp" android:gravity="center" android:padding="8dp" android:text="End Time" android:textColor="@color/white" android:textSize="15sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/centerView" app:layout_constraintTop_toTopOf="parent" /> <View android:id="@+id/bottomView" android:layout_width="match_parent" android:layout_height="0.5dp" android:background="@color/white" app:layout_constraintTop_toBottomOf="@+id/btnEndTime" /> <TimePicker android:id="@+id/timePicker" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:headerBackground="@color/windowsBlue" android:numbersBackgroundColor="@color/color_num" android:numbersSelectorColor="@color/goldenYellow" android:numbersTextColor="@color/black" app:layout_constraintTop_toBottomOf="@+id/bottomView" /> <TextView android:id="@+id/tvOK" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="16dp" android:layout_weight="1" android:background="@drawable/bg_rad_white" android:gravity="center" android:paddingTop="10dp" android:paddingBottom="10dp" android:text="OK" android:textColor="@color/royal" android:textSize="14sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/timePicker" tools:ignore="ButtonStyle" /> </androidx.constraintlayout.widget.ConstraintLayout>
Các file liên quan background, icon, string, color download tại source code bên dưới.
2. Tạo interface dể trả về giá trị sau khi chọn.
interface RangePickerListener { fun onTimePick(startTime: String?, endTime :String?) }
3. Tạo PickerRangeTimeDialog.kt để xử lý các logic về chọn thời gian
- Khai báo các giá trị đầu vào, các hàm format thời gian.
//Start time- End time arguments private var startTime: String? = null private var endTime: String? = null //Callback return time selected var listener: RangePickerListener? = null //Mode picker start/end private var isStartMode = true //Format date/time private val fullSDF = SimpleDateFormat("yyyy-MM-dd HH:mm") private var currentDate = SimpleDateFormat("yyyy-MM-dd").format(Date()) private val timeSDF = SimpleDateFormat("HH:mm")
- Tạo newInstance cho PickerRangeTimeDialog
companion object { private const val KEY_START_TIME = "KEY_START_TIME" //HH:mm private const val KEY_END_TIME = "KEY_END_TIME" //HH:mm fun newInstance(startTime: String?, endTime: String?): PickerRangeTimeDialog { val args = Bundle() args.putString(KEY_START_TIME, startTime) args.putString(KEY_END_TIME, endTime) val fragment = PickerRangeTimeDialog() fragment.arguments = args return fragment } }
Instance này có thể truyền vào startTime- EndTime để hiện giá trị default mà bạn muốn.
Tiếp tục xử lý các logic ở onViewCreated
Đầu tiên đọc giá trị truyền vào.
startTime = arguments?.getString(KEY_START_TIME) endTime = arguments?.getString(KEY_END_TIME)
Setup Picker format 24h và tiến hành lắng nghe khi picker thay đổi và update giá trị theo các mode tương ứng.
timePicker.setIs24HourView(true) timePicker.setOnTimeChangedListener { _, hourOfDay, minute -> val date = fullSDF.parse("$currentDate $hourOfDay:$minute") date?.let { if (isStartMode) { startTime = timeSDF.format(date) Log.d("Picker", startTime!!) } else { endTime = timeSDF.format(date) Log.d("Picker", endTime!!) } } }
Tạo hàm setUpStartTime() cho mode start và thiết lập giá trị startTime vào picker.
private fun setUpStartTime() { context?.let { btnStartTime.setTextColor(ContextCompat.getColor(it, R.color.goldenYellow)) btnEndTime.setTextColor(ContextCompat.getColor(it, R.color.white)) btnEndTime.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_end_time_24dp, 0, 0) btnStartTime.setCompoundDrawablesWithIntrinsicBounds( 0, R.drawable.ic_start_time_selected_24dp, 0, 0 ) } isStartMode = true startTime?.let { val startFull = "$currentDate $startTime" val cal = Calendar.getInstance() cal.time = parserDate(startFull) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { timePicker.hour = cal.get(Calendar.HOUR_OF_DAY) timePicker.minute = cal.get(Calendar.MINUTE) } else { timePicker.currentHour = cal.get(Calendar.HOUR_OF_DAY) timePicker.currentMinute = cal.get(Calendar.MINUTE) } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { startTime = "${timePicker.hour}:${timePicker.minute}" } else { startTime = "${timePicker.currentHour}:${timePicker.currentMinute}" } Log.d("PickerStartTime", it) } }
Tương tự tạo hàm setUpEndTime() cho mode end và thiết lập giá trị endTime vào picker.
private fun setUpEndTime() { context?.let { btnEndTime.setTextColor(ContextCompat.getColor(it, R.color.goldenYellow)) btnStartTime.setTextColor(ContextCompat.getColor(it, R.color.white)) btnStartTime.setCompoundDrawablesWithIntrinsicBounds( 0, R.drawable.ic_start_time_24dp, 0, 0 ) btnEndTime.setCompoundDrawablesWithIntrinsicBounds( 0, R.drawable.ic_end_time_selected_24dp, 0, 0 ) } isStartMode = false endTime?.let { val endFull = "$currentDate $endTime" val cal = Calendar.getInstance() cal.time = parserDate(endFull) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { timePicker.hour = cal.get(Calendar.HOUR_OF_DAY) timePicker.minute = cal.get(Calendar.MINUTE) } else { timePicker.currentHour = cal.get(Calendar.HOUR_OF_DAY) timePicker.currentMinute = cal.get(Calendar.MINUTE) } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { endTime = "${timePicker.hour}:${timePicker.minute}" } else { endTime = "${timePicker.currentHour}:${timePicker.currentMinute}" } Log.d("PickerEndTime", it) } }
//Format full: yyyy-MM-dd HH:mm to Date private fun parserDate(fullDate: String): Date { val date = fullSDF.parse(fullDate) date?.let { return it }.run { return Date() } }
Oke đã xong các hàm cần thiết, chung ta quay lại onViewCreate để sử dụng các hàm trên.
//Lần đầu mở Dialog setup startTime setUpStartTime()
//Chuyển đổi 2 mode start và end khi click vào phần header. btnStartTime.setOnClickListener { setUpStartTime() } btnEndTime.setOnClickListener { setUpEndTime() }
Setup buttton OK để lấy giá trị.
tvOK.setOnClickListener { startTime?.let { start -> endTime?.let { end -> //Full DateTime: yyyy-MM-dd HH:mm val startFull = "$currentDate $startTime" val endFull = "$currentDate $endTime" //Convert to Date val dateStart: Date = parserDate(startFull) val dateEnd: Date = parserDate(endFull) //Compare start with end if (dateStart.time > dateEnd.time) { //TODO: Show Alert Error nếu cần. } else { Log.d("Picker", "$start~$end") listener?.onTimePick(start, end) dialog?.dismiss() } }.run { listener?.onTimePick(startTime, endTime) dialog?.dismiss() } } }
Như bạn thấy để parser giá trị nhận được về format “HH:mm” thì đầu tiên mình đã chuyển nó về dạng đầy đủ “yyyy-MM-dd HH:mm“.
Sau đó mình muốn so sánh startTime không được quyền lớn hơn endTime mình format nó về date và tiến hành so sánh.
Và khi thoả mãn điều kiện thì giá trị sẽ được return về qua hàm callback:
listener?.onTimePick(startTime, endTime)
Và không quên hàm quan trong để mở Dialog:
val picker = PickerRangeTimeDialog.newInstance("10:00", "14:00") picker.listener = object : RangePickerListener { override fun onTimePick(startTime: String?, endTime: String?) { } } picker.show(supportFragmentManager, "PickerRangeTimeDialog")