Tutoriales Android

Introducción y objetivos

La forma más habitual para interactuar con un ordenador es el teclado y el ratón. Por desgracia, estos dispositivos de entrada no existen, o están muy limitados, en un teléfono móvil. Afortunadamente, los nuevos móviles permiten nuevas formas de interacción con el usuario, por lo que el  diseño de nuestras aplicaciones ha de adaptarse a estas nuevas formas de interacción. A lo largo de este capítulo se estudiarán diferentes alternativas para recoger las acciones que los usuarios realizan sobre la aplicación.

Tras una visión general del manejo de eventos en Android, comenzaremos con el dispositivo más tradicional, el teclado, luego estudiaremos los eventos de la pantalla táctil, y finalizaremos con los sensores. Estos tres mecanismos de interacción serán aplicados al manejo de nuestra nave en la aplicación Asteroides. De forma adicional se tocarán otros aspectos importantes, como el manejo de hilos de ejecución (threads) y las gestures.     

Objetivos:

  • Describir el uso de hilos de ejecución (Thread).
  • Mostrar las distintas alternativas para manejar los eventos de usuario en Android.
  • Describir como se manejan los eventos del teclado.
  • Aprender a interaccionar con la pantalla táctil.
  • Descubrir que son las Gestures y como pueden ayudarte en el diseño del interfaz de usuario.
  • Enumerar los sensores disponibles en muchos terminales Android y aprender a utilizarlos.
  • Seguir mejorando la aplicación Asteroides.

{jcomments on}

Uso de hilos de ejecución (Threads)

Introducción a los procesos e hilos de ejecución

Cada vez que se lanza una nueva aplicación en Android el sistema crea un nuevo proceso Linux para ella y la ejecuta en su propia máquina virtual Dalvik (Por supuesto si está programada en Java, si lo estuviera en código nativo no haría falta la máquina virtual). Trabajar en procesos diferentes nos garantiza que desde una aplicación no se pueda acceder a la memoria (código o variables) de otras aplicaciones. Como se verá en un próximo capítulo esta característica se hereda directamente del sistema operativo Linux.

Los S.O. modernos incorporan el concepto de hilo de ejecución (thread). En un sistema multihilo un proceso va a poder realizar varias tareas a la vez, cada una en un hilo diferente. Los diferentes hilos de un proceso lo comparten todo: variables, código, permisos, ficheros abiertos, etc.

Cuando trabajamos con varios hilos, estos pueden acceder a las variables de forma simultánea. Hay que tener cuidado de que un hilo no modifique el valor de una variable mientras otro hilo está leyéndola. Este problema se resuelve en Java definiendo secciones críticas mediante la palabra reservada synchronized. Trataremos este problema más adelante.

Hilos de ejecución en Android

Cuando se lanza una nueva aplicación el sistema crea un nuevo hilo de ejecución (thread) para esta aplicación conocida como hilo principal. Este hilo es muy importante dado que se encarga de atender los eventos de los distintos componentes. Es decir, este hilo ejecuta los métodos onCreate(), onDraw(), onKeyDown(), … Por esta razón al hilo principal también se le conoce como hilo del interfaz de usuario.

El sistema no crea un hilo independiente cada vez que se crea un nuevo componente. Es decir, todas las actividades y servicios de una aplicación son ejecutados por el hilo principal.

Cuando tu aplicación ha de realizar trabajo intensivo como respuesta a una interacción de usuario, hay que tener cuidado porque es posible que la aplicación no responda de forma adecuada. Por ejemplo, imagina que has de esperar para descargar unos datos de Internet, si lo haces en el hilo de ejecución principal este quedará bloqueado a la espera de que termine la descarga. Por lo tanto, no se podrá redibujar la vista (onDraw()) o atender eventos del usuario (onKeyDown()). Desde el punto de vista del usuario se tendrá la impresión de que la aplicación se ha colgado. Mas todavía, si el hilo principal es bloqueado más de 5 segundos, el sistema mostrará un cuadro de dialogo al usuario “La aplicación no responde” para que el usuario decida si quieres esperar o detener la aplicación.

La solución en estos casos es crear un nuevo hilo de ejecución, para que realice este trabajo intensivo. De esta forma no bloqueamos el hilo principal, que puede seguir atendiendo los eventos de usuario. Es decir, cuando estés implementando un método del hilo principal (Empiezan por on…) nunca realices una acción que pueda bloquear este hilo, como cálculos largos o que requieran esperar mucho tiempo. En estos casos hay que crear un nuevo hilo de ejecución y encomendarle esta tarea. En el siguiente apartado describimos cómo hacerlo.

Las herramientas del interfaz de usuario de Android han sido diseñadas para ser ejecutadas desde un único hilo de ejecución, el hilo principal. Por lo tanto no se permite manipular el interfaz de usuario desde otros hilos de ejecución.  

  poli[Media] Hilos de ejecución en Android

Ejercicio paso a paso: Una tarea que bloquea el hilo principal.

En muchas ocasiones hemos de realizar costosas operaciones o hemos de esperar a que concluyan lentas operaciones en la red. En ambos casos, hay que tener la precaución de no bloquear el hilo principal. De hacerlo el resultado puede ser catastrófico, como se muestra en el siguiente ejercicio.

1. Crea un nuevo proyecto y llámalo Hilos:

2.  Reemplaza el código del layout activity_main por:

<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical" >
   <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content" >

      <EditText

         android:id="@+id/entrada"
         android:layout_width="0dip"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:inputType="numberDecimal"
         android:text="5" >

         <requestFocus />
      </EditText>
      <Button
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:onClick="calcularOperacion"
         android:text="Calcular factorial" />

   </LinearLayout>

   <TextView

      android:id="@+id/salida"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:text=" " 
      android:textAppearance="?android:attr/textAppearanceMedium"/>

</LinearLayout>

Reemplaza el código de MainActivity por el siguiente:

public class MainActivity extends Activity {
       private EditText entrada;
       private TextView salida;


       @Override
       public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
             entrada = (EditText) findViewById(R.id.entrada);
             salida = (TextView) findViewById(R.id.salida);
       }

       public void calcularOperacion(View view) {
             int n = Integer.parseInt(entrada.getText().toString());
             salida.append(n +"! = ");
             int res = factorial(n);
             salida.append(res +"\n");
       }

       public int factorial(int n) {
             int res=1;
             for (int i=1; i<=n; i++){
                    res*=1;
                    SystemClock.sleep(1000);
             }

             return res;

       }

}

 

El método calcularOperacion() será llamado cuando se pulse el botón. Comienza obteniendo el valor entero introducido en entrada y muestra la operación a realizar por el TextView salida. Luego, se llama al factorial() y se muestra el resultado.

El método factorial() calcula la operación mátemática factorial de un entero n (n!). Se calcula multiplicando todos los enteros desde uno hasta n. Por ejemplo:

factorial(5) = 5! = 1 · 2 · 3 · 4 · 5 = 120

      Esta operación se calcula con un bucle con la variable i tomando valores de 1 hasta n. Obviamente, esta operación se va computar de forma muy rápida y apenas bloquearemos el hilo principal una milésimas de segundo. Para que aparezca el problema a tratar, vamos a simular que se realizan un gran número de operaciones en cada pasada del bucle. Para esto llamaremos a  SystemClock.sleep(1000) que bloqueará el hilo durante 1000 ms (1 segundos).

4. Ejecuta la aplicación. El resultado ha de ser similar al siguiente:

Observa como mientras se realiza la operación el usuario no puede pulsar el botón ni modificar el EditText. El usuario tendrá la sensación de que la aplicación está bloqueada.

5 Calcula ahora el factorial de 10. Si mientras se calcula tratas de interaccionar con el interfaz de usuasio el sistema nos mostrará en siguiente error:

Mensajes del tipo “La aplicación no responde” son frecuentes en Android. Aparecen cuando el hilo principal se bloquea demasiado tiempo. En el siguiente apartado mostraremos como realizar esta operación de forma correcta.

 

 

Creación de nuevos hilos con la clase Thread

Como acabamos de ver siempre que tengamos que ejecutar un método que requiera bastante tiempo de ejecución, no podremos ejecutarlo en el hilo principal. Dado que este hilo ha de estar siempre disponible para atender los eventos generados por el usuario, nunca debe ser bloqueado. En este apartado aprenderemos a crear nuevos hilos usando la clase de Java Thread. El proceso es muy sencillo, no tendremos más que escribir una clase como la siguiente:

class MiThread extends Thread {
   @Override
   public void run() {
     …
   }
}

El método run() contiene el código que queremos que el hilo ejecute. Para crear un nuevo hilo y ejecutarlo escribiremos:

 MiThread hilo = new MiThread();
hilo.start();

La llamada al método start() ocasionará que se cree un nuevo hilo y se ejecute el método run() en este hilo. La llamada al método start() es asíncrona. Es decir, continuaremos ejecutando las instrucciones siguientes, sin esperar a que el métodorun() concluya. Como veremos más adelante, si espereamos algún resultado de este método, será imprescidible establecer algún mecanismo de sincronización para saber cuando ha terminado.

 

Ejercicio paso a paso: Crear un nuevo hilo con la clase Thread

En este ejercicio ejecutaremos el método factorial() en un hilo nuevo. Además, veremos algunas limitaciones de usar nuevos hilos, como la imposibilidad de acceder al interfaz de usuario. 

  1. Abre el proyecto creado en el ejercicio anterior.
  2. Dentro de MainActivity introduce el siguiente código:
class MiThread extends Thread {
             private int n, res;


             public MiThread(int n) {
                    this.n = n;
             }
       }

Siguiendo el esquema mostrado anteriormente, hemos creado un hilo que en su método run(), llama a factorial() y muestra el resultado por pantalla. Para realizar la operación necesitamos el parámetro de entrada n. Este no puede incorporarse el el método run() dado que ha de ser sobreescrito (@Override) sin alteración alguna. Para resolverlo hemos añadido un constructor a la clase, donde se indica este parámetro.

3. Reemplaza el siguiente método en MainActivity:

public void calcularOperacion(View view) {

             int n = Integer.parseInt(entrada.getText().toString());

             salida.append(n + "! = ");

             MiThread thread = new MiThread(n);

             thread.start();

       }

4. Ejecuta la aplicación. Tras pulsar el botón el resultado ha de ser:

5. Abre la vista LogCat y busca el siguiente error:

Este mensaje indica que solo desde el hilo principal se puede interactuar con las vistas de la interfaz de usuario. NOTA: También está prohibido desde otros hilos usar la clase Toast

Una forma elegante de resolver este problema podría ser usar la clase Callable. Esta  clase de Java nos permite llamar un método en un nuevo hilo, esperar a que este termine y recoger los resultados. No obstante, nuestro objetivo es mostrar  las peculiaridades de Android con el manejo de hilos y vamos a resolver el problema de otra forma.

6. En el método run() reemplaza:

salida.append(res + "\n");

por:

             runOnUiThread(new Runnable() {
                 @Override public void run() {
                           salida.append(res + "\n");
                }
             });

 

 

De esta forma indicamos al sistema que ejecute parte de nuestro código en el hilo principal o hilo del interfaz de usuario.

7. Ejecuta la aplicación y comprueba que el resultado es satisfactorio.

 

 

Introduciendo movimiento en Asteroides

Para que el juego cobre vida será necesario animar todos los gráficos introducidos. En el siguiente ejercicio veremos cómo hacerlo.

Ejercicio paso a paso: Introduciendo movimiento en Asteroides

1.     Comienza declarando las siguientes variables en la clase VistaJuego:

 

// //// THREAD Y TIEMPO //////
// Thread encargado de procesar el juego
private ThreadJuego thread = new ThreadJuego();
// Cada cuanto queremos procesar cambios (ms)
private static int PERIODO_PROCESO = 50;
// Cuando se realizó el último proceso
private long ultimoProceso = 0;

