Processing para Android

EN ES

Utilizando los Sensores

Este tutorial te ofrece los conceptos básicos para leer datos de los sensores desde Processing.

API de sensores en el SDK de Android

Los teléfonos inteligentes y las tabletas incluyen una gran variedad de sensores, como por ejemplo acelerómetros, giroscopios, magnetómetros, y termómetros. Estos sensores nos permiten obtener información sobre el movimiento, el entorno, y la posición de nuestro dispositivo.

Si bien Processing para Android no tiene funciones especializadas para leer datos de estos sensores, nosotros podemos importar los paquetes del Android SDK en nuestro bosquejo y así hacer uso de las funciones del SDK que permiten obtener los valores siendo medidos por los sensores.

Obteniendo un administrador de sensores

Como primer ejemplo, crearemos un bosquejo que responda a los cambios en la velocidad del movimiento del dispositivo, por lo que utilizaremos los datos del acelerómetro. El primer paso para usar la API del sensor es obtener el contexto de la actividad que contiene el bosquejo, y una vez hecho eso, obtener un administrador de sensores de este contexto. Con el administrado de sensores podemos inicializar el sensor particular que necesitamos en nuestro bosquejo, en este caso un acelerómetro. Todo esto lo hacemos en la función setup():


import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;

Context context;
SensorManager manager;
Sensor sensor;

void setup() {
  fullScreen();
  
  context = getActivity();
  manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
  sensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}

void draw() {
}

Agregando el oyente del sensor

Ahora debemos agregar una clase de "oyente" que notificará al bosquejo que los nuevos datos están disponibles desde el sensor. Un oyente para cada sensor se deriva de la base clase SensorEventListener en el Android SDK. Una vez que tengamos una instancia de oyente en el bosquejo, podemos registrar la escucha con el administrador:


import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;

Context context;
SensorManager manager;
Sensor sensor;
AccelerometerListener listener;

void setup() {
  fullScreen();
  
  context = getActivity();
  manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
  sensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  listener = new AccelerometerListener();
  manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME);
}

void draw() {
}

class AccelerometerListener implements SensorEventListener {
  public void onSensorChanged(SensorEvent event) {
  }
  public void onAccuracyChanged(Sensor sensor, int accuracy) {
  }
}

Probablemente hayas notado el argumento SensorManager.SENSOR_DELAY_GAME en el registro del oyente. Este argumento establece la velocidad a la que see leen nuevos datos del sensor. Las velocidades más rápidas significan una mayor capacidad de respuesta, pero también un mayor consumo de batería. Hay varias constantes de velocidad predefinidas además de SENSOR_DELAY_GAME, también podemos usar SENSOR_DELAY_FASTEST, SENSOR_DELAY_NORMAL, y SENSOR_DELAY_UI.

Leyendo los datos del sensor

El detector de eventos tiene dos métodos, onSensorChanged() y onAccuracyChanged(). Solo necesitamos usar onSensorChanged() para obtener los datos del sensor. En el caso del acelerómetro, los datos consisten en tres números decimales, que representan la aceleración a lo largo de los ejes X, Y y Z del dispositivo, que están definidos de la siguiente manera:

Device axis

Como primera prueba de nuestro bosquejo, podemos simplemente imprimir estos valores en la pantalla y verificar que si colocamos el teléfono plano sobre la mesa con la pantalla hacia arriba, vemos una aceleración Z de 9.81 m/s2, correspondiente al fuerza de gravedad .


import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;

Context context;
SensorManager manager;
Sensor sensor;
AccelerometerListener listener;
float ax, ay, az;

void setup() {
  fullScreen();
  
  context = getActivity();
  manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
  sensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  listener = new AccelerometerListener();
  manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME);
  
  textFont(createFont("SansSerif", 30 * displayDensity));
}

void draw() {
  background(0);
  text("X: " + ax + "\nY: " + ay + "\nZ: " + az, 0, 0, width, height);
}

class AccelerometerListener implements SensorEventListener {
  public void onSensorChanged(SensorEvent event) {
    ax = event.values[0];
    ay = event.values[1];
    az = event.values[2];    
  }
  public void onAccuracyChanged(Sensor sensor, int accuracy) {
  }
}
Aquí hacemos uso del factor DisplayDensity utilizado para escalar el tamaño de fuente, que el modo Android obtiene de DisplayMetrics.density en el SDK de Android. Este factor es 1 en una pantalla de 160ppp (por ejemplo, una pantalla de 240x320, 1.5" screen) y nos permite ajustar el tamaño de los elementos gráficos en nuestro bosquejo, como el texto, proporcionalmente al ppp.

Google lista un número de prácticas recomendadas para usar sensores, entre ellas el de-registrar el oyente cuando la actividad del bosquejo está en pausa para reducir el uso de la batería, y luego volver a registrarlo cuando se reanude la actividad. Podemos hacer esto en Processing agregando el siguiente código a nuestro bosquejo:


public void onResume() {
  super.onResume();
  if (manager != null) {
    manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME);
  }
}

public void onPause() {
  super.onPause();
  if (manager != null) {
    manager.unregisterListener(listener);
  }
}

