Dialogos de selección de fecha y hora

Los cuadros de dialogo fueron introducidos en el ejercicio Un cuadro de dialogo para indicar el id de lugar. En esa ocasión aprendimos a realizar un cuadro de dialogo personalizado. En este apartado aprenderemos a utilizar cuadros de diálogo específicos para trabajar con fechas y horas.Empezaremos introduciendo algunos conceptos y clases que nos ayudarán a trabajar con este tipo de información.

Clases para trabajar con fechas en Java:

 

Clase Date[1]

 La clase Date representa un instante en el tiempo con una precisión de milisegundos. Se utiliza un sistema de medición del tiempo independiente de la zona horaria, conocido como UTC (Tiempo Universal Coordinado). El estándar de medición de tiempo UTC utiliza el tiempo en el meridiano de Greenwich independientemente de .donde nos encontremos. De esta forma, se evitan los problemas que aparecen cuando se comunican dos sistemas con mediciones locales de tiempo diferentes.

Para representar un instante de tiempo se suele utilizar la codificación conocida como “Tiempo Unix”. Esta codificación consiste en medir el número de milisegundos trascurridos desde el 1 de enero de 1970. Para almacenar este valor se utiliza un entero de 64 bits, en Java la palabra reservada long representa a un entero de este tipo. Si quieres en Android obtener el tiempo actual en este formato utiliza el método currentTimeMillis() de la clase System.

long ahora = System.currentTimeMillis();
Date fecha = new Date(ahora); 


Clase DateFormat[2]

La clase Date está pensada para contar el tiempo de forma Universal en toda la tierra, de forma que sea sencilla de manipular por una máquina. Sin embargo, las personas utilizamos una medición del tiempo que depende de la zona horaria donde estemos o incluso dependerá de si el país donde estemos utiliza el horario de verano. Cuando tengas que mostrar o solicitar una fecha a una persona, deberás utilizar la representación del tiempo a la que está acostumbrada. En este caso la clase abstracta DateFormat o su descendiente SimpleDateFormat[3] te serán de gran ayuda para este propósito.

A continuación se muestra un ejemplo sencillo:

DateFormat df = new SimpleDateFormat("dd/MM/yy");
String salida = df.format(fecha);


Clase Calendar[4]

Como hemos comentado, la clase Date utiliza internamente un simple entero para representar un instante de tiempo. Por el contrario, los humanos nos complicamos algo más dado que usamos la combinación de varios campos: como año, mes, día, hora, minuto y milisegundo. Utiliza la clase Calendar para obtener estos campos desde un objeto Date. A diferencia de la clase Date, la clase Calendar depende de la configuración local del dispositivo (locale). Para obtener, la fecha actual según la representación local del dispositivo utiliza el método getInstance():

Calendar calendario = Calendar.getInstance();
calendario.setTimeInMillis(ahora);            
int hora = calendario.get(Calendar.HOUR_OF_DAY);
int minuto = calendario.get(Calendar.MINUTE); 

La clase Calendar es una clase abstracta, que en principio te permitiría trabajar con cualquier clase de calendario (como el calendario maya o el musulmán). No obstante, el calendario usado oficialmente en casi todo el mundo es el calendario Gregoriano, definido en la clase GregorianCalendar.
 

Ejercicio: Añadiendo un dialogo de selección para cambiar la hora.
 

Un cuadro de diálogo es un tipo de ventana emergente que solicita al usuario de la aplicación algún tipo de información, antes de realizar algún proceso. Este tipo de ventanas no suele ocupar la totalidad de la pantalla. En la aplicación Mis Lugares hemos utilizado diálogos en dos ocasiones: para indicar el id a mostrar y para confirmar el borrado de un lugar. En este ejercicio aprenderemos a hacer un dialogo más complejo, que permite modificar la hora y los minutos.

1.    Abre el layout vista_lugar.xml y localiza el <imageView> que indica la hora asociada al lugar. Añade el atributo marcado:

<ImageView
    android:id="@+id/icono_hora"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:contentDescription="logo de la hora"
    android:src="@android:drawable/ic_menu_recent_history" /> 