2.     La animación del juego la llevará a cabo el método actualizaFisica() que será ejecutado a intervalos regulares definidos por la constante PERIODO_PROCESO. Esta constante ha sido inicializada a 50 ms. En ultimoProceso se almacena el instante en que se llamó por última vez a actualizaFisica().

3.     Copia el siguiente método dentro de la clase VistaJuego:

protected ahora="System.currentTimeMillis();" cumplido.="" de="" el="" ha="" hagas="" if="" long="" nada="" no="" odo="" periodo_proceso="" proceso="" se="" si="" ultimoproceso="" void=""> ahora) {
             return;
       }
       // Para una ejecución en tiempo real calculamos retardo           
       double retardo = (ahora - ultimoProceso) / PERIODO_PROCESO;
       ultimoProceso = ahora; // Para la próxima vez
       // Actualizamos velocidad y dirección de la nave a partir de 
       // giroNave y aceleracionNave (según la entrada del jugador)
       nave.setAngulo((int) (nave.getAngulo() + giroNave * retardo));
       double nIncX = nave.getIncX() + aceleracionNave *
                            Math.cos(Math.toRadians(nave.getAngulo())) * retardo;
       double nIncY = nave.getIncY() + aceleracionNave * 
                           Math.sin(Math.toRadians(nave.getAngulo())) * retardo;
       // Actualizamos si el módulo de la velocidad no excede el máximo
       if (Math.hypot(nIncX,nIncY) <= MAX_VELOCIDAD_NAVE){
             nave.setIncX(nIncX);
             nave.setIncY(nIncY);
       }
       // Actualizamos posiciones X e Y
       nave.incrementaPos(retardo);
       for (Grafico asteroide : Asteroides) {
             asteroide.incrementaPos(retardo);
       }
}

 

Como veremos a continuación este método será llamado de forma continua. Como queremos desplazar los gráficos cada PERIODO_PROCESO milisegundos, verificamos si ya ha pasado este tiempo desde la última vez que se ejecutó (ultimoProceso).

Como también es posible que el sistema esté ocupado y no nos haya podido llamar hasta un tiempo superior a PERIODO_PROCESO, vamos a calcular el factor de retardo en función del tiempo adicional que haya pasado. Si por ejemplo, desde la última llamada ha pasado dos veces PERIODO_PROCESO, la variableretardo ha de valer 2. Lo que significará que los gráficos han de desplazarse el doble que en circunstancias normales. De esta forma conseguiremos un desplazamiento continuo en tiempo real.

A continuación, se actualizan las variables que controlan la aceleración y cambios de dirección de la nave. Se consiguen por medio de las variables aceleracionNave y giroNave. En el siguiente capítulo modificaremos estas variables para que el jugador pueda pilotar la nave. A partir de estas variables se obtiene una nueva velocidad de la nave, descompuesta en sus componentes x e y. Si el módulo de estos componentes es mayor que la velocidad máxima permitida, no se actualizará la velocidad.

Finalmente se actualizan las posiciones de todos los gráficos (nave y asteroides) a partir de sus velocidades. Esto se consigue llamando al método incrementaPos() definido en la clase Grafico.

4.     Ahora necesitamos que esta función sea llamada continuamente, para lo que utilizaremos un Thread. Crea la siguiente clase dentro de la clase VistaJuego:

class ThreadJuego extends Thread {
   @Override
   public void run() {
          while (true) {
                 actualizaFisica();
          }
   }
}

5.     Introduce estas líneas al final del método onSizeChanged():

ultimoProceso = System.currentTimeMillis();
thread.start();

Esto ocasionará que se llame al método run() del hilo de ejecución. Este método es un bucle infinito que continuamente llama al actualizaFisica().

6.     Ejecuta la aplicación y observa como el juego cobra vida.

 

El trabajo con hilos de ejecución es especialmente delicado. Como veremos en próximos capítulos, este código nos va a ocasionar varios quebraderos de cabeza. Un problema es que seguirá ejecutándose aunque nuestra aplicación esté en segundo plano. Veremos cómo detener el hilo de ejecución, cuando estudiemos el ciclo de vida de una actividad. (NOTA: Si ejecutas el programa en el terminal real y detectas que este funciona más lentamente, puede ser buena idea detener la aplicación). Un segundo problema, aparecerá cuando dos hilos de ejecución traten de acceder a la misma variable a la vez. También se resolverá más adelante.

 

Ejercicio paso a paso: Introduciendo secciones críticas en Java (synchronized)

 

Cuando se realiza una aplicación que ejecuta varios hilos de ejecución hay que prestar un especial cuidado a que ambos hilos pueden acceder de forma simultánea a los datos. Cuando se limitan a leer las variables, no suele haber problemas. El problema a parece cuando un hilo esté modificando algún dato y justo en este instante se pasa a ejecutar un segundo hilo que ha de leer estos datos. Este segundo hilo va a encontrar unos datos a mitad de modificar, lo que posiblemente cause errores en su interpretación. El método más común para evitar que dos hilos accedan al mismo tiempo a un recurso es el de la exclusión mutua. En Java que se consigue utilizando la palabra reservada synchronized.

 

1.   Introduce la palabra reservada synchronized delante del método onDraw() y actualizaFisica(). De esta forma se evita que cuando actualizaFisica() esté modificando alguno de los valores de la nave o los asteroides en método onDraw() acceda a estos valores.

Nota sobre Java: La palabra clave synchronized permite definir una sección crítica en Java. Expliquemos en qué consiste: Cada vez que un hilo de ejecución (thread) entra en un método o bloque de instrucciones marcado con synchronized se pregunta al objeto si ya hay algún otro thread que haya entrado en la sección crítica de ese objeto. La sección crítica está formada por todos los bloques de instrucciones marcados con synchronized. Si nadie ha entrado en la sección crítica, se entrará normalmente. Si ya hay otro thread dentro, entonces el thread actual es suspendido y ha de esperar hasta que la sección crítica quede libere. Esto ocurrirá cuando el thread que está dentro de la sección crítica salga.

Dos matizaciones importantes: La primera es que la sección crítica se define a nivel de objeto no de clase. Es decir, cada objeto instanciado no influyen en las secciones críticas de otros objetos. En segundo lugar, solo se define una sección crítica por clase. Aunque se haya utilizado synchronized en varios métodos, realmente solo hay una sección crítica.

Ejecutar una tarea en un nuevo hilo con AsyncTask

En Android es muy frecuente lanzar nuevos hilos. Tendremos que hacerlo siempre que exista la posibilidad de que una tarea pueda bloquear el hilo del interfaz de usuario. Esto suela ocurrir en cálculos complejos o en accesos a la red.

Tras ver el uso de las herramientas estándares en Java para crear hilos; en este apartado veremos una clase creada en Android que nos ayudará a resolver este tipo de problemas de forma más sencilla, la clase AsyncTask.

Una tarea asincrónica (asinc tasck) se define por un cálculo que se ejecuta en un hilo secundario y cuyo resultado queremos que se publique en el hilo del interfaz de usuario. Para crear una nueva tarea asíncrona puedes basarte en el siguiente esquema:

class MiTarea extends AsyncTask {

      

       @Override protected void onPreExecute() {

              …     

       }

       @Override protected Resultado doInBackground(Parametros... par) {

             …     

       }

  @Override protected void onProgressUpdate(Progreso... prog) {

       …     

 }

  @Override protected void onPostExecute(Resultado result) {

          …    

}

}


donde Parametros, Progreso y Resultado han de ser reemplazados por nombres de clases segun los tipos de datos con los que trabaje la tarea.

Los cuatro métodos que podemos sobreescribir corresponden a los cuatro pasos que seguirá AsyncTask para ejecutar la tarea:

  • onPreExecute(): En este método tenemos que realizar los trabajos previos a la ejcución de la tarea. Se utiliza normalmente para configurar la tarea y para mostrar en el la interfaz de usuario que empieza la tarea.
  • doInBackground(Parametros...): Es llamado cuando termina onPreExecute(). Es la parte más importante donde tenemos que realizar la tarea propiamente dicha. Es el único método de los cuatro que no se ejecuta en el hilo del interfaz de usuario. Lo va a hacer en un hilo nuevo creado para este propósito. Como hemos visto la clase AsyncTask ha de ser paramétrizada con tres tipos de datos. Es decir, cuando crees un AsyncTask la clase Parametros ha de ser reemplazada por una clase concreta que será utilizada para indicar la información de entrada a la tarea. Observa como en el parámetro de este método se han añadido tres puntos detrás de Parametros. Esto significa que se puede pasar al método un número variable de parámetros de esta clase[1].
  • onProgressUpdate(Progress...): Este método se utiliza para mostrar el progreso de la tarea al usuario. Se ejecuta en el hilo interfaz de usuario, por lo que podremos interactuar con las vistas. El progreso de una determinada tarea ha de ser controlado por el programador llamando al método publishProgress(Progress...) desde doInBackground(). La clase Progress es utilizada para pasar la información de progreso. Un uso frecuente es reemplazarla por Integer y representar el porcentaje de progreso en un valor entre el 0 y el 100.
  • onPostExecute(Result): Este método se usa para mostrar en el interfaz de usuario el resultado de la tarea. El parámetro de entrada (de la clase Result) corresponde con el objeto devuelto por el método doInBackground()

Una vez definida la clase descenciente de AsyncTask podremos arrancar una tarea de la siguiente forma:

MiTarea tarea = newMiTarea();

 tarea.execute(p1, p2, p3);

Donde p1, p2, p3 ha de ser una lista de objetos de la clase Parametros, pudiendo introducirse un número variable de parámetros. Ha y que resaltar que execute() es un método asíncrono. Esto significa que, tras llamardo, se pondrá en marcha la tarea en otro hilo, pero en paralelo se continuarán ejecutando las instrucciones que hayas escrito a continuación de execute. En nombre se AsyncTask se ha puesto precisamente por este comportamiento.

En el siguiente diagrama se muestra el orden de ejecución de estos métodos:

 

Ejercicio paso a paso: Crear un nuevo hilo con AsyncTask

En este ejercicio resolveremos la operación del ejercicio anterior pero ahora usando AsyncTask en lugar de Thread

  1. Abre el proyecto Hilos creado en el ejercicio anterior.
  2. Dentro de MainActivity introduce el siguiente código:
 class MiTarea extends AsyncTask {

             @Override

             protected Integer doInBackground(Integer... n) {

                    return factorial(n[0]);

             }

             @Override

             protected void onPostExecute(Integer res) {

                    salida.append(res + "\n");

             }

       }

Cuando usamos esta clase hemos de comenzar decidiendo los tres tipos de datos que utilizaremos usando el siguiente esquema AsyncTask<Parametros, Progreso, Resultado>. En nuestra tarea necesitamos un entero como entrada, no usaremos información de progreso y devolveremos un entero. Estos tres tipos de datos solo pueden ser clases. Si queremos utilizar un tipo simple, como int, tendremos que usar una clase envolvente, como Integer[1].

En el método doInBackground() se ha indicado como parámetro Integer..., de manera que se le podrán pasar una lista variable de enteros. Aunque, en nuestro caso solo nos interesa el primero (n[0]), estamos obligados a sobreescribir el método exactamente como se espera, y no podemos quitar los .... En este método nos limitamos a calcular el factorial y devolverlo. Al terminar este método será llamado onPostExecute(), pasándole como parámetro el valor devuelto. Para terminar la explicación, recuerda que el método doInBackground() se ejecutará en un nuevo hilo, mientras que onPostExecute() se ejecutará en el hilo del interfaz de usuario.

  1. Comenta las dos últimas líneas del método y añade las siguientes:

            

MiTarea tarea = new MiTarea();

             tarea.execute(n);

La llamada a execute() provocará que los diferentes métodos definidos en MiTarea sean llamados en el orden adecuado.

  1. Comprueba que funciona correctamente.


[1] Ver Anexo A: Referencia Java - Envolventes (wrappers)

