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 VistaLugarActivity y añade en el método onCreate() el siguiente código. :

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

NOTA: Selecciona el paquete android.view.OnClickListener.
 

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 { 

   protected AppCompatActivity actividad;
   protected LugaresBDAdapter lugares;

   public CasosUsoLugarFecha(AppCompatActivity actividad, 
                             LugaresBDAdapter lugares){
      this.actividad = actividad;
      this.lugares = lugares;
   }
} 
class CasosUsoLugarFecha(val actividad: AppCompatActivity,
                         val lugares: LugaresBDAdapter) { 
} 

4.   Añade en la nueva clase:

private int pos =-1;
private Lugar lugar;

public void cambiarHora(int pos) {
   lugar = lugares.elementoPos(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 = lugares.elementoPos(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 la 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 escribiendo dos variables donde recordaremos la información que estamos modificando, 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.
 

5.    Crea la siguiente clase en el paquete presentacion:

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())
      )
   }
} 

NOTA: Pulsa Alt-Intro para añadir los imports automáticamente. Algunas clases se encuentran en varios paquetes, por lo que te preguntará. Utiliza: androidx.fragment.app.DialogFragment y android.text.format.DateFormat

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.
 

6.    Haz que CasosUsoLugarFecha implemente la siguiente interfaz:

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


7.    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());
   lugares.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()
   lugares.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.   

8.    En VistaLugarActivity crea la variable usoLugarFecha de la clase CasosUsoLugarFecha de igual forma como se ha creado usoLugar.

9.    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:
 

Añade en VistaLugarActivity dentro de onCreate():

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

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

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

public void cambiarFecha(int pos) {
   lugar = lugares.elementoPos(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());
   lugares.actualizaPosLugar(pos, lugar); 
   TextView textView = actividad.findViewById(R.id.fecha);    
   textView.setText(DateFormat.getDateInstance().format(
                                             new Date(lugar.getFecha())));
} 
class CasosUsoLugarFecha(…) : 
   TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener{

fun cambiarFecha(pos: Int) {
   lugar = lugares.elementoPos(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
   lugares.actualizaPosLugar(pos, lugar)
   val textView = actividad.findViewById<TextView>(R.id.fecha)
   textView.text = java.text.DateFormat.getDateInstance().format(Date(lugar.fecha))
} 
Crea la 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)
   }
}