Usa el TimePicker
en Android para seleccionar la hora del día a partir de controles que proveen hora y minuto. Podrás elegir entre modos de 24 horas o en el sistema de 12 horas AM/PM.
Al igual que el DatePicker
, Google nos recomienda envolver este view en un diálogo del tipo TimePickerDialog
. Y a su vez, este diálogo lo recubrimos con un fragmento DialogFragment
para encapsular ciclo de vida y mejorar la flexibilidad de UI.
En este tutorial del TimePicker
verás cómo crear, mostrar y procesar eventos del widget. Para ello te guiaremos con el siguiente ejemplo:
Esta App permite seleccionar una fecha inicial y final para luego determinar la duración temporal, acotada por ambos límites. Puedes descargar el proyecto Android Studio en Kotlin desde el siguiente enlace:
Crear TimePicker En Android
En primera instancia, nuestra necesidad de desplegar un selector de tiempo nace de presionar dos TextViews ubicados en nuestro layout, para determinar el rango temporal.
Para replicar este diseño, copia y pega la siguiente composición 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="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/time_selection_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Seleccionar fechas"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/to_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hasta"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/from_label" />
<TextView
android:id="@+id/from_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Desde"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/time_selection_label" />
<TextView
android:id="@+id/from_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="@color/light_blue_800"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/from_label"
tools:text="10:00 p. m." />
<TextView
android:id="@+id/to_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="@color/light_blue_800"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/to_label"
tools:text="12:00 p. m." />
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/from_time"
tools:text="2 Horas" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
Ambos requieren el registro de un observador OnClickListener
, por lo que invocamos setOnClickListener()
desde las referencias.
El cuerpo de la lambda de ambos contendrá la llamada a un método que en el futuro mostrará al TimePicker
:
class MainActivity : AppCompatActivity() {
private lateinit var startTime: TextView
private lateinit var endTime: TextView
private lateinit var duration: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
duration = findViewById(R.id.duration)
setUpStartTime()
setUpEndTime()
}
private fun setUpStartTime() {
startTime = findViewById(R.id.from_time)
startTime.text = LocalTime.now().format(formatter)
startTime.setOnClickListener {
showStartTimePicker()
}
}
private fun showStartTimePicker() {
}
private fun setUpEndTime() {
endTime = findViewById(R.id.to_time)
endTime.text = LocalTime.now().plusHours(2).format(formatter)
endTime.setOnClickListener {
showEndTimePicker()
}
}
private fun showEndTimePicker() {
}
}
Como ves, los métodos setUp*()
toman la referencia de los textos e inicializan sus valores con el método LocalTime.now()
. En el caso de setUpEndTime()
, añadimos 2 horas al tiempo actual con plusHours()
.
Añadir Soporte Para LocalTime
Como ves, usaremos a LocalTime
de java.time
para facilitar el procesamiento del tiempo. Sin embargo, para soportarla en versiones inferiores al nivel 26 del SDK, debes añadir las siguientes líneas en tu archivo build.gradle
:
android {
defaultConfig {
multiDexEnabled true
}
compileOptions {
coreLibraryDesugaringEnabled true
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:[ultima-version]'
}
Con esta estructura codificada, pasemos a explorar la receta para crear el selector de tiempo.
1. Crear DialogFragment Para TimePickerDialog
El primer paso será que crees una clase llamada TimePickerFragment
que extienda de DialogFragment
. En su interior sobrescribe al método onCreateDialog()
y pasa como retorno la creación de una instancia de TimePickerDialog
:
class TimePickerFragment : DialogFragment() {
private var hour: Int
private var minute: Int
init {
val now = LocalTime.now()
hour = now.hour
minute = now.minute
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return TimePickerDialog(requireActivity(), null, hour, minute, false)
}
}
Usaremos dos propiedades mutables para almacenar la hora y minuto del tiempo que se pasarán en la construcción del TimePickerDialog. Y en el bloque de inicio asignaremos los valores enteros del tiempo actual.
Por otro lado, los parámetros pasados al constructor público de TimePickerDialog
tienen el siguiente propósito:
context
: Contexto en el que vivirá el diálogothemeResId
: El ID del recurso de estilo que deseas aplicar al diálogo. Más adelante lo usaremos.listener
: ObservadorOnTimeSetListener
que notificará el momento en que se seleccione una fecha. Por el momento esnull
, pero ahora asignaremos una propiedad para que cumpla con este rol.hourOfDay
: Representa la hora con que se inicializará elTimePicker
minute
: Representa los minutos iniciales delTimePicker
is24HourView
: Si es modo 24 horas o AM/PM
En nuestro ejemplo usaremos el formato AM/PM, por lo que usamos false
en el último parámetro del constructor.
Pero si quisieras usar el formato que el usuario ha declarado en sus ajustes de preferencias, puedes averiguarlo con DateFormat.is24hourView()
. Con este resultado podrás maniobrar la lógica al construir el TimePicker
.
2. Mostrar TimePicker
Como ya es sabido, usamos el método show()
desde la instancia de TimePickerFragment
en los métodos show*()
:
private fun showStartTimePicker() {
showDialog()
}
private fun showEndTimePicker() {
showDialog()
}
private fun showDialog() {
TimePickerFragment().show(supportFragmentManager, "time-picker")
}
3. Procesar Cambio De Tiempo
Hasta aquí el selector de tiempo se despliega al clickear en los text views para el intervalo. Sin embargo, lo que deseamos es actualizar su propiedad text
para materializar la interacción:
¿Cómo hacerlo?
Habíamos hablado que el tipo OnTimeSetListener
habilita al TimePickerDialog
para notificar cambios en el tiempo. Por lo que añadiremos una propiedad llamada timeObserver
al fragmento y la pasaremos al constructor:
class TimePickerFragment : DialogFragment() {
private var timeObserver: OnTimeSetListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return TimePickerDialog(requireActivity(), timeObserver, hour, minute, false)
}
}
Ahora bien, la idea es crear un método de utilidad newInstance()
en un objeto compañero del fragmento. Esto permitirá aislar la construcción y la asignación de una escucha que venga como parámetro:
companion object {
fun newInstance(observer: OnTimeSetListener): TimePickerFragment {
return TimePickerFragment().apply {
timeObserver = observer
}
}
}
Luego, desde nuestra actividad, modificamos a showTimePicker()
para que invoque el método de fabricación y encadene la asignación de la fecha que es emitida desde el evento:
private fun showStartTimePicker() {
showDialog { _, hour, minute ->
val currentTime = LocalTime.of(hour, minute)
if (isValidStartTime(currentTime)) {
startTime.setTime(currentTime)
}
}
}
private fun showEndTimePicker() {
showDialog{ _, hour, minute ->
val currentTime = LocalTime.of(hour, minute)
if (isValidEndTime(currentTime)) {
endTime.setTime(currentTime)
}
}
}
private fun showDialog(observer: OnTimeSetListener) {
TimePickerFragment.newInstance(observer)
.show(supportFragmentManager, "time-picker")
}
OnTimeSetListener
posee un solo método abstracto llamado onTimeSet()
que recibe el TimePicker
, la hora y los minutos. Por esta razón es posible convertir la clase anónima en una lambda con dichos parámetros.
Una vez notificado el evento de cambio de tiempo, validaremos si el valor es asignable al tiempo. Para ello tenemos dos métodos de validación:
private fun isValidStartTime(time: LocalTime): Boolean {
return time < endTime.getTime()
}
private fun isValidEndTime(time: LocalTime): Boolean {
return time > startTime.getTime()
}
En consecuencia, creamos dos métodos para obtener y asignar la fecha a los TextViews a través de dos funciones de extensión alojadas en un nuevo archivo llamado TimeExt.kt
:
import android.widget.TextView
import java.time.Duration
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.util.*
private val formatter = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH)
fun TextView.getTime(): LocalTime {
return LocalTime.parse(text, formatter)
}
fun TextView.setTime(time: LocalTime) {
text = time.toTimeText()
}
fun LocalTime.toTimeText(): String {
return format(formatter)
}
infix fun LocalTime.hoursBetween(end: LocalTime): Double {
return Duration.between(this, end).toMinutes() / 60.0
}
Las anteriores funciones cumplen con las siguientes tareas:
getTime()
: Obtiene el texto delTextView
y lo parsea conLocalTime.parse()
setTime()
: Formatea el objetoLocalTime
entrante y lo asigna alTextView
toTimeText()
: Formatea un objetoLocalTime
aString
hoursBetween()
: Función de infijo que calcula la cantidad de minutos entre dosLocalTime
conDuration.between()
. Se divide en60.0
para obtener decimales en la duración.
Con este código ya tienes un funcionamiento básico del DatePickerFragment
.
Calcular Duración
Nuestra App de ejemplo incluye el cálculo de la duración, por lo que tendremos este paso adicional. La idea es proyectar la cantidad de horas contenidos en el intervalo:
El cálculo consiste en la sustracción del tiempo inicial del tiempo final cada vez que hay un cambio. Este resultado será mostrado en el tercer TextView
con el id duration
:
private fun updateDuration() {
val start = startTime.getTime()
val end = endTime.getTime()
val hours = start hoursBetween end
duration.text = "%.1f Horas".format(hours)
}
Al obtener la diferencia entre ambos tiempos, formateamos el resultado a un solo decimal en el TextView
de duración.
¡Muy bien!
Habiendo terminado la inducción sobre el uso del TimePicker
, pasemos a ver algunas configuraciones extras sobre este.
Pasar Argumentos A DatePickerFragment
Si te fijas en la aparición del TimePickerDialog
verás que el valor inicial es siempre la hora actual. Para mejorar esta situación, pasaremos como valor inicial el último valor seleccionado en cada TextView:
Asignaremos los valores iniciales vía argumentos del fragmento. Lo que significa que modificaremos al método newInstance()
para que reciba la hora y minutos:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { args ->
hour = args.getInt(HOUR_ARG)
minute = args.getInt(MINUTE_ARG)
}
}
companion object {
private const val MINUTE_ARG: String = "args.minute"
private const val HOUR_ARG: String = "args.hour"
fun newInstance(
hour: Int, minute: Int, observer: OnTimeSetListener
): TimePickerFragment {
val args = Bundle().apply {
putInt(HOUR_ARG, hour)
putInt(MINUTE_ARG, minute)
}
return TimePickerFragment().apply {
timeObserver = observer
arguments = args
}
}
}
De esta forma podrás modificar los componentes del tiempo a través el diálogo y personalizar al selector. Por ejemplo, para el tiempo inicial, obtenemos con getTime()
el valor actual del TextView
y lo pasamos al método:
private fun showStartTimePicker() {
val time = startTime.getTime()
showDialog(time.hour, time.minute) { _, hour, minute ->
val currentTime = LocalTime.of(hour, minute)
if (isValidStartTime(currentTime)) {
startTime.setTime(currentTime)
updateDuration()
}
}
}
private fun showDialog(initialHour: Int, initialMinute: Int, observer: OnTimeSetListener) {
TimePickerFragment.newInstance(initialHour, initialMinute, observer)
.show(supportFragmentManager, "time-picker")
}
Cambiar Tema Del TimePicker
Personalizar los colores y elementos del selector de fechas se puede realizar desde recursos de estilo aplicados en themes.xml
o con el parámetro del constructor de TimePickerDialog visto en el inicio:
Ambos casos requieren la temificación basada en uno cualquier estilo base del framework como ThemeOverlay.MaterialComponents.TimePicker
.
Si deseas aplicarlo a todos los diálogos, entonces aplícalo con el atributo android:timePickerDialogTheme
en tu tema de App (niveles mayores al SDK 21):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.TimePickerEnAndroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="android:timePickerDialogTheme">@style/ThemeOverlay.App.TimePicker</item>
</style>
<style name="ThemeOverlay.App.TimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker">
<item name="colorPrimary">@color/light_blue_900</item>
<item name="colorSecondary">@color/light_blue_800</item>
</style>
</resources>
O aplícalo individualmente desde el constructor en onCreateDialog(
):
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
//DateFormat.is24HourFormat()
val dialog = TimePickerDialog(
requireActivity(),
R.style.ThemeOverlay_App_TimePicker,
timeObserver,
hour,
minute,
false
)
return dialog
}
Más Recursos Android
- MaterialTimePicker (todo)
- Diálogos
- DatePicker
- Interfaz De Usuario
- Guía Android
- Cursos Android