[2] Ver Anexo A: Referencia Java - métodos con argumentos variables en número

Mostrar un cuadro de progreso en un AsyncTask

Si estamos realizando una tarea que puede prolongarse en el tiempo, resulta muy importante mostrar al usuario cuando empieza, su progreso y cuando termina. En el siguiente ejercicio vamos a ver un ejemplo algo más complejo de AsyncTask, donde usaremos la clase ProgressDialog para mostrar la evolución de la tarea.

Ejercicio paso a paso: Uso de un cuadro de progreso en un AsyncTask

 

  1. Siguiendo con el ejercicio anterior, reemplaza la clase MiTarea por el código:


       class MiTarea extends AsyncTask<Integer, Integer, Integer> {

             private ProgressDialog progreso;

                   @Override protected void onPreExecute() {

                    progreso = new ProgressDialog(MainActivity.this);

                    progreso.setProgressStyle(ProgressDialog.
                                                                                              STYLE_HORIZONTAL);

                    progreso.setMessage("Calculando...");

                    progreso.setCancelable(false);

                    progreso.setMax(100);

                    progreso.setProgress(0);

                    progreso.show();

             }

             @Override protected Integer doInBackground(Integer... n) {

                    int res = 1;

                    for (int i = 1; i <= n[0]; i++) {

                           res *= i;

                           SystemClock.sleep(1000);

                           publishProgress(i*100 / n[0]);

                    }

                    return res;

             }

             @Override protected void onProgressUpdate(Integer... porc) {

                    progreso.setProgress(porc[0]);

             }

             @Override protected void onPostExecute(Integer res) {

                    progreso.dismiss();

                    salida.append(res + "\n");

             }

       }
 

En esta nueva versión se han incluido los cuatro métodos principales de AsynTask. El primero en ejecutarse será onPreExecute(), donde creamos un ProgressDialog lo configuramos y lo mostramos. Cuando acabe, se ejecuará doInBackground(). En esta versión no podemos llamar simplemente a factorial(), dado que ahora queremos insertar en el bucle la sentencia publishProgress(i*100/n[0]). Lo que ocasionará una llamada a  onProgressUpdate(), desde donde tendremos acceso al interfaz de usuario y podremos actualizar el ProgressDialog. Finalmente cuando doInBackground() termine se llamará a onPostExecute(), donde destruiremos el ProgressDialog y mostraremos el resultado.

  1. Verifica el resultado obtenido

Ejercicio paso a paso: Cancelando un AsyncTask

Dado que AsyncTask se utiliza en tareas prolongadas, es posible que el usuario no quiera esperar a que termine o que descubramos en medio del proceso que no podemos terminar la tarea. Podemos utilizar el método cancel() cuando ocurra esta circunstancia. Si cancelamos una tarea el método onPostExecute(Resultado) no será llamado, y en su lugar se llamará a onCanceled(). El siguiente ejercicio ilustra su uso:

  1. En el método onPreExecute() del AsyncTask cambia el parámetro de progreso.setCancelable(false) a true.
  2. Añade a continuación de la línea que acbas de modificar:


       progreso.setOnCancelListener(new OnCancelListener() {

           @Override public void onCancel(DialogInterface dialog) {

               MiTarea.this.cancel(true);

           }

       });
 

