Create Range Time Picker Android Simple

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")

Source Github.

Nguyễn Linh

Chia sẻ để cùng tiến bộ...