El DatePicker
en Android es un view group compuesto por varios elementos que permiten al usuario seleccionar una fecha a partir de la configuración individual de año, mes y día.
Google te recomienda mostrarlo como un dialogo en vez de incrustarlo directamente en tus layouts, por lo que el framework de Android te provee a la clase DatePickerDialog
para aislar la aparición y comportamiento del widget.
Y a su vez, también recomiendan envolver al DatePickerDialog
con un DialogFragment
, para aislar su manejo del ciclo de vida y posibilitar la construcción de interfaces flexibles.
En este tutorial aprenderás a cómo crear, mostrar y procesar eventos de un DatePicker
a través del siguiente ejemplo:
Puedes descargar el proyecto Android Studio desde el siguiente enlace:
Crear DatePicker En Android
Antes que nada, vamos a habilitar varias funcionalidades de java.time
del JDK 8 para acceder a las clases LocalDate
y LocalDateTime
. Esto requiere que abras el archivo build.gradle
del módulo y habilites las siguientes características:
android {
defaultConfig {
multiDexEnabled true
}
compileOptions {
coreLibraryDesugaringEnabled true
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:[ultima-version]'
}
Ahora bien, el primer paso es crear un fragmento que herede de DialogFragment
y sobrescriba al método onCreateDialog()
. Como viste en el tutorial de diálogos, este método debe retornar la creación de una instancia AlertDialog
. Y como ya sabes, DatePickerDialog
es subclase de ella.
class DatePickerFragment() : DialogFragment() {
private var day: Int
private var month: Int
private var year: Int
init {
LocalDate.now().let { now ->
year = now.year
month = now.monthValue
day = now.dayOfMonth
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { args ->
year = args.getInt(YEAR_ARG)
month = args.getInt(MONTH_ARG)
day = args.getInt(DAY_ARG)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return DatePickerDialog(requireContext(), null, year, month, day)
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
// [Acciones si deseas sobrescribir al presionar "CANCELAR"]
}
}
Analicemos el código para comprender el funcionamiento:
- Añadimos tres propiedades enteras para representar el año, el mes y el día
- Usamos el bloque de
init
para inicializar las variables con valores por defecto. En este caso usaremos el paquetejava.time
para acceder a la claseLocalDate
y obtener la fecha actual con la funciónnow()
. - En
onCreateDialog()
usamos el constructor publico deDatePickerDialog()
y pasamos el contexto,null
(luego veremos este argumento) y los valores de la fecha. - E incluimos la sobrescritura de
onCancel()
por si deseas realizar alguna acción al cancelar el diálogo
Mostrar El DatePicker
El diálogo selector de fechas construido previamente lucirá así cuando lo despliegues:
Recuerda que puedes usar el método show()
para mostrar al DialogFragment
cuando lo desees. Lo importante es definir el punto donde se dispara esta invocación.
Nuestra App de ejemplo usa un EditText
como origen del evento de selección dentro de un formulario hipotético. La configuración del layout es la siguiente:
<?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/date_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fecha"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/purple_700"
app:layout_constraintBottom_toTopOf="@+id/date_field"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/date_field"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:focusable="false"
android:focusableInTouchMode="false"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
Si te fijas, el EditText
lleva el valor del atributo android:focusable
en false
, para evitar que el usuario pase el foco al campo de texto y no permitir la entrada de texto. Esto permitirá disparar el evento de click al primer toque.
El anterior diseño te permitirá obtener la referencia del campo de texto desde tu código Kotlin y añadirle un observador OnClickListener
. En el momento que ocurra el click, mostramos al diálogo selector de fecha:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dateField = findViewById(R.id.date_field)
dateField.setOnClickListener {
showDatePicker()
}
}
private fun showDatePicker() {
DatePickerFragment().show(supportFragmentManager, "date-picker")
}
Como ves, en la lambda que representa la escucha del click sobre el campo de texto, se ejecuta el método showDatePicker()
, el cual muestra una instancia de DatePickerFragment
.
Observar Eventos De Selección De Fecha
Para darle significado a la selección de fechas necesitamos tomar el valor y darle utilidad. Por ejemplo, mostrarle al usuario en el EditText
que el estado ha cambiado al actualizar el texto.
Es aquí donde usas la interfaz DatePickerDialog.OnDateSetListener
para detectar cuando el usuario cambia la fecha.
Esta cuenta con el controlador onDateSet()
, donde escribes las sentencias que deseas ejecutar en los cambios de fecha. Debido a que necesitamos delegar este comportamiento hacia nuestra actividad, añadimos una propiedad llamada observer
que le redireccione la ejecución.
class DatePickerFragment : DialogFragment() {
private var observer: DatePickerDialog.OnDateSetListener? = null
//...
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return DatePickerDialog(requireContext(), observer, year, month, day)
}
// ...
companion object {
fun newInstance(
listener: DatePickerDialog.OnDateSetListener
): DatePickerFragment {
val datePicker = DatePickerFragment()
datePicker.listener = listener
return datePicker
}
}
}
Adicionalmente añadiremos un objeto compañero para encapsular la creación de la instancia de DatePickerFragment
, con el fin de asignar el observador y otros parámetros.
Luego, desde la actividad, creamos una instancia de DatePickerFragment
y le asignamos el observador de cambios de fecha. Este puede ser representado por una lambda con los mismos parámetros de onDateSet()
y el cuerpo será la asignación del texto al EditText
:
class MainActivity : AppCompatActivity() {
private val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
//...
private fun showDatePicker() {
DatePickerFragment.newInstance{ _, year, month, day ->
dateField.setText(formatDate(year, month, day))
}.show(supportFragmentManager, "date-picker")
}
private fun formatDate(year: Int, month: Int, day: Int): String {
val sanitizeMonth = month + 1
return LocalDate.of(year, sanitizeMonth, day).format(formatter)
}
}
Puntos a tener en cuenta:
showDatePicker()
: Este método fue modificado para invocar anewInstance()
con el observador de cambios de fecha. En el cuerpo de la lambda que representa a la instanciaOnDateSetListener
, asignamos un texto que crea un string a partir de los parámetrosyear
,month
yday
.formatDate()
: Recibe el año, mes y día para crear un String formateado para mostrar en el campo de texto. Para ello se usaformat()
con el formateador existente. Adicionalmente sumamos la unidad al mes, ya queDatePickerDialog
interpreta al mes con el rango[0, 11]
.
Establecer Fecha Desde Argumentos
Hasta el momento nuestro date picker se muestra y actualiza el campo de texto, pero siempre que se abre, comienza seleccionado el mismo día. La idea es iniciar el selector con el valor previo seleccionado.
Una de las formas de asignar los valores iniciales para la fecha es pasar argumentos al DialogFragment
. Por lo que luego de usar su constructor, asignamos la propiedad arguments
al Bundle
con el día, mes y año:
companion object {
const val YEAR_ARG = "args.year"
const val MONTH_ARG = "args.month"
const val DAY_ARG = "args.day"
fun newInstance(
year: Int, month: Int, day: Int,
observer: DatePickerDialog.OnDateSetListener
): DatePickerFragment {
val datePicker = DatePickerFragment()
datePicker.arguments = Bundle().apply {
val adjustMonth = month - 1
putInt(YEAR_ARG, year)
putInt(MONTH_ARG, adjustMonth)
putInt(DAY_ARG, day)
}
datePicker.observer = observer
return datePicker
}
}
Como LocalDate
trabaja con el rango [1, 12]
para el valor entero del mes, entonces aprovechamos para restar la unidad al argumento MONTH_ARG
.
Luego desde onCreate()
en el fragmento, los extraemos y almacenamos en propiedades que serán usadas en el constructor del DatePickerDialog()
:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { args ->
year = args.getInt(YEAR_ARG)
month = args.getInt(MONTH_ARG)
day = args.getInt(DAY_ARG)
}
}
En seguida, vamos a la actividad y obtenemos la fecha actual desde el EditText
:
private fun showDatePicker() {
val date = getCurrentDate()
DatePickerFragment.newInstance(
date.year,
date.monthValue,
date.dayOfMonth
) { _, year, month, day ->
dateField.setText(formatDate(year, month, day))
}.show(supportFragmentManager, "date-picker")
}
private fun getCurrentDate(): LocalDate {
val date = dateField.text.toString()
return LocalDate.parse(date, formatter)
}
getCurrentDate()
obtiene el texto actual del campo de texto y luego lo parsea a LocalDate
. Tanto parse()
como format()
requieren el mismo formateador para mantener la consistencia de fechas.
Cambiar Color De DatePicker
Supón que vamos a personalizar el color usado sobre el DatePicker
de nuestra App de ejemplo con el siguiente azul:
¿Cómo lograrlo?
Depende del objetivo. Si quieres que todos los Date Pickers en tu App sean del mismo color, entonces realiza una superposición del tema con el atributo de estilo android:datePickerDialogTheme
:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.DatePickerEnAndroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="android:datePickerDialogTheme">@style/DatePickerStyle</item>
</style>
<style name="DatePickerStyle" parent="ThemeOverlay.MaterialComponents.Dialog">
<item name="colorPrimary">@color/blue_500</item>
<item name="colorSecondary">@color/blue_500</item>
</style>
</resources>
En el caso que solo desees aplicarlo a un DatePicker
en específico, entonces usa la versión del constructor público, que acepta el tema personalizado, sobre el método onCreateDialog()
:
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return DatePickerDialog(
requireActivity(),
R.style.DatePickerStyle,
observer,
year,
month,
day
)
}
Añadir Límites A La Fecha
Considera un cambio de reglas de negocio que te exigen acotar la selección de fechas a los sietes últimos días a partir de hoy. El DatePicker
tiene que deshabilitar los demás valores para evitar que el usuario rompa esta acotación:
Para establecer la fecha mínima y la fecha máxima, usa los atributos minDate
y maxDate
de la clase DatePicker
respectivamente:
import java.time.LocalDateTime
import java.time.ZoneId
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = DatePickerDialog(
requireActivity(),
R.style.DatePickerStyle,
observer,
year,
month,
day
)
val today = LocalDateTime.now()
val sevenDaysAgo = today.minusDays(7)
dialog.datePicker.minDate= sevenDaysAgo.toMs()
dialog.datePicker.maxDate = today.toMs()
return dialog
}
private fun LocalDateTime.toMs(): Long {
return atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
}
La clase DatePickerDialog
tiene como propiedad a la instancia del DatePicker
. Por lo que es posible acceder directamente a los atributos de límite de fechas.
Ambos son de tipo Long
, ya que reciben la fecha en milisegundos. Para ello usamos la clase LocalDataTime
y una función de extensión que convierta a milisegundos estas instancias.
Obtener la fecha de siete días previos se realiza con el método minusDays()
sobre la fecha actual today
. De esta forma asignamos los límites al selector.