Con esto conseguimos poner un escuchador de evento al ProgressDialog, para que cuando sea cancelado también cancele el AsyncTask.

  1. Dentro de doInBackground() añade la siguiente condición de finalización en el bucle for:


       for (int i = 1; i <= n[0] && !isCancelled(); i++) {
 

De esta forma no seguiremos realizando la tarea si nos cancelan.

  1. Para terminar añade el siguiente método en la clase AsyncTask:


       @Override protected void onCancelled() {

             salida.append("cancelado\n");

       }
 

  1. Verifica el resultado.

 

El metodo get() de AsyncTask

En caso de que necesites el resultado de esta tarea para poder continuar ejecutando tu programa puedes utilizar el siguiente método:

Resultado r = tarea.get();

Lo que hace es esperar a que termine la tarea y devuelve el resultado obtenido. Aunque parezca muy útil, este método no es recomendable usarlo dado que se bloquea hasta que termine la tarea, y esto es justo lo que queríamos evitar al crear el AsyncTasck. Veamos un ejemplo con el siguiente esquema:

Si el método onClickButton() es asociado a la pulsación de un botón y este se pulsa, has de tener claro que el hilo principal quedará bloqueado hasta que termine la tarea.

El método get() dispone de una sobrecarga alternativa que resulta muy práctica. En ella indicamos dos parámetros, donde fijamos un tiempo máximo de la tarea y en que unidades está este tiempo. Por ejemplo, si escribimos get(4, TimeUnit.SECONDS), pasado 4 segundos se detendrá la tarea y se lanzará una excepción. Usar este método puede resultar interesante cuando no tenemos más remedio que bloquear el hilo del interfaz del usuario hasta que termine una tarea.

Vemos un ejemplo de uso. Si hemos creado un AsyncTask que valida un usuario en nuestro servidor podríamos usar el siguiente método:

 

publicbooleanonLogin(String usuario, contrasena){

       try{

             TareaLogin tarea = newTareaLogin();

             tarea.execute(usuario, contrasena);

             returntarea.get(4, TimeUnit.SECONDS);

       } catch(TimeoutException e) {

             Toast.makeText(contexto, "Tiempo excedido al validar",

                           Toast.LENGTH_LONG).show();

       } catch(CancellationException e) {

             Toast.makeText(contexto, "Error al conectar con servidor",

                           Toast.LENGTH_LONG).show();

       } catch(Exception e) {

             Toast.makeText(contexto, "Error con tarea asíncrona",

                           Toast.LENGTH_LONG).show();

       }

   returnfalse;

 

}

Cuando este método sea invocado has de tener claro que el hilo del interfaz de usuario se va a bloquear hasta que termine la tarea. Sin embargo, en este caso particular no queremos que el usuario realice ninguna acción hasta ser validado. Por lo que no se apreciará falta de interactividad. Además, estamos limitando este bloqueo a un máximo de 4 segundos, impidiendo que aparezca el error “La aplicación no responde”. En caso de producirse cualquier problema será tratado en la sección catch, donde mostraremos al usuario el error que se ha producido. TimeoutException ocurrirá si la tarea tarda más de 4 segundos. CancellationException ocurrirá si el método cancel() es invocado dentro de la tarea, supuestamente si el servidor no responde o si la contraseña no es correcta. Pueden producirse un par de tipos de excepciones más relacionadas con hilos de ejecución. Ambas son capturadas mediante la excepción genérica Exception.  

Al final del capítulo10 se hace una discusión más profunda de este método en el ejercicio “Uso síncrono de AsyncTask para acceso al servicio Web PHP de puntuaciones”.

{jcomments on}

  Manejando eventos de usuario

Android captura los distintos eventos de usuario de forma homogénea y se los pasa a la clase encargada de recogerlos. Por lo general va a ser un objeto tipo View el que recogerá estos eventos por medio de dos técnicas alternativas. Los escuchadores de eventos (Event Listener) y los manejadores de eventos (Event Handler).

Escuchador de eventos

Un Escuchador de eventos o  Event Listener es una interfaz de la clase View que contiene un métodocallback que ha de ser registrado. Cada Escuchador de Eventos tiene solo un método callback, que será llamado por Android cuando se produzca la acción correspondiente. Tenemos los siguientes escuchadores de eventos:

onClick()

Método de la interfaz View.OnClickListener. Se llama cuando el usuario selecciona un elemento. Se puede utilizar cualquier medio como la pantalla táctil, las teclas de navegación o eltrackball.

onLongClick()

Método de la interfaz View.OnLongClickListener. Se llama cuando el usuario selecciona un elemento durante más de un segundo.

onFocusChange()

Método de la interfaz View.OnFocusChangeListener. Se llama cuando el usuario navega dentro o fuera de un elemento.

onKey()

Método de la interfaz View.OnKeyListener. Se llama cuando se pulsa o se suelta una tecla del dispositivo.

onTouch()

Método de la interfaz View.OnTouchListener. Se llama cuando se pulsa o se suelta o se desplaza en la pantalla táctil.

onCreateContextMenu()

Método de la interfaz View.OnCreateContextMenuListener. Se llama cuando se crea un menú de contexto.

Existen dos alternativas para crear un escuchador de evento. La primera es crear un objeto anónimo por ejemplo de la clase OnClickListener():

 

protected void onCreate(Bundle savedValues) {

      ...

      Button boton = (Button)findViewById(R.id.boton);

      boton.setOnClickListener( new OnClickListener() {

         public void onClick(View v) {

            // Acciones a realizar

         }

          });

      ...

   }

 

La segunda alternativa consiste en implementar la interfaz OnClickListener como parte de tu clase y recoger los eventos en el método onClick(). Esta alternativa es la recomendada por Android, al tener menos gasto de memoria. A continuación se muestra un ejemplo:

 

public class Ejemplo extends Activity implements OnClickListener{

           protected void onCreate(Bundle savedValues) {

               ...

              Button boton = (Button)findViewById(R.id.boton);

              boton.setOnClickListener(this);

           }

 

           public void onClick(View v) {

            // Acciones a realizar

           }

           ...

       }

 

Manejador de eventos

Si estás creando un descendiente de la clase View, podrás utilizar varios métodos (callback) directamente usados como manejadores de eventos por defecto (Event Handlers). En esta lista se incluye:

 

 

onKeyDown(int keyCode, KeyEvent e) Llamado cuando una tecla es pulsada.

onKeyUp(int keyCode, KeyEvent e)             Cuando una tecla deja de ser pulsada.

onTrackballEvent(MotionEvent me)             Llamado cuando se mueve el trackball.

onTouchEvent(MotionEvent me)                    Cuando se pulsa en la pantalla táctil.

onFocusChanged(boolean obtengoFoco, int direccion, Rect           prevRectanguloFoco)                   Llamado cuando cambia el foco.

 

Es la forma más sencilla, dado que no hace falta usar una interfaz, ni registrar el método callback. Como en nuestro ejemplo estamos creando JuegoView, que es un descendiente de View, podremos utilizar directamente manejadores de evento.

Puedes ver algunos aspectos relacionados en formato poli[Media]

Escuchador y manejador de eventos

 

Aunque cada vez existen menos terminales Android con teclado físico, siempre es interesante aprender a gestionar los eventos procedentes del teclado. Su manejo se ilustra en el siguiente ejercicio.

 

Ejercicio paso a paso: Manejo de la nave con el teclado

Veamos cómo podemos utilizar un manejador de eventos de teclado para maniobrar la nave de Asteroides:

1. Abre el proyecto Asteroides.

2. Inserta este código en la clase VistaJuego.

@Override

       public boolean onKeyDown(int codigoTecla, KeyEvent evento) {

             super.onKeyDown(codigoTecla, evento);

              // Suponemos que vamos a procesar la pulsación

             boolean procesada = true;

             switch (codigoTecla) {

             case KeyEvent.KEYCODE_DPAD_UP:

                    aceleracionNave = +PASO_ACELERACION_NAVE;

                    break;

             case KeyEvent.KEYCODE_DPAD_LEFT:

                    giroNave = -PASO_GIRO_NAVE;

                    break;

             case KeyEvent.KEYCODE_DPAD_RIGHT:

                    giroNave = +PASO_GIRO_NAVE;

                    break;

             case KeyEvent.KEYCODE_DPAD_CENTER:

             case KeyEvent.KEYCODE_ENTER:

                    ActivaMisil();

                    break;

             default:

                    // Si estamos aquí, no hay pulsación que nos interese

                    procesada = false;

                    break;

             }

             return procesada;

       }

3. Cada vez que se pulse una tecla se realizará una llamada al método onKeyDown() con los siguientes parámetros: El primero es un entero que nos identifica el código de la tecla pulsada. El segundo es de la clase KeyEventy nos permite obtener información adicional sobre el evento, como por ejemplo, cuando se produjo. Este método ha de devolver un valor booleano, verdadero, si consideramos que la pulsación ha sido procesada por nuestro código, y falso, si queremos que otro manejador de evento siguiente al nuestro reciba la pulsación.

4. Antes de ponerlo en marcha comenta la llamada a ActivaMisil(), dado que esta función aún no está implementada.

5. Verifica si funciona correctamente.

NOTA:Para poder recoger eventos de teclado desde una vista es necesario que esta tenga el foco y para que esto sea posible verifica que tiene la propiedad focusable="true"

 

Práctica: Manejo de la nave con el teclado

El ejercicio anterior no funciona de forma satisfactoria. Cuando pulsamos una tecla para girar la nave se pone a girar pero ya no hay manera de pararla. El manejador de eventos onKeyDown solo se activa cuando se pulsa una tecla, pero no cuando se suelta.

Trata de escribir el manejador de eventos onKeyUp para que la nave atienda a las órdenes de forma correcta. Puedes partir del siguiente código:


@Override

       public boolean onKeyUp(int codigoTecla, KeyEvent evento) {

             super.onKeyUp(codigoTecla, evento);

             // Suponemos que vamos a procesar la pulsación

             boolean procesada = true;

             ...

             return procesada;

       }

 

Solución: Una posible solución al ejercicio se muestra a continuación:

@Override

       public boolean onKeyUp(int codigoTecla, KeyEvent evento) {

             super.onKeyUp(codigoTecla, evento);

             // Suponemos que vamos a procesar la pulsación

             boolean procesada = true;

             switch (codigoTecla) {

             case KeyEvent.KEYCODE_DPAD_UP:

                    aceleracionNave = 0;

                    break;

             case KeyEvent.KEYCODE_DPAD_LEFT:

             case KeyEvent.KEYCODE_DPAD_RIGHT:

                    giroNave = 0;

                    break;

             default:

                    // Si estamos aquí, no hay pulsación que nos interese

                    procesada = false;

                    break;

             }

             return procesada;

       }


{jcomments on}

La pantalla táctil

Los teléfonos Android suelen incorporar una pantalla táctil, que es utilizada como dispositivo principal de entrada. El uso más importante de la pantalla táctil es como sustituto del ratón de un ordenador de sobremesa. De esta forma podemos seleccionar, arrastrar y soltar cualquier elemento de la pantalla de forma sencilla. No obstante el uso de este dispositivo no acaba aquí. Suele utilizarse en sustitución del teclado en aquellos dispositivos que no disponen de teclado físico. También puede ser utilizada como entrada de un videojuego, como se verá en este apartado. Otra alternativa para usar la pantalla táctil consiste en el uso de gestures soportado a partir del SDK 1.6. Las gestures serán estudiadas en el siguiente punto. Otro abanico de nuevas posibilidades se abre con el multi-touch, soportado a partir del SDK 2.0. 

 

El manejo básico de la pantalla táctil pasa por definir el método OnTouchEventen una clase View (o implementar la interfaz OnTouchListener en otras clases). Este método nos devolverá en un parámetro, un objeto de la clase MotionEvent.

Los métodos más interesantes de la clase MotionEvent se indican a continuación:

getAction() Tipo de acción realizada. En API level 1 puede ser: ACTION_DOWN, ACTION_MOVE, ACTION_UPo ACTION_CANCEL.

getX(), getY() posición de la pulsación.

getDownTime() Tiempo en ms en que el usuario presionó por primera vez en una cadena de eventos de posición.

getEventTime() Tiempo en ms del evento actual.

getPressure() Estima la presión de la pulsación. El valor 0 es el mínimo, el valor 1 representa una pulsación normal.

getSize() Valor escalado en 0 y 1 que estima el grosor de la pulsación.

A partir del API level 5 estos métodos pueden indicar como parámetro un índice de puntero para decirle al sistema sobre cuál de los distintos punteros estamos consultando.

 

Ejercicio paso a paso: Uso de la pantalla táctil

En este ejercicio se mostrará cómo podemos capturar los eventos procedentes de la pantalla táctil. También se aprovechará para repasar otros conceptos, como: Creación de Layouts y herramientas de revisión de código en Eclipse.

1. Crea un nuevo proyecto con nombre PantallaTactilEl nombre de la actividad principal debe de ser, PantallaTactilActivity.

2. Modifica el Layout main.xml para que tenga una apariencia similar a la siguiente. De esta forma practicarás la creación de Layouts. A la derecha se muestra la estructura de vistas que contiene.

3. Una posible solución se muestra a continuación:


<LinearLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent"

    android:orientation="vertical" >

    <TextView

        android:id="@+id/TextViewEntrada"

        android:layout_width="fill_parent"

        android:layout_height="0dp"

        android:layout_weight="1"

        android:text="Pulsa sobre esta vista"

        android:gravity="center"

        android:background="#0000FF"

        android:layout_margin="2mm"

        android:textSize="10pt"/>

    <ScrollView

        android:id="@+id/scrollView1"

        android:layout_width="fill_parent"

        android:layout_height="0dp"

        android:layout_weight="1" >

        <TextView

            android:id="@+id/TextViewSalida"

            android:layout_width="fill_parent"

            android:layout_height="fill_parent"

            android:text="Pulsaciones:"/>

    </ScrollView>

</LinearLayout>

4. Introduce las siguientes dos líneas al final del método onCreate():

TextView entrada = (TextView)findViewById(R.id.TextViewEntrada);

entrada.setOnTouchListener(this);

5. Pulsa Shift-Ctrl-O para añadir los imports.

6. Observa como el método setOnTouchListener está marcado como erróneo. Si pones el cursor encima, te indicará que el parámetro de este método (this) es de la clase PantallaTactilActivity, y es necesario que sea de tipo OnTouchListener.

7. Para evitar el error te mostrará una lista de posibles soluciones. Selecciona la última “Lef  ‘PantallaTactilActivity’ implement ‘OnTouchListener’” de esta forma implementaremos este interfaz y nuestra clase podrá ser considerada de este tipo. La declaración de la clase cambiará a:

public class PantallaTactilActivity extends Activity 
                                                                        implementsOnTouchListener {

8. Se ha solucionado el problema anterior, pero ha aparecido otro. Ahora, la PantallaTactilActivity está marcada como errónea. El problema consiste en que estamos diciendo que implementamos el interfaz OnTouchListener pero no hemos implementado ninguno de los métodos de este interfaz.

9. Para evitar el error selecciona en la lista de posibles soluciones: Add unimplemented methods” de esta forma se añadirán todos los métodos necesarios de este interfaz. La declaración de la clase cambiará a:

public boolean onTouch(View arg0, MotionEvent arg1) {

   // TODO Auto-generated method stub

   return false;

}

10. Reemplaza el nombre de los parámetros por otros más expresivos. Por ejemplo: arg0 por vista y arg1 por evento.

11. Observa como este método ha de devolver un parámetro. Actualmente es false, que significa que no nos hemos hecho cargo de la pulsación, el sistema seguirá pasando este evento a otras vistas. En este caso el LinearLayout que contiene la vista. Cámbialo a true, para que el sistema no siga propagando este evento.

12. Reemplaza la línea “// TODO Auto-generated method stub“ por:

      TextView salida = (TextView) findViewById(R.id.TextViewSalida);

salida.append(evento.toString()+"\n" );

13. Ejecuta el proyecto y verifica el resultado.

acction=0 significa que se ha pulsado sobre la pantalla, acction=1 significa que se ha soltado yacction=2 que se está desplazando el dedo. (estos tres valores corresponden con las constantesMotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP y MotionEvent.ACTION_MOVE)

14. Modifica el proyecto para que cuando el móvil se ponga en apaisado el Layout que se visualice sea:

 

16. Verifica el resultado en un dispositivo real.

17. No todas las pantallas táctiles soportan los métodos getPression() y getSize(). Prueba con tu terminal si lo soporta y en tal caso observa el rango de valores que obtienes.

18.  Conéctate a www.androidcurso.com y localiza el ejercicio que acabas de realizar. Comprueba los valores obtenidos con otros terminales y comparte los valores obtenidos por tu terminal.

 

Gestures

La pantalla táctil es uno de los mecanismos más cómodos para interaccionar con un teléfono Android. No obstante el reducido tamaño de la pantalla en los teléfonos móviles hace que el diseño de la interfaz de usuario sea complejo. Por ejemplo, tratar de introducir las decenas de botones y menús que incorporan la mayoría de aplicaciones de sobremesa sería imposible en una pantalla de 3 pulgadas. Para tratar de dar nuevas alternativas en el diseño de interfaz de usuario, a partir del SDK 1.6, se incorporan las gestures.

Una gesture es un movimiento pregrabado sobre la pantalla táctil, que la aplicación puede reconocer. De esta forma, la aplicación podrá realizar acciones especiales en función de la gesture introducida por el usuario. Esto permite simplificar mucho una interfaz de usuario al poder reducir el número de botones.

Si quieres ver un ejemplo de cómo es utilizado gestures en una aplicación real, te recomendamos que bajes “Google Gesture Search” del Market. Como puedes ver en la siguiente ilustración, esta aplicación te permite introducir una secuencia de letras o dígitos escrita directamente en la pantalla. A partir de estos caracteres realiza una búsqueda global en tu teléfono (aplicaciones, música, contactos...).

Creación y uso de una librería de gestures

El primer paso para usar gestures en nuestra aplicación es crear una librería que contenga algunas de ellas. Con este propósito, puedes utilizar la aplicación Gesture Builder, que está pre-instalada en la versión 1.6 del emulador.

 

Ejercicio paso a paso: Creación de una librería de gestures

1. Abre un emulador con nivel de API 24 y con memoria externa.

2. En el menú de programas busca el siguiente icono y abre la aplicación.

3. Para añadir una nueva gesture a tu librería pulsa el botón “Add gesture” y realiza un trazado sobre la pantalla (por ejemplo un visto bueno) a continuación introduce un nombre asociado a esta gesture (por ejemplo “configurar”). El resultado se muestra a continuación:

4. Si no te gusta como ha quedado el trazado, no tienes más que realizar uno nuevo. El anterior será descartado.

5. Pulsa el botón “Done” para aceptar la gesture. Tras pulsar este botón aparecerá un cuadro de texto indicándote donde se acaba de guardar el fichero con la librería de gestures.

NOTA: Si intentas crear con el emulador una gesture formada por varios trazos (por ejemplo el símbolo “X”). Es posible que solo quede almacenado el último trazo. Para que ambos trazos sean reconocidos en la misma gesture, has de introducir el segundo justo a continuación del primero. Puede resultar algo difícil, pero tras un par de intentos lo conseguirás. No te preocupes, introducir una gesture de varios trazos en un dispositivo real no resulta tan complicado como en el emulador. Concretamente, el problema está en el valorFadeOffset que indica el tiempo máximo en milisegundos entre dos trazos de la misma gesture. Si al introducir dos trazos el tiempo entre ellos es mayor que FadeOffset, se considerará que se han introducido dos gestures diferentes. Por defecto, este valor es asignado a 420 milisegundos. El valor resulta adecuado con un dispositivo real, pero muy pequeño para el emulador. En el ejemplo descrito más adelante daremos un valor más alto a FadeOffset si queremos trabajar de forma más cómoda con el emulador.

6. Trata de introducir las gestures mostradas en la siguiente captura. Para mejorar el porcentaje de reconocimientos correctos, puede ser interesante introducir varias veces la misma gesture con trazados alternativos. Esto se consigue dándole el mismo nombre a dos gestures

7. Cada vez que una nueva gesture es introducida aparece una ventana de texto que indica el fichero donde está almacenanda nuestra librería. Seguramente será “/sdcard/gestures”. Con Android Studio pulsa el botón Android Device Monitor  de la barra de herramientas y selecciona la lengüeta File Explorer.

8. Selecciónalo y pulsa el botón "guardar" para guardarlo en tu ordenador.

 

Ejercicio paso a paso: Añadiendo la librería de gestures a nuestra aplicación

1. Para utilizar la librería que acabas de guardar, crea un nuevo proyecto con los siguientes datos:

Application name: Gestures
Package name: org.example.gestures
☑ Phone and Tablet
    Minimum SDK: API 15 Android 4.0.3 (IceCreamSandwich)
Add an activity: Empty Activity

2. El siguiente paso va a consistir en crear la carpeta res/raw en el proyecto y copiar en ella el fichero que has guardado (gestures) en el ejercicio anterior. 

3. Reemplaza res/layout/activity_main.xml por el siguiente código:

<LinearLayout 
   xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent">

  <TextView 

    android:layout_width="fill_parent"

    android:layout_height="wrap_content"

    android:gravity="center_horizontal"

    android:text="Introduce una gesture"

    android:textSize="8pt"

    android:layout_margin="10dip"/>

  <TextView 

    android:id="@+id/salida"

    android:layout_width="fill_parent"

    android:layout_height="wrap_content"/>

  <android.gesture.GestureOverlayView

    android:id="@+id/gestures"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent"

    android:gestureStrokeType="multiple"

    android:fadeOffset="800"/>

</LinearLayout>

El layout anterior está formado por un LinearLayot que contiene: un TextView con un título, un TextView para mostrar la salida del programa y un GestureOverlayView que será utilizado para introducir los gestures. En esta última etiqueta el parámetro gestureStrokeType indica que permitimos gestures formados por varios trazos. El parámetro fadeOffset ha sido explicado en el apartado anterior.

4. Reemplaza el código de la actividad por:

public class Gestures extends Activity implements OnGesturePerformedListener {

    private GestureLibrary libreria;

    private TextView salida;

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        libreria = GestureLibraries.fromRawResource(this, R.raw.gestures);

        if (!libreria.load()) {

          finish();

        }

        GestureOverlayView gesturesView = (GestureOverlayView) findViewById(R.id.gestures);

        gesturesView.addOnGesturePerformedListener(this);

        salida = (TextView) findViewById(R.id.salida);

    }

  

    public void onGesturePerformed(GestureOverlayView ov, Gesture gesture) {

        ArrayList<Prediction> predictions = libreria.recognize(gesture);

        salida.setText("");

        for (Prediction prediction : predictions){

          salida.append(prediction.name+" " + prediction.score+"\n");       

         }

    }

}

En este código se comienza declarando dos campos de la clase librería contendrá la librería de gestures creada en el ejercicio anterior y salida que corresponde al TextView donde escribiremos los resultados.

En el constructor, tras realizar las operaciones habituales, se carga la librería de gestures desde los recursos y en caso de no ser cargada finaliza la aplicación. A continuación, asocia el GestureOverlayView de main.xml a el objeto gestureView y se indica que nuestra clase será el escuchador de este elemento.  Finalmente se asocia el TextView donde queremos sacar la salida al objeto salida.

El método onGesturePerformed se introduce para implementar la interfaz OnGesturePerformedListener. Este método tiene dos parámetros, el GestureOverlayView donde se ha introducido el gesture y el objeto Gesture que ha sido introducido. El primer paso consiste en reconocer el gerture comparándolo con la lista de nuestra librería. El resultado es una lista ordenada de Predictions con las gestures que considera más parecidas a la introducida. Tras borrar salida, se recorre todos los elementos de esta lista mostrando el nombre del gesture (prediction.name) y la puntuación de reconocimiento (prediction.score). Resulta complicado fijar un umbral, pero una puntuación inferior a 1 se suele considerar demasiado baja para tenerla en cuenta como predicción.

5. Ejecuta la aplicación y estudia las puntuaciones obtenidas.

 

Añadiendo gestures a Asteroides

En el siguiente ejercicio trataremos de aplicar lo aprendido a la aplicación Asteroides. La idea es que de forma alternativa a usar el menú de cuatro botones que se muestra al arrancar la aplicación, se pueda utilizar gestures.

Práctica: Añadiendo gestures a Asteroides

1. Crea la carpeta res/raw y copia el fichero gestures, que contiene la librería creada anteriormente.

2. Modifica el Layout main.xml para que disponga de un GestureOverlayView.

3. Cuando el usuario esté utilizando este Layout ha de poder introducir alguna de las cuatro gestures de la librería de forma que se ejecute la acción correspondiente.

 

Solución:

Los pasos a seguir para realizar el ejercicio anterior se describen a continuación:

1. Añade al principio de res/layout/main.xml el siguiente código. Cierra la etiqueta al final del fichero.

<android.gesture.GestureOverlayView

   xmlns:android="http://schemas.android.com/apk/res/android"

   android:id="@+id/gestures"

   android:layout_width="fill_parent"

   android:layout_height="fill_parent"

   android:gestureStrokeType="multiple"

   android:fadeOffset="800">
 

2. En la clase MainActivity añade en la definición:

public class MainActivity extends Activity implements OnGesturePerformedListener{

    private GestureLibrary libreria;

    …         
3. Añade al final del método onCreate:

libreria= GestureLibraries.fromRawResource(this, R.raw.gestures);

if(!libreria.load()) {

   finish();

}

GestureOverlayView gesturesView = (GestureOverlayView) findViewById(R.id.gestures);

gesturesView.addOnGesturePerformedListener(this);
 

4.  Añade el siguiente método:

public void onGesturePerformed(GestureOverlayView ov, Gesture gesture) {

   ArrayList<Prediction> predictions=libreria.recognize(gesture);

   if(predictions.size()>0){

          String comando = predictions.get(0).name;

          if(comando.equals("play")){

                 lanzarJuego();

          } else if(comando.equals("configurar")){

                 lanzarPreferencias();

          } else if(comando.equals("acerca_de")){

                 lanzarAcercaDe();

          } else if(comando.equals("cancelar")){

                 finish();

          }

   }

}

Manejo de la pantalla táctil con multi-touch

Las pantallas táctiles más modernas tienen la posibilidad de indicar la posición de varios punteros sobre la pantalla a un mismo tiempo. Para averiguar si el dispositivo tiene esta capacidad puedes utilizar el siguiente código:

 

boolean multiTouch = getPackageManager().hasSystemFeature( PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH);

 

A partir de la versión 2.0 (API level 5), un objeto de la clase MotionEvent contendrá información de todos estos punteros. Un puntero estará activo desde que se pulsa sobre la pantalla hasta que se deja de presionar. El número de punteros activos puede consultarse llamando al método getPointerCount(). Cada puntero tiene un id para identificarlo que es asignado cuando se produce la primera pulsación.

La clase MotionEvent amplia la lista de constantes para identificar las acciones posibles para adaptarse a multi-touch a partir de la versión 2.0. Veamos una lista:

 

ACTION_DOWNSe pulsa en la pantalla sin haber otro puntero activo

ACTION_UPSe deja de presionar el último puntero activo

ACTION_MOVECualquiera de los punteros activos se desplaza

ACTION_CANCEL– Se cancela un gesture.

ACTION_OUTSIDE– El puntero se sale de la vista

ACTION_POINTER_DOWN– Se pulsa un nuevo puntero distinto al primero.

ACTION_POINTER_UP– Se deja de presionar un puntero pero no es el último

Ejercicio paso a paso: Uso de la pantalla táctil multi-touch

1. Ejecuta el ejercicio anterior en un dispositivo real con capacidad de multi-touch (si no dispones de uno te será imposible realizar este ejercicio).

2. Pulsa simultáneamente con dos dedos en la pantalla: Si lo haces sin desplazar los dedos recibirás 4 eventos. Los dos primeros por las pulsaciones de cada dedo y los dos siguientes cuando se levanten. El resultado puede ser simular al siguiente:

3. Como puedes ver cuando hay más de un puntero en pantalla la acción resulta compleja de interpretar. Veremos como hacerlo a continuación.

4. En primer lugar asegúrate que tu proyecto utiliza las APIs 2.0 o superior. Para ello utiliza la opción de menú Project/Properties/Android y selecciona en Project Build Target la opción Android 2.0.

5. Reemplaza la siguiente línea del método onTouch()


salida.append(evento.toString()+"\n");
 

por:

String acciones[] = { "ACTION_DOWN", "ACTION_UP", "ACTION_MOVE", "ACTION_CANCEL","ACTION_OUTSIDE", "ACTION_POINTER_DOWN", "ACTION_POINTER_UP" };

int accion = evento.getAction();

int codigoAccion = accion & MotionEvent.ACTION_MASK;

salida.append(acciones[codigoAccion]);

for (int i = 0; i < evento.getPointerCount(); i++) {

salida.append(" puntero:" + evento.getPointerId(i) + 
" x:" + evento.getX(i) + " y:" + evento.getY(i));

}

salida.append("\n");

return true;

6. Para visualizar cada posible acción hemos creado un array con sus nombres. A continuación averiguamos la acción en la variable accion. Esta nueva acción la ha podido hacer cualquier puntero de los activos o uno nuevo. A partir de la versión 2.0 en esta variable se codifica simultáneamente el código de la acción (8 bits menos significativos) e índice de puntero que la ocasiona (siguientes 8 bits). Para obtener esta información por separado puedes utilizar el siguiente código:

int codigoAccion = accion & MotionEvent.ACTION_MASK;

int iPuntero = (accion & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;

7. A partir de la versión 2.2 (API level 8) las dos últimas constantes quedan obsoletas y se definen otras dos equivalentes cuyos nombres son más adecuados:

int iPuntero = (accion & MotionEvent.ACTION_POINTER_INDEX_MASK)
                                             >> MotionEvent.
ACTION_POINTER_INDEX_SHIFT;

8. Una vez obtenido el código de la acción mostramos su nombre en la vista salida. Luego hacemos un bucle para mostrar información de todos los punteros activos. El método getPointerCount() nos permite averiguar su número. Vamos a recorrer los punteros activos con la variable i. Al principio de este apartado vimos una serie de métodos para averiguar información sobre el puntero (getX(), getSize(),…). A partir de la versión 2.0 estos métodos siguen dándome información sobre el primer puntero activo que se pulsó, pero ahora también disponemos de los mismos métodos pero indicando un índice de puntero (getX(i), getSize(i),…) para averiguar información del resto de punteros.

9. El método getPointerId(int indice)nos permite averiguar el identificador del puntero. No hay que confundir el índice de puntero con su identificador. El índice se asigna en función del orden en que fueron pulsados. El índice cero siempre es el más antiguo. El índice de un puntero decrece a medida que los punteros anteriores a él dejan de estar activos. Por el contrario, el identificador de un puntero es asignado cuando se crea y permanece constante durante toda su vida. Nos será muy útil para seguir la pista de un determinado puntero. El método findPointerIndex(int id)nos permite averiguar el índice de un puntero a partir de su identificador.

10. Ejecuta de nuevo el proyecto y vuelve a pulsar con dos dedos. El resultado ha de ser similar al siguiente:

11. Prueba con otras combinaciones de pulsaciones e investiga la relación entre el índice y el id de puntero.

12. Modifica el programa para que además se muestre en cada evento, el índice de puntero que lo ocasionó.

 

 

 

Manejo de la nave con la pantalla táctil

Ejercicio paso a paso: Manejo de la nave con la pantalla táctil.

 

Veamos cómo podemos utilizar un manejador de eventos de la pantalla táctil para maniobrar la nave de Asteroides. El código que se muestra permite manejar la nave de la siguiente forma: un desplazamiento del dedo horizontal hace girar la nave, un desplazamiento vertical produce una aceleración y, si al soltar la pulsación no hay movimiento, se provoca un disparo.

1. Abre el proyecto Asteroides.

2. Inserta este código en la clase  VistaJuego. 

private float mX=0, mY=0;

private boolean disparo=false;

 

@Override

public boolean onTouchEvent (MotionEvent event) {

   super.onTouchEvent(event);

   float x = event.getX();

   float y = event.getY();

   switch (event.getAction()) {

   case MotionEvent.ACTION_DOWN:

          disparo=true;

          break;

   case MotionEvent.ACTION_MOVE:

          float dx = Math.abs(x - mX);

          float dy = Math.abs(y - mY);

          if (dy<6 && dx>6){

                 giroNave = Math.round((x - mX) / 2);

                 disparo = false;

          } else if (dx<6 && dy>6){

                 aceleracionNave = Math.round((mY - y) / 25);

                 disparo = false;

          }

          break;

   case MotionEvent.ACTION_UP:

          giroNave = 0;

          aceleracionNave = 0;

          if (disparo){

          ActivaMisil();

          }

          break;

   }

   mX=x; mY=y;       

    return true;

}

3. Las variables globales mX y mY van a ser utilizadas para recordar las coordenadas del último evento. Comparándolas con las coordenadas actuales (x, y) podremos verificar si se trata de un desplazamiento horizontal o vertical. Por otra parte, la variable disparo es activada cada vez que comienza una pulsación (ACTION_DOWN). Si esta pulsación es continuada con un desplazamiento horizontal o vertical, disparo es desactivado. Si por el contrario, se levanta el dedo (ACTION_UP) sin haberse producido estos desplazamientos, disparo no estará desactivado y se llamará a ActivaMisil().

4. Antes de ponerlo en marcha comenta la llamada a ActivaMisil(), dado que esta función aún no está implementada.

5. Verifica si funciona correctamente.

6. Modifica los parámetros de ajuste (<6,>6, /2, /25), para que se adapten de forma adecuada a tu terminal.

7. En el juego original podíamos acelerar pero no decelerar. Si queríamos detener la nave teníamos que dar un giro de 180 grados y acelerar lo justo. Modifica el código anterior para que no sea posible decelerar.

{jcomments on}

Creación y uso de una librería de gestures

El primer paso para usar gestures en nuestra aplicación es crear una librería que contenga algunas de ellas. Con este propósito, puedes utilizar la aplicación Gesture Builder, que está pre-instalada en la versión 1.6 del emulador.

 

Ejercicio paso a paso: Creación de una librería de gestures

1. Abre un emulador con versión 1.6 y con memoria externa.

2. En el menú de programas busca el siguiente icono y abre la aplicación.

3. Para añadir una nueva gesture a tu librería pulsa el botón “Add gesture” y realiza un trazado sobre la pantalla (por ejemplo un visto bueno) a continuación introduce un nombre asociado a esta gesture (por ejemplo “configurar”). El resultado se muestra a continuación:

4. Si no te gusta como ha quedado el trazado, no tienes más que realizar uno nuevo. El anterior será descartado.

5. Pulsa el botón “Done” para aceptar la gesture. Tras pulsar este botón aparecerá un cuadro de texto indicándote donde se acaba de guardar el fichero con la librería de gestures.

NOTA: Si intentas crear con el emulador una gesture formada por varios trazos (por ejemplo el símbolo “X”). Es posible que solo quede almacenado el último trazo. Para que ambos trazos sean reconocidos en la misma gesture, has de introducir el segundo justo a continuación del primero. Puede resultar algo difícil, pero tras un par de intentos lo conseguirás. No te preocupes, introducir una gesture de varios trazos en un dispositivo real no resulta tan complicado como en el emulador. Concretamente, el problema está en el valorFadeOffset que indica el tiempo máximo en milisegundos entre dos trazos de la misma gesture. Si al introducir dos trazos el tiempo entre ellos es mayor que FadeOffset, se considerará que se han introducido dos gestures diferentes. Por defecto, este valor es asignado a 420 milisegundos. El valor resulta adecuado con un dispositivo real, pero muy pequeño para el emulador. En el ejemplo descrito más adelante daremos un valor más alto a FadeOffset si queremos trabajar de forma más cómoda con el emulador.

 

6. Trata de introducir las gestures mostradas en la siguiente captura. Para mejorar el porcentaje de reconocimientos correctos, puede ser interesante introducir varias veces la misma gesture con trazados alternativos. Esto se consigue dándole el mismo nombre a dos gestures

7. Cada vez que una nueva gesture es introducida aparece una ventana de texto que indica el fichero donde está almacenanda nuestra librería. Seguramente será “/sdcard/gestures”. Utiliza la vista File Explorer de Eclipse para localizar este fichero. 

8. Selecciónalo y pulsa el botón "guardar" para guardarlo en tu ordenador.

 

Ejercicio paso a paso: Añadiendo la librería de gestures a nuestra aplicación

1. Para utilizar la librería que acabas de guardar, crea un nuevo proyecto con los siguientes datos:

Application Name: Gestures

Package Name: org.example.gestures

Minimun Requiered SDK: API 7: Android 2.1 (Eclair)

Create Activity: Gestures

Layout Name: main

2. El siguiente paso va a consistir en crear la carpeta res/raw en el proyecto y copiar en ella el fichero que has guardado (gestures) en el ejercicio anterior. 

3. Reemplaza res/layout/main.xml por el siguiente código:

<LinearLayout 
   xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent">

  <TextView 

    android:layout_width="fill_parent"

    android:layout_height="wrap_content"

    android:gravity="center_horizontal"

    android:text="Introduce una gesture"

    android:textSize="8pt"

    android:layout_margin="10dip"/>

  <TextView 

    android:id="@+id/salida"

    android:layout_width="fill_parent"

    android:layout_height="wrap_content"/>

  <android.gesture.GestureOverlayView

    android:id="@+id/gestures"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent"

    android:gestureStrokeType="multiple"

    android:fadeOffset="800"/>

</LinearLayout>

 

4. El layout anterior está formado por un LinearLayot que contiene: un TextView con un título, un TextView para mostrar la salida del programa y un GestureOverlayView que será utilizado para introducir los gestures. En esta última etiqueta el parámetro gestureStrokeType indica que permitimos gestures formados por varios trazos. El parámetro fadeOffset ha sido explicado en el apartado anterior.

5. Reemplaza el código de la actividad por:


public class Gestures extends Activity implements 
                                                                      OnGesturePerformedListener {

    private GestureLibrary libreria;

    private TextView salida;

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        libreria = GestureLibraries.fromRawResource(this, 
                                                                                      R.raw.
gestures);

        if (!libreria.load()) {

          finish();

        }

        GestureOverlayView gesturesView = (GestureOverlayView) 
                                                                      findViewById(R.id.
gestures);

        gesturesView.addOnGesturePerformedListener(this);

        salida = (TextView) findViewById(R.id.salida);

    }

  

    public void onGesturePerformed(GestureOverlayView ov, 
                                                                                   Gesture gesture) {

        ArrayList<Prediction> predictions = 
                                                                      
libreria.recognize(gesture);

        salida.setText("");

        for (Prediction prediction : predictions){

          salida.append(prediction.name+" " + 
                                                                               prediction.
score+"\n");       

         }

    }

}

6. En este código se comienza declarando dos campos de la clase librería contendrá la librería de gestures creada en el ejercicio anterior y salida que corresponde al TextView donde escribiremos los resultados.

7. En el constructor, tras realizar las operaciones habituales, se carga la librería de gestures desde los recursos y en caso de no ser cargada finaliza la aplicación. A continuación, asocia el GestureOverlayView de main.xml a el objeto gestureView y se indica que nuestra clase será el escuchador de este elemento.  Finalmente se asocia el TextView donde queremos sacar la salida al objeto salida.

8. El método onGesturePerformed se introduce para implementar la interfaz OnGesturePerformedListener. Este método tiene dos parámetros, el GestureOverlayView donde se ha introducido el gesture y el objeto Gesture que ha sido introducido. El primer paso consiste en reconocer el gerture comparándolo con la lista de nuestra librería. El resultado es una lista ordenada de Predictions con las gestures que considera más parecidas a la introducida. Tras borrar salida, se recorre todos los elementos de esta lista mostrando el nombre del gesture (prediction.name) y la puntuación de reconocimiento (prediction.score). Resulta complicado fijar un umbral, pero una puntuación inferior a 1 se suele considerar demasiado baja para tenerla en cuenta como predicción.

9. Ejecuta la aplicación y estudia las puntuaciones obtenidas.

 

Añadiendo gestures a Asteroides

En el siguiente ejercicio trataremos de aplicar lo aprendido a la aplicación Asteroides. La idea es que de forma alternativa a usar el menú de cuatro botones que se muestra al arrancar la aplicación, se pueda utilizar gestures.

 

Práctica: Añadiendo gestures a Asteroides

 

1. Si el proyecto Asteroides ha sido con una versión del SDK anterior a la 1.6, tendrás que actualizarlo como mínimo a esta versión. Para ello pulsa sobre el proyecto con el botón derecho y selecciona “properties”, selecciona en la lista de la izquierda “Android” y marca la versión adecuada.

2. Crea la carpeta res/raw y copia el fichero gestures, que contiene la librería creada anteriormente.

3. Modifica el Layout main.xml para que disponga de un GestureOverlayView.

4. Cuando el usuario esté utilizando este Layout ha de poder introducir alguna de las cuatro gestures de la librería de forma que se ejecute la acción correspondiente.

 

Solución:

Los pasos a seguir para realizar el ejercicio anterior se describen a continuación:

1. Añade al principio de res/layout/main.xml el siguiente código. Cierra la etiqueta al final del fichero.

<android.gesture.GestureOverlayView

   xmlns:android="http://schemas.android.com/apk/res/android"

   android:id="@+id/gestures"

   android:layout_width="fill_parent"

   android:layout_height="fill_parent"

   android:gestureStrokeType="multiple"

   android:fadeOffset="800">
 

2. En el fichero Asteroides.java añade en la definición de la clase:

public class Asteroides extends Activity

implements OnGesturePerformedListener{

    private GestureLibrary libreria;

    …         
3. Añade al final del método onCreate:

libreria= GestureLibraries.fromRawResource(this,
                                                                                       R.raw.
gestures);

if(!libreria.load()) {

   finish();

}

GestureOverlayView gesturesView =

   (GestureOverlayView) findViewById(R.id.gestures);

gesturesView.addOnGesturePerformedListener(this);
 

4.  Añade el siguiente método:

public void onGesturePerformed(GestureOverlayView ov,
                                                                                           Gesture gesture) {

   ArrayList<Prediction> predictions=libreria.recognize(gesture);

   if(predictions.size()>0){

          String comando = predictions.get(0).name;

          if(comando.equals("play")){

                 lanzarJuego();

          } else if(comando.equals("configurar")){

                 lanzarPreferencias();

          } else if(comando.equals("acerca_de")){

                 lanzarAcercaDe();

          } else if(comando.equals("cancelar")){

                 finish();

          }

   }

}

{jcomments on}

Añadiendo gestures a Asteroides

En el siguiente ejercicio trataremos de aplicar lo aprendido a la aplicación Asteroides. La idea es que de forma alternativa a usar el menú de cuatro botones que se muestra al arrancar la aplicación, se pueda utilizar gestures.

Práctica: Añadiendo gestures a Asteroides

1. Si el proyecto Asteroides ha sido con una versión del SDK anterior a la 1.6, tendrás que actualizarlo como mínimo a esta versión. Para ello pulsa sobre el proyecto con el botón derecho y selecciona “properties”, selecciona en la lista de la izquierda “Android” y marca la versión adecuada.

2. Crea la carpeta res/raw y copia el fichero gestures, que contiene la librería creada anteriormente.

3. Modifica el Layout main.xml para que disponga de un GestureOverlayView.

4. Cuando el usuario esté utilizando este Layout ha de poder introducir alguna de las cuatro gestures de la librería de forma que se ejecute la acción correspondiente.

 

Solución:

Los pasos a seguir para realizar el ejercicio anterior se describen a continuación:

1. Añade al principio de res/layout/main.xml el siguiente código. Cierra la etiqueta al final del fichero.


<android.gesture.GestureOverlayView

   xmlns:android="http://schemas.android.com/apk/res/android"

   android:id="@+id/gestures"

   android:layout_width="fill_parent"

   android:layout_height="fill_parent"

   android:gestureStrokeType="multiple"

   android:fadeOffset="800">

2. En el fichero Asteroides.java añade en la definición de la clase:

public class Asteroides extends Activity

implements OnGesturePerformedListener{

    private GestureLibrary libreria;

    


      

3. Añade al final del método onCreate:


libreria = GestureLibraries.fromRawResource(this, 
                                                                                      R.raw.
gestures);

if (!libreria.load()) {

   finish();

}

GestureOverlayView gesturesView =

   (GestureOverlayView) findViewById(R.id.gestures);

gesturesView.addOnGesturePerformedListener(this);

4.  Añade el siguiente método:

public void onGesturePerformed(GestureOverlayView ov, 
                                                                                          Gesture gesture) {

   ArrayList<Prediction> predictions=libreria.recognize(gesture);

   if (predictions.size()>0){

          String comando = predictions.get(0).name;

          if (comando.equals("play")){

                 lanzarJuego(null);

          } else if (comando.equals("configurar")){

                 lanzarPreferencias(null);

          } else if (comando.equals("acerca_de")){

                 lanzarAcercaDe(null);

          } else if (comando.equals("cancelar")){

                 finish();

          }

   }

}

{jcomments on}

Los sensores

Bajo la denominación de sensores se engloba un conjunto de dispositivos con los que podremos obtener información del mundo exterior (en este conjunto no se incluye la cámara, el micrófono o el GPS). Como se verá en este apartado todos los sensores se manipulan de forma homogénea. Son los dispositivos de entrada más novedosos que incorpora Android y con ellos podremos implementar formas atractivas de interacción con el usuario.

Puedes ver algunos aspectos relacionados en formato poli[Media]

Sensores en dispositivos móviles

Si no dispones de un terminal físico, puedes instalar un software de emulación que te permitirá realizar las pruebas sobre el emulador, mientras desde una aplicación de tu PC cambias la orientación de un teléfono ficticio mediante el ratón. Puedes descargarte el software dehttp://www.openintents.org/en/node/6, no obstante, el proceso resulta bastante laborioso. Además, tendrás que cambiar el código de los ejemplos para adaptarlos a los requerimientos del emulador. Por lo tanto, tendrás que realizar dos programas diferentes: uno para sensores reales y otro para el emulador.

Android permite acceder a los sensores internos del dispositivo a través de las clases Sensor,SensorEvent, SensorManager, y la interfaz SensorEventListener, del paquete android.hardware.

La clase Sensor acepta ocho tipos de sensores. Aunque, los sensores disponibles varían en función del dispositivo utilizado:

 

Tipo

CONSTANTE

Utilidad

dim.

desde
API

acelerómetro

TYPE_ACCELEROMETER

medir aceleraciones por grave-dad y cambios de movimiento

3

3

campo magnético

TYPE_MAGNETIC_FIELD

brújula, detectar campor magnéticos

3

3

giroscopio

TYPE_GYROSCOPE

detectar giros

3

3

orientación

TYPE_ORIENTATION

indicar dirección a la que apunta el dispositivo

3

3

luz ambiental

TYPE_LIGHT

ajustar iluminación pantalla

1

3

proximidad

TYPE_PROXIMITY

si hay un objeto a menos de 5 cm (al hablar por teléfono)

bool
eano

3

presión atmosférica

TYPE_PRESSURE

altímetro, barómetro

1

3

temperatura interna

TYPE_TEMPERATURE

evitar sobrecalentamientos

(obsoleto desde API14)

1

3

gravedad

TYPE_GRAVITY

medir la aceleración debida a la gravedad

3

9

acelerómetro lineal

TYPE_LINEAR_ACCELERATION

medir aceleraciones descontando la gravedad

3

9

vector de rotación

TYPE_ROTATION_VECTOR

detectar giros

3

9

temperatura ambiental

TYPE_AMBIENT_TEMPERATURE

medir la temperatura del aire

1

14

humedad relativa

TYPE_RELATIVE_HUMIDITY

medir el punto de rocío, humedad absoluta y relativa.

1

14

Ejercicio paso a paso: Listar los sensores del dispositivo

No todos los dispositivos disponen de los mismos sensores. Por lo tanto, la primera tarea consiste en averiguar los sensores disponibles.

1. Crea un nuevo proyecto con nombre Sensores.

2. Añade la siguiente propiedad al TextView de res/layout/main.xml:

android:id="@+id/salida"

3. Inserta este código en la actividad principal:


public class SensoresActivity extends Activity {

    private TextView salida;

 

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        salida = (TextView) findViewById(R.id.salida);

        SensorManager sensorManager = (SensorManager) 
                                                        getSystemService(
SENSOR_SERVICE);

        List<Sensor> listaSensores = sensorManager. 
                                                        getSensorList(Sensor.
TYPE_ALL);

        for(Sensor sensor: listaSensores) {

             log(sensor.getName());

        }

    }

   private void log(String string) {

          salida.append(string + "\n");

   }

}

 

4. El método comienza indicando el Layout de la actividad y obteniendo el TextView salida, donde mostraremos los resultados. A continuación vamos a vamos a utilizar el método getSystemService para solicitar al sistema servicios específicos. Este método pertenece a la clase Context (Como somos Activity también somos Context) y será muy utilizados para acceder a gran cantidad de servicios del sistema. Al indicar como parámetro  SENSOR_SERVICE,  indicamos que queremos utilizar los sensores. Lo haremos a través del objeto sensorManager. En primer lugar llamamos al método getSensorList() del objeto para que nos de listaSensores, una lista de objetos Sensor. La siguiente línea recorre todos los elementos de esta lista parar llamar a su método getName() para mostrar el nombre de sensor.

5. Ejecuta el programa. Esta es una lista de los valores devueltos por el código anterior ejecutándose en el HTC Magic:

AK8976A 3-axis Accelerometer

AK8976A 3-axis Magnetic field sensor

AK8976A Orientation sensor

AK8976A Temperature sensor

6.    El AK8976A es una combinación de acelerómetro de tres ejes y magnetómetro de tres ejes. Combinando la lectura de los campos gravitatorio y magnético terrestres proporciona también información de orientación. Incluye además un sensor interno de temperatura, útil para comprobar si el móvil se está calentado demasiado.

Como hemos visto la case Sensor nos permite manipular los sensores. A continuación se listan los métodos públicos de la clase Sensor:

public float getMaximumRange()

 

Rango máximo en las unidades del sensor

public String getName()

 

Nombre del sensor

public float getPower()

 

Potencia (mA) usada por el sensor mientras está en uso

public float getResolution()

 

Resolución en las unidades del sensor

public int getType()

 

Tipo genérico del sensor

Public String getVendor()

 

Fabricante del sensor

public int getVersion()

 

Versión del sensor

La clase SensorManager tiene además tres métodos (getInclination, getOrientation y getRotationMatrix), usados para calcular transformaciones de coordenadas.

 

Ejercicio paso a paso: Acceso a los datos del sensor

Veamos ahora como obtener la lectura de cada uno de los sensores.

1. Copia el siguiente código al final de onCreate.


listaSensores = sensorManager.getSensorList(Sensor.TYPE_ORIENTATION);

if (!listaSensores.isEmpty()) {

       Sensor orientationSensor = listaSensores.get(0);

       sensorManager.registerListener(this, orientationSensor,
                                                                                SensorManager.
SENSOR_DELAY_UI);}

listaSensores = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);

if (!listaSensores.isEmpty()) {

       Sensor acelerometerSensor = listaSensores.get(0);

       sensorManager.registerListener(this, acelerometerSensor, 
                                                                                SensorManager.
SENSOR_DELAY_UI);}

listaSensores = sensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);

if (!listaSensores.isEmpty()) {

       Sensor magneticSensor = listaSensores.get(0);

       sensorManager.registerListener(this, magneticSensor, 
                                                                                SensorManager.
SENSOR_DELAY_UI);}

listaSensores = sensorManager.getSensorList(Sensor.TYPE_TEMPERATURE);

if (!listaSensores.isEmpty()) {

       Sensor temperatureSensor = listaSensores.get(0);

       sensorManager.registerListener(this, temperatureSensor, 
                                                                                SensorManager.
SENSOR_DELAY_UI);}

Comenzamos consultando si disponemos de un sensor de orientación. Para ello preguntamos al sistema que nos de todos los sensores de este tipo llamando a getSensorList(). Si la lista no está vacía obtenemos el primer elemento (el 0). Es necesario registrar cada tipo de sensor por separado para poder obtener información de él. El método registerListener() toma como primer parámetro un objeto que implemente el interface SensorEventListener, veremos a continuación cómo se implementa esta interfaz (se indica this porque la clase que estamos definiendo implementará este interfaz para recoger eventos de sensores). El segundo parámetro es el sensor que estamos registrando. Y el tercero indica al sistema con qué frecuencia nos gustaría recibir actualizaciones del sensor. Acepta cuatro posibles valores, de menor a mayor frecuencia tenemos: SENSOR_DELAY_NORMAL, SENSOR_DELAY_UI, SENSOR_DELAY_GAME y SENSOR_DELAY_FASTEST. Esta indicación sirve para que el sistema estime cuánta atención necesitan los sensores, pero no garantiza una frecuencia concreta.

2. Para que nuestra clase implemente el interface que hemos comentado añade a la declaración de la clase:

implements SensorEventListener

3. Para recibir los datos de los sensores tenemos que implementar dos métodos de la interfaz SensorEventListener:

@Override 
public void onAccuracyChanged(Sensor sensor, int precision) {}

 

@Override

public void onSensorChanged(SensorEvent evento) {

  //Cada sensor puede provocar que un thread principal pase por aquí

  //así que sincronizamos el acceso (se verá más adelante)

       synchronized (this) {

             switch(evento.sensor.getType()) {

             case Sensor.TYPE_ORIENTATION:

                    for (int i=0 ; i<3 ; i++) {

                           log("Orientación "+i+": "+evento.values[i]);

                    }

                    break;

             case Sensor.TYPE_ACCELEROMETER:

                    for (int i=0 ; i<3 ; i++) {

                           log("Acelerómetro "+i+": "+evento.values[i]);

                    }

                    break;

             case Sensor.TYPE_MAGNETIC_FIELD:

                    for (int i=0 ; i<3 ; i++) {

                           log("Magnetismo "+i+": "+evento.values[i]);

                    }

                    break;

             default:

                    for (int i=0 ; i<evento.values.length ; i++) {

                           log("Temperatura "+i+": "+evento.values[i]);

                    }

             }

       }

}

4. Cuando implementamos un interface estamos obligados a implementar todos sus métodos. En este caso son dos. Para onAccuracyChanged no queremos ninguna acción específica, pero lo tenemos que incluir.Cuando un sensor cambie se llamará al método onSensorChanged, aquí comprobamos qué sensor ha causado la llamada y leeremos los datos.

5. Verifica que el programa funciona correctamente.

 

Cuando el evento se dispara en el método onSensorChanged comprobamos qué sensor lo ha causado y leemos los datos. Los posibles valores devueltos se indican en la documentación de la clase SensorEvent[1].

Un programa que muestra los sensores disponibles y sus valores en tiempo real

Un programa que muestra los sensores disponibles y sus valores en tiempo real

La aplicación realizada en el ejercicio anterior resulta algo dificil de utilizar. En primer lugar, presupone que se dispone de cuatro sensores, cosa que no será cierta en muchos dispositivos. Además, los datos de los sensores cambian demasiado rápido para poder leerlos en un listado. El ejercicio mostrado a continuación es un resumen de los ejemplos anteriores, pero que permite mostrar en la misma pantalla los valores actuales de todos los sensores de un dispositivo. Además, es un buen ejemplo para ilustrar cómo crear una vista desde código.

Ejercicio paso a paso: Creación de una vista desde código para mostrar los datos de los sensores

1.     Crea un nuevo proyecto con nombre Sensores2.

2.     Abre el Layout main.xml y añade al LinearLayout el siguiente atributo:

<LinearLayout

 android:id="@+id/raiz"
 …

3.     Reemplaza el código de la actividad por el siguiente:

public class Sensores2Activity extends Activity 
                                                                                
implements SensorEventListener {

private List<Sensor> listaSensores;

private TextView aTextView[][] = new TextView[20][3];

   

@Override public void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.main);

       LinearLayout raiz = (LinearLayout) findViewById(R.id.raiz);

   SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);

       listaSensores = sm.getSensorList(Sensor.TYPE_ALL);

       int n = 0;

       for (Sensor sensor : listaSensores) {

             TextView mTextView = new TextView(this);

             mTextView.setText(sensor.getName());

             raiz.addView(mTextView);

             LinearLayout nLinearLayout = new LinearLayout(this);

             raiz.addView(nLinearLayout);

             for (int i = 0; i < 3; i++) {

                    aTextView[n][i] = new TextView(this);

                    aTextView[n][i].setText("?");

                    aTextView[n][i].setWidth(87);

             }

             TextView xTextView = new TextView(this);

             xTextView.setText("  X: ");

             nLinearLayout.addView(xTextView);

             nLinearLayout.addView(aTextView[n][0]);

             TextView yTextView = new TextView(this);

             yTextView.setText("  Y: ");

             nLinearLayout.addView(yTextView);

             nLinearLayout.addView(aTextView[n][1]);

             TextView zTextView = new TextView(this);

             zTextView.setText("  Z: ");

             nLinearLayout.addView(zTextView);

             nLinearLayout.addView(aTextView[n][2]);

             sm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI);

             n++;

       }

}

 