2.    Abre la clase VistaLugarFragment y añade en el método onActivityCreate() el siguiente código. Si no has realizado el ejercicio con fragments añadelo en la clase VistaLugar dentro de onCreate(), pero serán necesarias ligeras modificaciones en los siguientes puntos.

vista.findViewById(R.id.icono_hora).setOnClickListener(
   new OnClickListener() {
      public void onClick(View view) { usoLugar.cambiarHora(pos); } }); 
icono_hora.setOnClickListener { usoLugar.cambiarHora(pos) }
hora.setOnClickListener { usoLugar.cambiarHora(pos) } 

NOTA: Selecciona el paquete android.view.OnClickListener. Repite la operación, pero cambiando R.id.icono_hora por R.id.hora.
 

3.   Como acabas de ver vamos a crear un nuevo caso de uso para cambiar la hora de un lugar. La clase CasosUsoLugar empieza a ser demasiado grande. Podría ser interesante dividirla en tres partes: Operaciones de tipo CRUD (altas, bajas, modificaciones…), fotografías y de fecha y hora. De momento vamos a añadir las operaciones de fecha y hora en la clase CasosUsoLugarFecha:

public class CasosUsoLugarFecha extends CasosUsoLugar {

   public CasosUsoLugarFecha(FragmentActivity actividad, Fragment fragment,
                        LugaresBD lugares, AdaptadorLugaresBD adaptador) {
      super(actividad, lugares, adaptador);
   }
} 
class CasosUsoLugarFecha(
   override val actividad: FragmentActivity,
   override val fragment: Fragment,
   override val lugares: LugaresBD,
   override val adaptador: AdaptadorLugaresBD
) : CasosUsoLugar(actividad, lugares, adaptador) { 
Vamos a heredar de CasoUsoLugar al necesitar funciones definidas en esta clase (en concreto actualizaPosLugar(pos, lugar) )

4.   En Kotlin aparecerán errores dado que por defecto las clases y las propiedades son cerradas. Si utilizas el desplegable de la bobilla podrás corregir rápidamente estos errores. El resultado final ha de ser:

En Java para poder utilizar propiedades o métodos de la clase padre, estos no pueden tener el modificador private. Cambia los que vayas a utilizar a protected. 
private protected FragmentActivity actividad;
private protected Fragment fragment;
private protected AdaptadorLugaresBD adaptador;
private protected void actualizaPosLugar(int pos, Lugar lugar) { 
 open class CasosUsoLugar(
   open val actividad: FragmentActivity,
   open val fragment: Fragment,
   open val lugares: LugaresBD,
   open val adaptador: AdaptadorLugaresBD) { 

5.   Añade en la nueva clase:

int pos =-1;
Lugar lugar;

public void cambiarHora(int pos) {
   lugar = adaptador.lugarPosicion(pos);
   this.pos = pos;
   DialogoSelectorHora dialogo = new DialogoSelectorHora();
   dialogo.setOnTimeSetListener(this);
   Bundle args = new Bundle();
   args.putLong("fecha", lugar.getFecha());
   dialogo.setArguments(args); 
   dialogo.show(actividad.getSupportFragmentManager(), "selectorHora");
} 
var pos: Int = -1
lateinit var lugar: Lugar

fun cambiarHora(pos: Int, textView: TextView) {
   lugar = adaptador.lugarPosicion(pos)
   this.pos = pos
   val dialogo = DialogoSelectorHora()
   dialogo.setOnTimeSetListener(this)
   val args = Bundle();
   args.putLong("fecha", lugar.fecha)
   dialogo.setArguments(args) 
   dialogo.show(actividad.supportFragmentManager, "selectorHora")
} 

Este método se ejecutará cuando se pulse sobre el icono de hora. Su objetivo es mostrar un cuadro de diálogo para que el usuario pueda modificar la hora asociada al lugar. Los parámetros son: el pos del lugar a modificar y el TextView donde escribiremos la nueva hora. Comenzamos tres variables que usaremos para recordar la información tras volver del diálogo.
Continuamos creando un nuevo diálogo y luego le asignamos el escuchador a nuestra propia clase. De esta forma, cuando el usuario cambie la hora se llamará a un método de nuestra clase. Este método lo crearemos en uno de los puntos siguientes. A este diálogo le pasamos como argumento la fecha del lugar en un long. Finalmente, mostramos el diálogo llamando al método show(). Este método utiliza dos parámetros: el manejador de fragments y una etiqueta que identificará el cuadro de diálogo.
 

6.    Crea la siguiente clase

public class DialogoSelectorHora extends DialogFragment {

    private OnTimeSetListener escuchador;

    public void setOnTimeSetListener(OnTimeSetListener escuchador) {
        this.escuchador = escuchador;
    }
    
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Calendar calendario = Calendar.getInstance();
        Bundle args = this.getArguments();
        if (args != null) {
            long fecha = args.getLong("fecha");
            calendario.setTimeInMillis(fecha);
        }
        int hora = calendario.get(Calendar.HOUR_OF_DAY);
        int minuto = calendario.get(Calendar.MINUTE);
        return new TimePickerDialog(getActivity(), escuchador, hora, 
                  minuto, DateFormat.is24HourFormat(getActivity()));
    }
 } 
class DialogoSelectorHora : DialogFragment() {

   private var escuchador: OnTimeSetListener? = null

   fun setOnTimeSetListener(escuchador: OnTimeSetListener) {
      this.escuchador = escuchador
   }

   override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
      val calendario = Calendar.getInstance()
      val fecha = arguments?.getLong("fecha")?:System.currentTimeMillis()
      calendario.setTimeInMillis(fecha)
      val hora = calendario.get(Calendar.HOUR_OF_DAY)
      val minuto = calendario.get(Calendar.MINUTE)
      return TimePickerDialog(
         getActivity(), escuchador, hora,
         minuto, DateFormat.is24HourFormat(getActivity())
      )
   }
} 

Pulsa Alt-Intro para añadir los imports automáticamente. Algunas clases se encuentran en varios paquetes, por lo que te preguntará. Utiliza los que se muestran marcados:
 

 
 

Esta clase extiende DialogFragment, que define un fragment que muestra una ventana de diálogo flotante sobre la actividad. El control del cuadro de diálogo debe hacerse siempre a través de los métodos del API, nunca directamente.

Para definir un nuevo DialogFragment se puede sobreescribir onCreateView() para indicar el contenido del diálogo. Alternativamente, se puede sobreescribir onCreateDialog() para crear un diálogo totalmente personalizado, como hacemos en este ejercicio. En este método hay que devolver un objeto Dialog que se  mostrará.

Creamos un objeto Calendar y si nos han pasado una fecha se la asignamos. En caso contrario, la fecha corresponderá con la actual. Luego extraemos la hora y los minutos del calendario.

Finalmente creamos un nuevo dialogo de la clase TimePickerDialog. Se trata de un tipo de dialogo definido en el sistema que nos permite seleccionar horas y minutos. En su constructor indicamos cuatro parámetros: el contexto, un escuchador al que llamará cuando se seleccione la hora, la hora y minutos que se mostrarán al inicio y un valor booleano que indica si trabajamos con formato de 24 horas o de 12. En el código se usa el valor definido en nuestro contexto.
 

7.    Haz que CasosUsoLugarFecha implemente la siguiente interfaz:

public class CasosUsoLugarFecha extends CasosUsoLugar  
                           implements TimePickerDialog.OnTimeSetListener { 
class CasosUsoLugarFecha(…) : CasosUsoLugar(…),
                              TimePickerDialog.OnTimeSetListener { 


8.    Añade la siguiente función:

@Override public void onTimeSet(TimePicker vista, int hora, int minuto) {
   Calendar calendario = Calendar.getInstance();
   calendario.setTimeInMillis(lugar.getFecha());
   calendario.set(Calendar.HOUR_OF_DAY, hora);
   calendario.set(Calendar.MINUTE, minuto);        
   lugar.setFecha(calendario.getTimeInMillis());
   actualizaPosLugar(pos, lugar);
   TextView textView = actividad.findViewById(R.id.hora);        
   textView.setText(DateFormat.getTimeInstance().format(
                                            new Date(lugar.getFecha())));
} 
override fun onTimeSet(vista: TimePicker?, hora: Int, minuto: Int) {
   val calendario = Calendar.getInstance()
   calendario.setTimeInMillis(lugar.fecha)
   calendario.set(Calendar.HOUR_OF_DAY, hora)
   calendario.set(Calendar.MINUTE, minuto)
   lugar.fecha = calendario.getTimeInMillis()
   actualizaPosLugar(pos, lugar)
   val textView = actividad.findViewById<TextView>(R.id.hora)
   textView.text= DateFormat.getTimeInstance().format(Date(lugar.fecha))
} 

En el punto anterior hemos indicado que nuestra clase actuaría como escuchador, cuando se seleccionara una hora en el cuadro de diálogo. Como consecuencia este método será llamado. Se nos pasan tres parámetos. En este caso nos interesa la hora y los minutos seleccionados. Para cambiar esta información en la fecha asociada al lugar, comenzamos creando un objeto Calendar y lo inicializamos con la fecha que tiene el lugar. Luego, le modificamos la hora y los minutos según los parámetros que nos han indicado. Hay que aclarar que el resto de la fecha, como el día o el mes, no van a modifcarse. La nueva fecha es introducida en el objeto lugar y a continuación actualizamos la base de datos.

Para modificar el TextView de la hora, comenzamos creando un formato de fecha, donde se visualizará la hora y los minutos separado por dos puntos. Para convertir la fecha correctamente hay que conocer la zona horaria definida en el sistema. Esto se consigue con java.util.Locale.getDefault(). Finalmente usamos este formato sobre un objeto Date para cambiara el contenido del TextView.     
 
9.    En VistaLugarActivity utiliza la clase CasosUsoLugarFecha en lugar de CasosUsoLugar para la variable usoLugar.

10.    Ejecuta la aplicación y verifica el resultado.

 

Práctica: Añadiendo un dialogo de selección para cambiar la fecha.
 

Podrías crear un cuadro de dialogo para modificar la fecha asociada al lugar (día, mes y año). Has de realizar los mismos pasos que en el ejecio anterior, pero ahora se basará en el diálogo siguiente.

En este caso tendrás que usar un diálogo de la clase DatePickerDialog:

DatePickerDialog(actividad , escuchador, año, mes, dia); 

El escuchador ha de implentar el interface OnDateSetListener y este interface define el siguiente método:

@Override
public void onDateSet(DatePicker view, int anyo, int mes, int dia) {…} 

Finalmente utiliza el siguiente formato para representarlo:

DateFormat formato = DateFormat.getDateInstance(); 

En este caso no se define un formato concreto como en el ejercicio anterior, si no que se selecciona el definido en el sistema para representar una fecha. De esta forma, el formato será el que ha configurado el usuario en el dispositivo.
 

Solución:
 

Clase VistaLugarFragment dentro de onActivityCreated():

vista.findViewById(R.id.icono_fecha).setOnClickListener(
   new OnClickListener() {
      public void onClick(View view) { usoLugar.cambiarFecha(pos); } }); 
icono_fecha.setOnClickListener { usoLugar.cambiarFecha(pos) }
fecha.setOnClickListener { usoLugar.cambiarFecha(pos) } 


En la clase DialogoSelectorFecha añade la interfaz y las dos funciones indicadas.

public class CasosUsoLugarFecha extends CasosUsoLugar implements
  TimePickerDialog.OnTimeSetListener , DatePickerDialog.OnDateSetListener {

public void cambiarFecha(int pos) {
   lugar = adaptador.lugarPosicion(pos);
   this.pos = pos;
   DialogoSelectorFecha dialogo = new DialogoSelectorFecha();
   dialogo.setOnDateSetListener(this);
   Bundle args = new Bundle();
   args.putLong("fecha", lugar.getFecha());
   dialogo.setArguments(args);
   dialogo.show(actividad.getSupportFragmentManager(),"selectorFecha");
}

@Override
public void onDateSet(DatePicker view, int año, int mes, int dia) {
   Calendar calendario = Calendar.getInstance();
   calendario.setTimeInMillis(lugar.getFecha());
   calendario.set(Calendar.YEAR, año);
   calendario.set(Calendar.MONTH, mes);
   calendario.set(Calendar.DAY_OF_MONTH, dia);
   lugar.setFecha(calendario.getTimeInMillis());
   actualizaPosLugar(pos, lugar); 
   TextView textView = actividad.findViewById(R.id.fecha);    
   textView.setText(DateFormat.getDateInstance().format(
                                             new Date(lugar.getFecha())));
} 
class CasosUsoLugarFecha(…) : CasosUsoLugar(…),
   TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener{

fun cambiarFecha(pos: Int) {
   lugar = adaptador.lugarPosicion(pos)
   this.pos = pos
   val dialogo = DialogoSelectorFecha()
   dialogo.setOnDateSetListener(this)
   val args = Bundle()
   args.putLong("fecha", lugar.fecha)
   dialogo.setArguments(args)
   dialogo.show(actividad.supportFragmentManager, "selectorFecha")
}

override fun onDateSet(view: DatePicker, año: Int, mes: Int, dia: Int) {
   val calendario = Calendar.getInstance()
   calendario.timeInMillis = lugar.fecha
   calendario.set(Calendar.YEAR, año)
   calendario.set(Calendar.MONTH, mes)
   calendario.set(Calendar.DAY_OF_MONTH, dia)
   lugar.fecha = calendario.timeInMillis
   actualizaPosLugar(pos, lugar)
   val textView = actividad.findViewById<TextView>(R.id.fecha)
   textView.text = java.text.DateFormat.getDateInstance().format(Date(lugar.fecha))
}
 
Clase DialogoSelectorFecha:
 
public class DialogoSelectorFecha extends DialogFragment {

   private OnDateSetListener escuchador;

   public void setOnDateSetListener(OnDateSetListener escuchador) {
      this.escuchador = escuchador;
   }
   @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
      Calendar calendario = Calendar.getInstance();
      Bundle args = this.getArguments();
      if (args != null) {
         long fecha = args.getLong("fecha");
         calendario.setTimeInMillis(fecha);
      }
      int año = calendario.get(Calendar.YEAR);
      int mes = calendario.get(Calendar.MONTH);
      int dia = calendario.get(Calendar.DAY_OF_MONTH);
      return new DatePickerDialog(getActivity(),escuchador,año,mes,dia);
   }
} 
class DialogoSelectorFecha : DialogFragment() {

   private var escuchador: OnDateSetListener? = null

   fun setOnDateSetListener(escuchador: OnDateSetListener) {
      this.escuchador = escuchador
   }

   override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
      val calendario = Calendar.getInstance()
      val fecha = getArguments()?.getLong("fecha")?:0L
      calendario.setTimeInMillis(fecha)
      val año = calendario.get(Calendar.YEAR)
      val mes = calendario.get(Calendar.MONTH)
      val dia = calendario.get(Calendar.DAY_OF_MONTH)
      return DatePickerDialog(getActivity(), escuchador, año, mes, dia)
   }
} 


Práctica: Separando casos de uso en varias clases

Acabamos de crear la clase CasosUsoLugarFecha que es una ampliación de uso de CasosUsoLugar. Se trata de una estrategia adecuada en muchas ocasiones, pero también existe otra alternativa que nos permitiría dividir los casos de uso en diferentes clases. En esta práctica se describe cómo hacerlo. Crea una clase abstracta con nombre CasosUsoLugarBase. Que solo contenga la función actualizaPosLugar(). Crea tres clases que extiendan de esta con nombres: CasosUsoLugarOperacion, CasosUsoLugarFoto, CasosUsoLugarFecha. Distribuye las funciones entre las tres clases. Desde las actividades o fragmentes que utilicen estos casos de uso tendrás que crear variables para cada una de las clases necesarias. Al hacer este cambio estamos complicando el código, necesitamos más clase y variables. Pero tenemos algunas ventajas: La responsabilidad de cada clase es más pequeña, lo que las hace más fáciles de entender y mantener. El código es más reutilizable. Por ejemplo, cuando necesites añadir fotografías en una nueva aplicación, será fácil localizar el código necesario y adaptarlo con el mínimo número de cambios.