Necesitamos verificar que la instancia de administrador no sea nula porque las funciones onPause() o onResume() pueden se llamadas antes de la función setup() en nuestro bosquejo.

Un ejemplo más complejo usando Box2D

Un uso posible de los datos de los sensores es controlar el movimiento de los elementos gráficos en nuestro bosquejo. Los valores del acelerómetro representan directamente la fuerza a la que está sujeto el dispositivo, por lo que si los usamos para controlar el comportamiento de, digamos, un sistema de cuerpo rígido, podríamos coordinar los comportamientos generados con el código con movimientos en el espacio real.

Para lograr este objetivo, utilizaremos la biblioteca Box2D para Processingo de Daniel Shiffman. Aunque esta biblioteca no fue creada específicamente para Android, se puede usar desde bosquejos en el modo Android ya que está escrita en Java y no hace uso de ninguna funcionalidad específica de computadoras Mac o PC.

Lo que haremos a continuación es combinar el código básico de lectura de los valores del acelerómetro del ejemplo anterior con una simulación simple de cuerpos rígidos hecha Box2D donde los bordes de la pantalla definen los límites de la simulación, y una número de rectángulos rebotan entre ellos bajo la aceleración medida por el dispositivo. El código completo del ejemplo está disponible aquí.

Sin considerar los datos del acelerómetro, podemos escribir el código de Box2D de la siguiente manera (la implementación de las clases de Box y Wall no está incluida en el listado que sigue, pero el código completo del ejemplo está en GitHub):


import shiffman.box2d.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.*;

Box2DProcessing box2d;
ArrayList<Box> boxes;
ArrayList<Wall> walls;

void setup() {
  fullScreen(P2D);
  orientation(PORTRAIT);
  
  box2d = new Box2DProcessing(this);
  box2d.createWorld();
  box2d.setGravity(0, -10);
  
  // A group of boxes
  boxes = new ArrayList<Box>();
  for (int i = 0; i < 20; i++) {
    Box p = new Box(random(200, width-200), random(200, height-200));
    boxes.add(p);
  }
  
  // Invisible walls
  walls = new ArrayList<Wall>();
  walls.add(new Wall(width/2, -25, width, 50));
  walls.add(new Wall(width/2, height+25, width, 50));
  walls.add(new Wall(-25, height/2, 50, height));
  walls.add(new Wall(width+25, height/2, 50, height));
}

void draw() {
  background(255);  
  
  box2d.step();
  
  for (Box b: boxes) {
    b.display();
  }
}

void mousePressed() {
  for (Box b: boxes) {
    b.shake();
  }   
}

En este código usamos la aceleración constante a lo largo de Y de (0, -10), pero ahora podemos vincular la fuerza gravitacional en Box2D a los valores de aceleración del sensor haciendo:


  box2d.setGravity(-ax, -ay);

Por lo tanto, el código final para el bosquejo que combina Box2D con los datos del acelerómetro sería algo así:


import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;

import shiffman.box2d.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.*;

Context context;
SensorManager manager;
Sensor sensor;
AccelerometerListener listener;
float ax, ay, az;

Box2DProcessing box2d;
ArrayList<Box> boxes;
ArrayList<Wall> walls;

void setup() {
  fullScreen(P2D);
  orientation(PORTRAIT);
  
  context = getActivity();
  manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
  sensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  listener = new AccelerometerListener();
  manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
  
  box2d = new Box2DProcessing(this);
  box2d.createWorld();
  
  // A group of boxes
  boxes = new ArrayList<Box>();
  for (int i = 0; i < 20; i++) {
    Box p = new Box(random(200, width-200), random(200, height-200));
    boxes.add(p);
  }
  
  // Invisible walls
  walls = new ArrayList<Wall>();
  walls.add(new Wall(width/2, -25, width, 50));
  walls.add(new Wall(width/2, height+25, width, 50));
  walls.add(new Wall(-25, height/2, 50, height));
  walls.add(new Wall(width+25, height/2, 50, height));
}

void draw() {
  background(255);  
  
  // update gravity from accelerometer data.
  box2d.setGravity(-ax, -ay);
  
  box2d.step();
  
  for (Box b: boxes) {
    b.display();
  }
}

void mousePressed() {
  for (Box b: boxes) {
    b.shake();
  }   
}

public void onResume() {
  super.onResume();
  if (manager != null) {
    manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME);
  }
}

public void onPause() {
  super.onPause();
  if (manager != null) {
    manager.unregisterListener(listener);
  }
}

class AccelerometerListener implements SensorEventListener {
  public void onSensorChanged(SensorEvent event) {
    ax = event.values[0];
    ay = event.values[1];
    az = event.values[2];    
  }
  public void onAccuracyChanged(Sensor sensor, int accuracy) {
  }
}

Usando otros sensores

La documentación para desarrolladores de Google proporciona información detallada sobre cómo obtener datos de otros sensores de movimiento.

Como alternativa, también sep uede usar la biblioteca Ketai de Daniel Sauter y Jesus Duran, que ofrece una interfaze más simple a la API de sensores Android y nos permite obtener valores de diferentes sensores con mucha facilidad. Ketai se puede instalar a través del administrador de contribuciones (CM) en el PDE.