@Override public void onAccuracyChanged(Sensor sensor, int accuracy) {}

 

@Override public void onSensorChanged(SensorEvent event) {

       synchronized (this) {

             int n = 0;

             for (Sensor sensor: listaSensores) {

                    if (event.sensor == sensor) {

                           for (int i=0; i<event.values.length; i++) {

                             aTextView[n][i].setText(Float.toString(event.values[i]));

                           }

                    }

                    n++;

             }

       }

}

}

Como puedes observar esta actividad utiliza el Layout creado por defecto  que básicamente es un LinearLayot (en el código corresponde a la variable raiz) con un TextView en su interior (“Hello Word …”). A raiz se le va a ir añadiendo una serie de vistas adicionales según los sensores encontrados en el dispositivo. Por cada sensor se añade: un TextView  con el nombre del sensor, un LinearLayot de tipo horizontal [2] para contener a su vez un TextView  con “ X: “,un TextView  con el valor del sensor en el eje X, un TextView  con “ Y “,un TextView con el valor del sensor en el eje Y, un TextView  con “ Z: “ y un TextView  con el valor del sensor en el eje Z. Las referencias a los TextView  donde se visualizará los valores de los sensores se almacenan en el array aTextView[][]  donde el primer índice identifica el número de sensor y el segundo la dimensión X,Y o Z.

En el método onSensorChanged() se hace un bucle para localizar el índice del sensor que ha cambiado y se modifican los TextView correspondiente al sensor con los valores leidos.

NOTA: No todos los sensores tienen tres dimensiones. Por ejemplo, en el caso del sensor de temperatura solo se cambiará en el valor de X.

4.     Verifica sobre un dispositivo real que el programa funciona correctamente.

 

[2] En un LinearLayout si no se indica nada, la orientación por defecto es horizontal.

Utilización de los sensores en Asteroides

Utilización de los sensores en Asteroides

A continuación, proponemos una serie de ejercicios y prácticas para manejar la nave de Asteroides utilizando el sensores.

 

Ejercicio paso a paso: Manejo de la nave con el sensor de orientación

 

1. En primer lugar, implementa la interfaz SensorEventListener.

public class VistaJuego extends View implements SensorEventListener {

2. En el constructor registra el sensor e indica que nuestro objeto recogerá la llamada callback:

SensorManager mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);

List<Sensor> listSensors = mSensorManager.getSensorList( 
                                                                           Sensor.
TYPE_ORIENTATION);

if (!listSensors.isEmpty()) {

   Sensor orientationSensor = listSensors.get(0);

   mSensorManager.registerListener(this, orientationSensor,

                              SensorManager.SENSOR_DELAY_GAME);

}

3. Añade los siguientes dos métodos que implementan la interfaz SensorEventListener:

@Override

       public void onAccuracyChanged(Sensor sensor, int accuracy){}

 

       private boolean hayValorInicial = false;

       private float valorInicial;

 

       @Override 
       
public void onSensorChanged(SensorEvent event) {

             float valor = event.values[1];

             if (!hayValorInicial){

              valorInicial = valor;

                     hayValorInicial = true;

             }

             giroNave=(int) (valor-valorInicial)/3 ;

       }

 

 

4. Prueba la aplicación. Has de tener cuidado de que el terminal este en una posición cómoda al entrar en la actividad Juego, dado que el movimiento de la nave se obtiene con la diferencia de la posición del terminal con respecto a la posición inicial.

 

Práctica: Manejo de la nave con sensor de aceleración

Modifica el ejemplo anterior para utilizar el sensor de aceleración en lugar del de orientación. Gracias a la fuerza de gravedad que la Tierra ejerce sobre el terminal podremos saber si este está horizontal. En caso de que la nave este horizontal (o casi) no ha de girar, pero cuando el terminal se incline, la nave a de girar proporcionalmente a esta inclinación. Utiliza los programas anteriores para  descubrir que eje (x, y o z) es el que te interesa y el rango de valores que proporciona.

Práctica: Aceleración de la nave con sensores

 

¿Te animarías a controlar la aceleración de la nave con los sensores? Ten cuidado de que no acelere con mucha facilidad, este juego resulta muy difícil cuando la nave está en movimiento. Puede ser una buena idea que permitas también decelerar la nave.

 

Práctica: Configuración de tipo de entrada en preferencias

 

Todos los controles de la nave (teclado, pantalla táctil y sensores) están activados simultáneamente. El teclado y la pantalla táctil no interfieren cuando el usuario no quiere utilizarlos. Sin embargo, la activación de los sensores sí que molestará a los usuarios que no quieran utilizar este método de entrada.

1.    Crea nuevas entradas en la configuración para activar o desactivar cada tipo de entrada (o al menos la de los sensores).

2.     Modifica el código anterior para que se desactiven las entradas que el usuario no haya seleccionado.


{jcomments on}

Introduciendo un misil en Asteroides

Para poder disparar a los asteroides va ha ser necesario introducir un misil en el juego. En el siguiente ejercicio aprenderemos a hacerlo:

Ejercicio paso a paso: Introduciendo un misil en Asteroides

1. En primer lugar añade las siguientes variables a la clase VistaJuego:

// //// MISIL //////

private Grafico misil;

private static int PASO_VELOCIDAD_MISIL = 12;

private boolean misilActivo = false;

private int tiempoMisil;

2. Para trabajar con gráficos vectoriales, puedes crear en el constructor la variable drawableMisil de la siguiente forma:

ShapeDrawable dMisil = new ShapeDrawable(new RectShape());

dMisil.getPaint().setColor(Color.WHITE);

dMisil.getPaint().setStyle(Style.STROKE);

dMisil.setIntrinsicWidth(15);

dMisil.setIntrinsicHeight(3);

drawableMisil = dMisil;

3. Crea la variable drawableMisil para el caso de que se deseen gráficos en bitmap, utilizando el fichero misil1.png.

4. Inicializa el objeto misil. de forma similar a como se ha hecho en nave.

5. En el método onDraw() dibuja misil, solo si lo indica la variable misilActivo.

6. Quita los comentarios de las llamadas a activaMisil()

7. En el método actualizaFisica() añade las siguientes líneas:

 

// Actualizamos posición de misil

if (misilActivo) {

       misil.incrementaPos(retardo);

       tiempoMisil-=retardo;

       if (tiempoMisil < 0) {

             misilActivo = false;

       } else {

for (int i = 0; i < Asteroides.size(); i++)

             if (misil.verificaColision(Asteroides.elementAt(i))) {

                    destruyeAsteroide(i);

                    break;

             }

       }

}

8. Añade los siguientes dos métodos:

private void destruyeAsteroide(int i) {

       Asteroides.remove(i);

       misilActivo = false;

}

 

private void ActivaMisil() {

     
       misil.setCenX(nave.getCenX();

       misil.setCenY(nave.getCenY();

       misil.setAngulo(nave.getAngulo());

       misil.setIncX(Math.cos(Math.toRadians(misil.getAngulo())) * 
                                                                                             
PASO_VELOCIDAD_MISIL);

       misil.setIncY(Math.sin(Math.toRadians(misil.getAngulo())) * 
                                                                                             
PASO_VELOCIDAD_MISIL);

       tiempoMisil = (int) Math.min(this.getWidth() / Math.abs( misil.                 getIncX()), this.getHeight() / Math.abs(misil.getIncY())) - 2;

       misilActivo = true;

}

Este último método requiere alguna explicación. Cuando se quiere activar un nuevo misil este ha de partir del centro de la nave. Como las coordenadas X, Y de Grafico, corresponden a la esquina superior izquierda hay que hacer algunos ajustes. El ángulo del misil ha de ser el mismo que actualmente tiene la nave. El módulo de la velocidad de la nave nos la indica la constante PASO_VELOCIDAD_MISIL. Para descomponerla en sus componentes X e Y utilizamos el coseno y el seno. Dada la naturaleza del espacio del juego (lo que sale por un lado aparece por el otro) si disparáramos un misil este podría acabar chocando contra la nave. Para solucionarlo vamos a dar un tiempo de vida al misil para impedir que pueda llegar de nuevo a la nave (tiempoMisil). Para obtener este tiempo nos quedamos con el mínimo entre el ancho dividido la velocidad en X y el alto dividido entre la velocidad en Y. Luego le restamos una constante. Terminamos activando el misil.

9.verifica que todo funciona correctamente.

NOTA: Este código es posible que de algún problema de acceso concurrente a los datos desde dos threads diferentes. Se resolverá en el siguiente capítulo.

Práctica: Disparando varios misiles a la vez

Tal y como se ha planteado el código solo es posible lanzar un misil cada vez. Si disparamos un segundo misil el primero desaparece.  ¿Podrías modificar el código para que se pudieran lanzar tantos misiles cómo quisieras? Si no tienes muy claro por donde empezar, a continuación se plantean los pasos para una posible solución:

1. Elimina la variable misil, y en su lugar crea una vector de gráficos:

private Vector <Grafico> Misiles;

2. Elimina la variable misilActivo. Cuando el array de misiles está vacío querá decir que no hay ningún misil activo.

3. Elimina la variable tiempoMisil, y en su lugar crea una vector de enteros:

private Vector <Integer> tiempoMisiles;

Los elementos de los arrays Misiles y tiempoMisiles han de estar emparejados. Es decir, al misil en posición x de Misiles le quedará un tiempo que se almacenará en la posición x de tiempoMisiles.

Nota sobre Java: Observa como la variable tiempoMisil antes era de tipo int, pero ahora tiempoMisiles es en vertor de Integer, no de int. Estos dos tipos de datos representan un número entero, pero el primero es una clase y el segundo un tipo simple. Hemos tenido que realizar este cambio dado que la clase Vector solo admite elementos que sean clases. Para más información consultar Apendice A: Referencia Java, Envolventes (wrappers).

4. En los métodos onDraw() y antualizaFisica() tendrás que añadir un bucle para recorrer todos los misiles.

5. Observa la siguiente línea. Permite decrementar el elemento m de tiempoMisil

 

tiempoMisiles.set(m, tiempoMisiles.get(m)-1);

 

{jcomments on}