Bitácora de Javier Cancela

Entradas etiquetadas ‘desarrollo móvil

Pasado, presente y futuro de la realidad aumentada

Escrito por Javier Cancela

16 \16\UTC noviembre \16\UTC 2009 a 20:00

Escrito en Tecnología

Etiquetado con ,

Realidad aumentada en Android: AR Compass – III

La entrada anterior se centró la clase Compass, que describe la brújula. También presentaba la clase CompassRenderer, que es la encargada de dibujarla en su sitio. Vamos a ver esta clase con más detalle.

La clase CompassRenderer

   1: class CompassRenderer implements GLSurfaceView.Renderer, SensorEventListener {
   2:         private float   mAccelerometerValues[] = new float[3];
   3:         private float   mMagneticValues[] = new float[3];
   4:         private float rotationMatrix[] = new float[16];
   5:         private float remappedRotationMatrix[] = new float[16];
   6:
   7:         private Compass mCompass;
   8:
   9:     public CompassRenderer() {
  10:         mCompass = new Compass();
  11:     }

La clase CompassRenderer cumple dos funciones: capturar la información de los sensores y renderizar la brújula con esa información.

Implementación de la interfaz SensorEventListener

Como hemos visto en otras entradas, el método onSensorChanged es invocado cada vez que hay nueva información disponible de alguno de los sensores a los que nos hemos subscrito:

   1: @Override
   2: public void onSensorChanged(SensorEvent event) {
   3:        synchronized (this) {
   4:             switch(event.sensor.getType()) {
   5:             case Sensor.TYPE_ACCELEROMETER:
   6:                 mAccelerometerValues = event.values.clone();
   7:                 break;
   8:             case Sensor.TYPE_MAGNETIC_FIELD:
   9:                 mMagneticValues = event.values.clone();
  10:                 break;
  11:             default:
  12:                 break;
  13:             }
  14:        }
  15: }

Para obtener la matriz de rotación (como veremos después) sólo necesitamos el acelerómetro y el sensor de campo magnético.

Un punto a tener en cuenta en este caso es la sincronización. Al código de la clase CompassRenderer van a acceder dos threads: el principal y thread de OpenGL. Las llamadas a los métodos de los sensores vendrán del primero, mientras que las llamadas a los métodos de renderización vendrán del segundo. Esto quiere decir que las variables miembro mAccelerometerValues y mMagneticValues serán asignadas en un thread y leídas en el otro, por lo que usamos un lock para evitar que se lean mientras se están asignando.

Creación de la superficie OpenGL

   1: public void onSurfaceCreated(GL10 gl, EGLConfig config) {
   2:     /*
   3:      * By default, OpenGL enables features that improve quality
   4:      * but reduce performance. One might want to tweak that
   5:      * especially on software renderer.
   6:      */
   7:     gl.glDisable(GL10.GL_DITHER);
   8:
   9:     /*
  10:      * Some one-time OpenGL initialization can be made here
  11:      * probably based on features of this particular context
  12:      */
  13:      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
  14:              GL10.GL_FASTEST);
  15:
  16:      gl.glClearColor(0,0,0,0);
  17: }

Cuando el GLThread crea la superficie ejecuta el método onSurfaceCreated. Las líneas 7 y 13 son optimizaciones que le indican a OpenGL que optimice el rendimiento sobre la calidad de la imagen. La línea 16 establece los valores que se utilizarán para limpiar el buffer de color.

   1: public void onSurfaceChanged(GL10 gl, int width, int height) {
   2:      gl.glViewport(0, 0, width, height);
   3:
   4:      /*
   5:       * Set our projection matrix. This doesn't have to be done
   6:       * each time we draw, but usually a new projection needs to
   7:       * be set when the viewport is resized.
   8:       */
   9:      float ratio = (float) width / height;
  10:      gl.glMatrixMode(GL10.GL_PROJECTION);
  11:      gl.glLoadIdentity();
  12:      gl.glFrustumf(-ratio, ratio, -1, 1, 1, 100);
  13: }

El método onSurfaceChanged después de creada la superficie, y adicionalmente cada vez que esta cambia de tamaño. En la línea 2 establecemos el viewport. Básicamente le decimos a sistema que utilice todo el alto y ancho de la pantalla para pintar el modelo. Las líneas 9-12 inicializan la matriz de proyección y establece la región  que se va a visualizar (ver por ejemplo este tutorial).

Dibujo de la brújula

Cada cuadro correspondiente a la animación de la brújula se dibuja en el método onDrawFrame:

   1: public void onDrawFrame(GL10 gl) {
   2:     // Get rotation matrix from the sensor
   3:     SensorManager.getRotationMatrix(rotationMatrix, null, mAccelerometerValues,
                                          mMagneticValues);
   4:     // As the documentation says, we are using the device as a compass in landscape 
          // mode
   5:     SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_Y,
                                              SensorManager.AXIS_MINUS_X,
                                              remappedRotationMatrix);
   6:
   7:     // Clear color buffer
   8:     gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
   9:
  10:     // Load remapped matrix
  11:     gl.glMatrixMode(GL10.GL_MODELVIEW);
  12:     gl.glLoadIdentity();
  13:     gl.glLoadMatrixf(remappedRotationMatrix, 0);
  14:
  15:     gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
  16:     gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
  17:
  18:     mCompass.draw(gl);
  19: }

En la línea 3 obtenemos la matriz de rotación a partir de los sensores. Como vamos a ver la imagen en formato horizontal giramos los ejes en la línea 5. Las líneas 11-13 cargan la matriz. Después habilitamos los arrays de vértices y colores en el cliente que como vimos usa la clase Compass, y finalmente llamamos al método de dibujo de la brújula (visto en la entrada anterior).

El código completo del proyecto está disponible aquí: ARCompass.

Escrito por Javier Cancela

17 \17\UTC agosto \17\UTC 2009 a 7:00

Realidad aumentada en Android: AR Compass – II

Una vez obtenida la imagen de la cámara pasamos a la brújula. Necesitaremos dos cosas: obtener los datos de orientación y dibujarlos en el lugar adecuado. Empezaremos inicializando los sensores y los datos iniciales de la brújula.

Empezando con OpenGL

Como dijimos en la entrada anterior vamos a usar OpenGL para dibujar la brújula. Una advertencia: antes de realizar esta serie de entradas no sabía nada de OpenGL, así que es probable es que el código que incluyo no sea muy ortodoxo, incluso puede que contenga errores. Aún así funciona correctamente en el HTC Magic.

Para aprender algo de OpenGL utilicé el libro “The OpenGL Programming Guide 5th Edition. The Official Guide to Learning OpenGL Version 2.1, también conocido como el OpenGL Red Book. Por supuesto hay gran cantidad de tutoriales de OpenGL por internet. Los ejemplos de Android sobre OpenGL|ES permiten hacerse una idea de cómo usar OpenGL en Android. Algunas de las clases que aparecen en esta entrada están basadas en esos ejemplos.

Lo primero que necesitamos para trabajar con OpenGL es una superficie sobre la que pintar. La SDK incluye una diseñada para OpenGL: GLSurfaceView, un tipo especial de SurfaceView cuya principal característica es que ejecuta el código de renderizado en un thread aparte (llamado GLThread). Para ello, a la vista GLSurfaceView se le asigna un renderizador, que es una clase que hereda de GLSurfaceView.Renderer:

   1: class CompassRenderer implements GLSurfaceView.Renderer, SensorEventListener {

En nuestro caso, dado que esta clase va a renderizar la brújula en base a los datos de los sensores, haremos que implemente también SensorEventListener (como veíamos en esta entrada), para tener los datos en la misma clase.

Pero primero tenemos que decirle a nuestra actividad que use esta vista además de la vista de la cámara:

   1: public class ARCompass extends Activity {
   2:     private SensorManager mSensorManager;
   3:     private CameraView mCameraView;
   4:     private GLSurfaceView mGLSurfaceView;
   5:
   6:
   7:     /** Called when the activity is first created. */
   8:     @Override
   9:     public void onCreate(Bundle savedInstanceState) {
  10:         super.onCreate(savedInstanceState);
  11:
  12:         // Hide the window title.
  13:         requestWindowFeature(Window.FEATURE_NO_TITLE);
  14:
  15:         mGLSurfaceView = new GLSurfaceView(this);
  16:         mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
  17:         CompassRenderer compassRenderer = new CompassRenderer(true);
  18:         mGLSurfaceView.setRenderer(compassRenderer);
  19:         mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
  20:
  21:         setContentView(mGLSurfaceView);
  22:
  23:         mCameraView = new CameraView(this);
  24:         addContentView(mCameraView, new LayoutParams(LayoutParams.WRAP_CONTENT,
                                                         LayoutParams.WRAP_CONTENT));
  25:
  26:         mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  27:
  28:         List<Sensor> listSensors = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
  29:         if (listSensors.size() > 0)
  30:         {
  31:             mSensorManager.registerListener(compassRenderer, listSensors.get(0),
                                                   SensorManager.SENSOR_DELAY_UI);
  32:         }
  33:
  34:         listSensors = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);
  35:         if (listSensors.size() > 0)
  36:         {
  37:             mSensorManager.registerListener(compassRenderer, listSensors.get(0),
                                                                   SensorManager.SENSOR_DELAY_UI);
  38:         }
  39:     }
  40:  ...
  41: }

En las líneas 15-19 creamos la superficie OpenGL, establecemos los componentes de color RGB a 8 bits con profundidad de buffer a 60 bits, creamos nuestra clase CompassRenderer (que veremos después), le decimos a la superficie que vamos a renderizar en la clase CompassRenderer y finalmente establecemos en la superficie un formato de pixel que soporte varios de canal alfa. Este último paso es necesario para que la superficie sea transparente y poder ver lo que hay debajo, que será la imagen de la cámara.

En las líneas 21-24 asociamos la vista OpenGL a la actividad, creamos la vista de la cámara y la añadimos. La vista de la cámara queda así detrás de la vista OpenGL, visible porque esta última tiene una superficie transparente.

El resto del código registra los Listeners del acelerómetro y del sensor de campo magnético.

La clase Compass

La clase Compass le indica al renderizador las características del objeto a renderizar. Contiene un método draw que será llamado por el renderizador cada vez que quiera dibujar un frame:

   1: class Compass
   2: {
   3:     private FloatBuffer   mVertexBuffer;
   4:     private IntBuffer   mColorBuffer;
   5:     private ByteBuffer  mIndexBuffer;
   6:
   7:     public Compass()
   8:     {
   9:          ...
  10:     }
  11:
  12:     public void draw(GL10 gl)
  13:     {
  14:         gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
  15:         gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
  16:         gl.glDrawElements(GL10.GL_LINES, 32 + 6 + 10 + 8 + 8, GL10.GL_UNSIGNED_BYTE,
                                mIndexBuffer);
  17:     }
  18: }

La clase contiene tres buffers: uno con los vértices de cada primitiva a dibujar (en nuestro caso las primitivas son líneas), otro con los colores, y el último con el orden en el que se van a aplicar cada uno de los vértices. Los buffers se asignan en el constructor, como veremos después, y contienen una descripción de la posición y el color del modelo. En nuestro caso el modelo es una serie de líneas verticales y cuatro letras que se distribuyen en una circunferencia en el plano XY alrededor del punto (0, 0, 0).

La línea 14 define el array de vértices a partir de nuestro buffer. Son vértices de tres coordenadas, de tipo float, sin stride (desplazamiento entre vértices consecutivos, usado para empaquetar información de cada vértice, ver este artículo para más información). La línea 15 define el array de colores. Y la línea 16 renderiza las primitivas: indicamos el tipo de primitivas (en este caso líneas) y el número de elementos a renderizar (en este caso vértices, 32 de la brújula, 6, 10, 8 y 8 para cada letra).

Veamos ahora el constructor:

   1: public Compass()
   2:  {
   3:      int one = 0x10000;
   4:
   5:      int colorLines[] = {
   6:              0,  one,    0,  one,
   7:              0,  one,    0,  one,
   8:        };
   9:
  10:      int colorLetters[] = {
  11:              //North
  12:              one,  one,    0,  one,
  13:              one,  one,    0,  one,
  14:              one,  one,    0,  one,
  15:              one,  one,    0,  one,
  16:              one,  one,    0,  one,
  17:              one,  one,    0,  one,
  18:              // South
  19:              one,  one,    0,  one,
  20:              one,  one,    0,  one,
  21:              one,  one,    0,  one,
  22:              one,  one,    0,  one,
  23:              one,  one,    0,  one,
  24:              one,  one,    0,  one,
  25:              one,  one,    0,  one,
  26:              one,  one,    0,  one,
  27:              one,  one,    0,  one,
  28:              one,  one,    0,  one,
  29:              // East
  30:              one,  one,    0,  one,
  31:              one,  one,    0,  one,
  32:              one,  one,    0,  one,
  33:              one,  one,    0,  one,
  34:              one,  one,    0,  one,
  35:              one,  one,    0,  one,
  36:              one,  one,    0,  one,
  37:              one,  one,    0,  one,
  38:              // West
  39:              one,  one,    0,  one,
  40:              one,  one,    0,  one,
  41:              one,  one,    0,  one,
  42:              one,  one,    0,  one,
  43:              one,  one,    0,  one,
  44:              one,  one,    0,  one,
  45:              one,  one,    0,  one,
  46:              one,  one,    0,  one,
  47:      };
  48:
  49:      // Buffers to be passed to gl*Pointer() functions
  50:      // must be direct, i.e., they must be placed on the
  51:      // native heap where the garbage collector cannot
  52:      // move them.
  53:      //
  54:      // Buffers with multi-byte datatypes (e.g., short, int, float)
  55:      // must have their byte order set to native order
  56:
  57:      // (( vertices_per_compass_line * coords_per_vertex * lines_number) 
  58:      // + north_vertices * coords_per_vertex + south_vertices * coords_per_vertex 
  59:      // + east_vertices * coords_per_vertex + west_vertices * coords_per_vertex) 
  60:      // * bytes_per_float
  61:      ByteBuffer vbb = ByteBuffer.allocateDirect(((2 * 3 * 16) + (6 * 3) + (10 * 3) +
                                                       (8 * 3) + (8 * 3)) * 4);
  62:      vbb.order(ByteOrder.nativeOrder());
  63:      mVertexBuffer = vbb.asFloatBuffer();
  64:
  65:      // ((total_compass_vertices * coords_per_color) + 
  66:      // (north_vertices * coords_per_color)  + (south_vertices * coords_per_color))
  67:      // * bytes_per_int
  68:      ByteBuffer cbb = ByteBuffer.allocateDirect(((32 * 4) + (6 * 4) + (10 * 4) +
                                                        (8 * 4) + (8 * 4)) * 4);
  69:      cbb.order(ByteOrder.nativeOrder());
  70:      mColorBuffer = cbb.asIntBuffer();
  71:
  72:      mIndexBuffer = ByteBuffer.allocateDirect(32 + 6 + 10 + 8 + 8);
  73:      float x;
  74:      float y;
  75:      float z;
  76:      for (int i = 0; i < 16; i++)
  77:      {
  78:          if (i % 2 == 0)
  79:              if (i % 4 == 0)
  80:                  z = 6.0f;
  81:              else
  82:                  z = 4.0f;
  83:          else
  84:              z = 2.0f;
  85:
  86:          x = (float)(Math.sin(((double)i / 16) * 2 * Math.PI) * 32);
  87:          y = (float)(Math.cos(((double)i / 16) * 2 * Math.PI) * 32);
  88:          mVertexBuffer.put(x);
  89:          mVertexBuffer.put(y);
  90:          mVertexBuffer.put(-z);
  91:          mIndexBuffer.put((byte)(2 * i));
  92:
  93:          mVertexBuffer.put(x);
  94:          mVertexBuffer.put(y);
  95:          mVertexBuffer.put(z);
  96:          mIndexBuffer.put((byte)(2 * i + 1));
  97:
  98:          mColorBuffer.put(colorLines);
  99:      }
 100:
 101:      float north[] = {
 102:          -2.0f, 32.0f, 7.0f,
 103:          -2.0f, 32.0f, 11.0f,
 104:          -2.0f, 32.0f, 11.0f,
 105:          2.0f, 32.0f, 7.0f,
 106:          2.0f, 32.0f, 7.0f,
 107:          2.0f, 32.0f, 11.0f,
 108:      };
 109:      mVertexBuffer.put(north);
 110:      byte indices[] = {
 111:              32, 33, 34, 35, 36, 37,
 112:      };
 113:      mIndexBuffer.put(indices);
 114:
 115:      float south[] = {
 116:              2.0f, -32.0f, 7.0f,
 117:              -2.0f, -32.0f, 7.0f,
 118:              -2.0f, -32.0f, 7.0f,
 119:              -2.0f, -32.0f, 9.0f,
 120:              -2.0f, -32.0f, 9.0f,
 121:              2.0f, -32.0f, 9.0f,
 122:              2.0f, -32.0f, 9.0f,
 123:              2.0f, -32.0f, 11.0f,
 124:              2.0f, -32.0f, 11.0f,
 125:              -2.0f, -32.0f, 11.0f,
 126:      };
 127:      mVertexBuffer.put(south);
 128:      indices = new byte[]{
 129:              38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
 130:      };
 131:      mIndexBuffer.put(indices);
 132:
 133:      float east[] = {
 134:              32.0f, -2.0f, 7.0f,
 135:              32.0f, 2.0f, 7.0f,
 136:              32.0f, -2.0f, 9.0f,
 137:              32.0f, 2.0f, 9.0f,
 138:              32.0f, -2.0f, 11.0f,
 139:              32.0f, 2.0f, 11.0f,
 140:              32.0f, 2.0f, 7.0f,
 141:              32.0f, 2.0f, 11.0f,
 142:      };
 143:      mVertexBuffer.put(east);
 144:      indices = new byte[]{
 145:              48, 49, 50, 51, 52, 53, 54, 55,
 146:      };
 147:      mIndexBuffer.put(indices);
 148:
 149:      float west[] = {
 150:              -32.0f, 2.0f, 11.0f,
 151:              -32.0f, 1.0f, 7.0f,
 152:              -32.0f, 1.0f, 7.0f,
 153:              -32.0f, 0, 9.0f,
 154:              -32.0f, 0, 9.0f,
 155:              -32.0f, -1.0f, 7.0f,
 156:              -32.0f, -1.0f, 7.0f,
 157:              -32.0f, -2.0f, 11.0f,
 158:      };
 159:      mVertexBuffer.put(west);
 160:      indices = new byte[]{
 161:              56, 57, 58, 59, 60, 61, 62, 63,
 162:      };
 163:      mIndexBuffer.put(indices);
 164:
 165:      mColorBuffer.put(colorLetters);
 166:
 167:      mColorBuffer.position(0);
 168:      mVertexBuffer.position(0);
 169:      mIndexBuffer.position(0);
 170:  }

Esta parte contiene código aparentemente ilógico, como arrays cuyos valores se asignan a mano en vez de en bucle. La idea es que se vea con más claridad de dónde sale cada cosa y para qué se usa.

La variable colorLines contiene los colores de los dos vértices de una línea (en este caso verde, para una explicación del color ver esta sección del libro rojo). Como vamos a crear cada línea en un bucle usaremos siempre ese array. La variable colorLetters contiene el color (amarillo) de cada vértice de cada línea de cada letra. Vamos a asignar los vértices de las letras una a una, así que esta variable contiene un color por vértice.

Después creamos los buffers, teniendo cuidado a la hora de dar el tamaño correcto en bytes de cada array.

En las líneas 76-99 vemos el bucle que crea cada línea de la brújula. Imaginamos un círculo de radio 32 en el plano XY. Las líneas (un total de 16) serán verticales a ese plano, con longitudes de 12 para las correspondientes a N, S, E y O, 8 para las que indiquen NO, NE, SE y SO, y 4 para las demás. La coordenada z se asigna al principio. Después se calculan x e y en función del seno y el coseno del ángulo (dividimos 2*PI en 16 porciones y vamos haciendo las cuentas).  En las líneas 88-96 asignan cada vértice, donde se puede ver que para cada línea de la brújula solo varía la coordenada z.

La variable mIndexBuffer contiene un array de bytes que le indacarán al renderizador en qué orden se deben procesar los vértices (en este array se puede indicar, por ejemplo, que se repitan vértices ya utilizados). El bucle acaba añadiendo los datos de color para cada vértice.

El resto del constructor es más de lo mismo, pero para las letras. Se dibujan las letras N, S, W, E con líneas, de una forma muy simple, y además utilizando como primitivas líneas sueltas, en vez de líneas continuas (se podía haber usado la primitiva GL_LINE_STRIP para no repetir los vértices que son comunes a más de una línea).

En la siguiente y última entrada, veremos cómo mostrar todo esto en pantalla.

Escrito por Javier Cancela

10 \10\UTC agosto \10\UTC 2009 a 7:00

Realidad aumentada en Android: AR Compass – I

Decíamos en el anterior artículo que la realidad aumentada combina datos generados por ordenador con imágenes del entorno obtenidas en tiempo real. La parte complicada es la superposición de los datos sobre la imagen en la posición correcta. Así que vamos a comenzar con una aplicación de realidad levemente aumentada: una brújula virtual.

AR CompassMi pantalla apunta al noroeste

AR Compass (brújula de realidad aumentada, en inglés queda más cool) es una aplicación para Android que muestra una brújula superpuesta a la imagen capturada por la cámara. La brújula consiste en una serie de líneas verticales con el norte, el sur, el este y el oeste marcados.

La aplicación usa OpenGL para mostrar los puntos cardinales sobre la imagen de la cámara. A partir de los datos del acelerómetro y del magnetómetro obtenemos la matriz de rotación que nos permite mostrar la dirección correcta en la pantalla.

La imagen de la cámara

Es código para mostrar la imagen de la cámara es bastante simple, y se puede sacar del ejemplo CameraPreview:

   1: public class CameraPreview extends Activity {
   2:     private Preview mPreview;
   3:
   4:     @Override
   5:     protected void onCreate(Bundle savedInstanceState) {
   6:         super.onCreate(savedInstanceState);
   7:
   8:         // Hide the window title.
   9:         requestWindowFeature(Window.FEATURE_NO_TITLE);
  10:
  11:         // Create our Preview view and set it as the content of our activity.
  12:         mPreview = new Preview(this);
  13:         setContentView(mPreview);
  14:     }
  15:
  16: }

En la línea 9 indicamos que no queremos que la ventana tenga titulo. Después simplemente instanciamos la vista y se la asignamos a la actividad. La vista es la clase Preview, que hereda de SurfaceView. SurfaceView es un tipo de vista que se caracteriza por contener una superficie (un objeto Surface) sobre la que dibujar.

   1: class Preview extends SurfaceView implements SurfaceHolder.Callback {
   2:     SurfaceHolder mHolder;
   3:     Camera mCamera;
   4:
   5:     Preview(Context context) {
   6:         super(context);
   7:
   8:         // Install a SurfaceHolder.Callback so we get notified when the
   9:         // underlying surface is created and destroyed.
  10:         mHolder = getHolder();
  11:         mHolder.addCallback(this);
  12:         mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  13:     }
  14: ...
  15: }

Además de heredar de SurfaceView, la clase Preview implementa la interfaz SurfaceHolder.Callback. La clase SurfaceHolder es el contendor que nos da acceso a la superficie, la cual no se suele usar directamente. A través de los métodos de esta interfaz nos enteraremos de cuando se crea, destruye o cambia de tamaño la superficie.

Las líneas 10 y 11 sirven para indicarle al SurfaceHolder que notifique los cambios de la superficie a nuestra instancia. La línea 12 especifica el tipo de superficie a usar, en este caso una que no posee sus propios buffers. No tengo muy claro qué significa esto, pero imagino que el resto de los tipos de superficie tendrán uno o dos buffers para renderizar la imagen, mientras que este tipo necesita que le propercionen los buffers con la imagen a buscar. En cualquier caso es el único tipo con el que funciona la cámara.

   1: public void surfaceCreated(SurfaceHolder holder) {
   2:     // The Surface has been created, acquire the camera and tell it where
   3:     // to draw.
   4:     mCamera = Camera.open();
   5:     try {
   6:        mCamera.setPreviewDisplay(holder);
   7:     } catch (IOException exception) {
   8:         mCamera.release();
   9:         mCamera = null;
  10:         // TODO: add more exception handling logic here
  11:     }
  12: }
  13:
  14: public void surfaceDestroyed(SurfaceHolder holder) {
  15:     // Surface will be destroyed when we return, so stop the preview.
  16:     // Because the CameraDevice object is not a shared resource, it's very
  17:     // important to release it when the activity is paused.
  18:     mCamera.stopPreview();
  19:     mCamera.release();
  20:     mCamera = null;
  21: }
  22:
  23: public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
  24:     // Now that the size is known, set up the camera parameters and begin
  25:     // the preview.
  26:     Camera.Parameters parameters = mCamera.getParameters();
  27:     parameters.setPreviewSize(w, h);
  28:     mCamera.setParameters(parameters);
  29:     mCamera.startPreview();
  30: }

En estos tres métodos se notifican los eventos relacionados con la superficie donde mostramos la imagen de la cámara. El primero de ellos es surfaceCreated. El bucle de nuestra aplicación recibe un mensaje indicándole que la superficie ha sido creada, e invoca este método pasándole el SurfaceHolder de la misma. En la línea 4 “abrimos” la cámara  y en la 6 le decimos que muestre la imagen de previsualización usando la superficie asociada al holder. Si hay algún problema liberamos la cámara.

Cuando la superficie se destruye (por ejemplo, porque se va a iniciar otra actividad) detenemos la previsualización y liberamos la cámara. El ejemplo de la SDK no incluye la llamada a release(), lo que provoca excepciones Out of memory cuando la ventana se destruye y posteriormente se quiere volver a usar la cámara.

El método surfaceChanged se invoca si la superficie cambia de tamaño (si la pantalla gira, por ejemplo). En nuestro ejemplo no se usará, porque vamos a fijar la ventana de la actividad a formato landscape.

Para usar la cámara necesitaremos los permisos adecuados, en este caso <uses-permission android:name=”android.permission.CAMERA” />. Precisamente la última revisión de la SDK corrige un error por el cual no se forzaba correctamente este permiso.

En la próxima entrada comenzaremos a usar OpenGL para añadir la brújula sobre la cámara.

Escrito por Javier Cancela

4 \04\UTC agosto \04\UTC 2009 a 7:00

Realidad aumentada en Android – Introducción

¿Qué es la realidad aumentada?
Handheld Augmented Reality

Buscando descuentos con el móvil

Un sistema de realidad aumentada es aquel que combina datos generados por ordenador con imágenes del entorno obtenidas en tiempo real. Un sistema básico de realidad aumentada consiste en dispositivo compuesto por una cámara, una fuente de datos y una pantalla donde se muestra la imagen capturada por la cámara con los datos superpuestos. Los datos que aparecen dependerán de la posición y orientación de la cámara, o bien de la presencia en la imagen (y por lo tanto en la escena real) de marcadores que indiquen la información a mostrar.

Dos ejemplos para que quede claro de qué estamos hablando:
¿Cómo funciona la realidad aumentada?
Un sistema de realidad aumentada proporciona información contextual, donde el contexto es el mundo que nos rodea. Añade datos de interés a lo que podemos apreciar a simple vista,  superponiéndolos en la pantalla. No es una ideanueva, pero ha adquirido relevancia en los últimos meses gracias a la aparición de los móviles de última generación, que poseen las características necesarias para hacer funcionar aplicaciones basadas en esta idea:

Los seis marcadores de la mesa se conviernte en figuras en la pantalla

  • Una cámara. No es necesario que grabe vídeo, basta con que la pantalla del móvil sirva como visor.
  • Una interfaz de programación que permita acceder a la imagen proporcionada por la cámara.
Estas dos características bastaría para desarrollar una aplicación de realidad aumentada basada en marcadores. Solo tendríamos que identificar y procesar estos marcadores para sustituirlos por la información apropiada. Sin embargo, dos sensores cada vez más habituales en los dispositivos móviles nos permiten ampliar las posibilidades:
  • El GPS.
  • El sensor de orientación

La Acrópolis restaurada virtualmente

Imaginemos que queremos diseñar una aplicación de realidad virtual que identifique edificios y lugares de interés turístico, mostrando sobre los mismos su nombre, fecha de construcción, horarios de visita… En este caso se plantea el problema técnico de mostrar esta información sobre la parte de la pantalla donde se esté viendo el lugar correspondiente. Para ello necesitamos que la información esté geoposicionada (disponer de las coordenadas del lugar), y necesitamos una forma de relacionar estas coordenadas con la posición y la orientación de la cámara, las cuales obtendremos de los sensores del dispositivo.

Esta última parte es la más complicada desde un punto del desarrollador. En los próximos artículos vamos a ver una serie de ejemplos básicos, con código, para hacernos una idea de cómo se podría desarrollar una aplicación de este tipo.

Escrito por Javier Cancela

3 \03\UTC agosto \03\UTC 2009 a 8:00

Escrito en Android

Etiquetado con , ,

Desarrollo en Android: acelerómetro, magnetómetro y sensores de orientación y temperatura en el HTC Magic

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

La clase Sensor acepta ocho tipos de sensores, como se puede ver en la referencia. Los sensores disponibles varían en función del aparato utilizado.

Listar los sensores del dispositivo

Para ver de qué sensores dispone nuestro dispositivo usamos la clase SensorManager:

   1: // Solicitamos al sistema el servicio que gestiona los sensores
   2: SensorManager mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
   3: // Peimos la lista con todos los sensores disponibles
   4: List<Sensor> listSensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
   5: // Iteramos y mostramos
   6: for(Sensor sensor:listSensors)
   7: {
   8:     Log.i("SENSOR", sensor.getName());
   9: }

La nueva versión de la SDK marca como obsoleto (deprecated) el método getSensors de la clase SensorManager, así como la interfaz SensorListener. Aunque los métodos antiguos son necesarios para versiones anteriores de la SDK, los ejemplos de este artículos se basan en la última versión.

Los sensores del HTC Magic

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

El AK8976A (arriba a la izquierda en esta figura, que muestra el hardware del HTC Dream) 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.

Acceso a los datos del sensor

Para tener acceso a los datos del sensor debemos indicárselo al SensorManager con el método registerListener:

   1: public class MiActividad extends Activity {
   2:     MiVista mVista; // Clase que implemente SensorEventListener
   3:     // ...
   4:
   5:
   6:     @Override
   7:    protected void onCreate(Bundle savedInstanceState) {
   8:         super.onCreate(savedInstanceState);
   9:
  10:         // En esta clase recibiré los eventos y usaré el resultado para lo que quiera
  11:         mVista = new MiVista(this);
  12:         SensorManager mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  13:
  14:         // Cada sensor se registra por separado
  15:         List<Sensor> listSensors = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);
  16:         Sensor orientationSensor = listSensors.get(0);
  17:         mSensorManager.registerListener(mTop, orientationSensor, SensorManager.SENSOR_DELAY_UI);
  18:
  19:         listSensors = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
  20:         Sensor acelerometerSensor = listSensors.get(0);
  21:         mSensorManager.registerListener(mTop, acelerometerSensor, SensorManager.SENSOR_DELAY_UI);
  22:
  23:         listSensors = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);
  24:         Sensor magneticSensor = listSensors.get(0);
  25:         mSensorManager.registerListener(mTop, magneticSensor, SensorManager.SENSOR_DELAY_UI);
  26:
  27:         listSensors = mSensorManager.getSensorList(Sensor.TYPE_TEMPERATURE);
  28:         Sensor temperatureSensor = listSensors.get(0);
  29:         mSensorManager.registerListener(mTop, temperatureSensor, SensorManager.SENSOR_DELAY_UI);
  30:         ...
  31:     }
  32:     ...
  33: }
  34:
  35: class MiVista extends View implements SensorEventListener {
  36: ...
  37: }

Es necesario registrar cada tipo de sensor por separado para poder obtener información de todos ellos. El método registerListener toma como primer parámetro la instancia de la clase que implementa el SensorEventListener, y que veremos a continuación. El tercer parámetro acepta cuatro posibles valores, que indican al sistema con qué frecuencia nos gustaría recibir actualizaciones del sensor. Esta indicación sirve para que el sistema estime cuánta atención necesitan los sensores, pero no garantiza una frecuencia concreta.

Obtención de datos

Para recibir los datos tenemos que implementar dos métodos de SensorEventListener:

   1: class MiVista extends View implements SensorEventListener {
   2:     private float   mOrientationValues[] = new float[3];
   3:     private float   mAccelerometerValues[] = new float[3];
   4:     private float   mMagneticValues[] = new float[3];
   5:     private float   mTemperatureValues;
   6:
   7:     /*
   8:     * El resto del código de la clase para mostrar los datos
   9:     */
  10:
  11:     // En este ejemplo no necesitamos enterarnos de las variaciones de 
  12:     // precisión del sensor
  13:     @Override
  14:     public void onAccuracyChanged(Sensor sensor, int accuracy) {
  15:         // TODO Auto-generated method stub
  16:
  17:     }
  18:
  19:     @Override
  20:     public void onSensorChanged(SensorEvent event) {
  21:         // Cada sensor puede provocar que un thread pase por aquí, así 
  22:         // que sincronizamos el acceso
  23:         synchronized (this) {
  24:             switch(event.sensor.getType()) {
  25:             case Sensor.TYPE_ORIENTATION:
  26:                 for (int i=0 ; i<3 ; i++) {
  27:                     mOrientationValues[i] = event.values[i];
  28:                 }
  29:                 break;
  30:             case Sensor.TYPE_ACCELEROMETER:
  31:                 for (int i=0 ; i<3 ; i++) {
  32:                     mAccelerometerValues[i] = event.values[i];
  33:                 }
  34:                 break;
  35:             case Sensor.TYPE_MAGNETIC_FIELD:
  36:                 for (int i=0 ; i<3 ; i++) {
  37:                     mMagneticValues[i] = event.values[i];
  38:                 }
  39:                 break;
  40:             default:
  41:                 for (int i=0 ; i<event.values.length ; i++) {
  42:                     mTemperatureValues = event.values[i];
  43:                 }
  44:             }
  45:
  46:             invalidate();
  47:         }
  48:     }
  49: }

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.

La clase SensorManager tiene además tres métodos (getInclination, getOrientation y getRotationMatrix), usados para calcular transformaciones de coordenadas. De ellos hablaremos en un próximo artículo.

Escrito por Javier Cancela

20 \20\UTC julio \20\UTC 2009 a 6:00

Programando en Android – NotePad (II)

La clase NotesList
La actividad principal de la aplicación NotePad es NotesList, como se puede ver en el manifest. La clase NotesList se define en el archivo NotesList.java, y es la responsable de la pantalla principal de la aplicación, mostrando una lista de las notas disponibles. Para ello hacemos que esta clase herede de ListActivity, un tipo de actividad especial diseñada para enlazarse a un cursor y mostrar los elementos del cursor en una lista.

Al iniciar la aplicación se invoca esta clase a través de su método onCreate:

   1: @Override
   2: protected void onCreate(Bundle icicle) {
   3:     super.onCreate(icicle);
   4:
   5:     setDefaultKeyMode(SHORTCUT_DEFAULT_KEYS);
   6:
   7:     Intent intent = getIntent();
   8:     if (intent.getData() == null)
   9:         intent.setData(NotePad.Notes.CONTENT_URI);
  10:
  11:     setupListStripes();
  12:
  13:     Uri uri = intent.getData();
  14:     mCursor = managedQuery(uri, PROJECTION, null, null);
  15:
  16:     ListAdapter adapter = new SimpleCursorAdapter(this,
  17:             R.layout.noteslist_item, mCursor,
  18:             new String[] {NotePad.Notes.TITLE},
  19:             new int[] {android.R.id.text1});
  20:     setListAdapter(adapter);
  21: }

En la línea 5 simplemente habilitamos los atajos de teclado. Las líneas 7-9 establecen el esquema de datos sobre el que vamos a operar, que se define en la clase NotePad. Lo veremos más adelante.

Las líneas 13 y 14 acceden a los datos. Las referencias a los orígenes de datos son objetos de tipo Uri, y en nuestro caso tienen esta forma:

   1: public static final Uri CONTENT_URI =
   2:     Uri.parse("content://com.google.provider.NotePad/notes");

El enlace entre este Uri y la base de datos se realiza en la clase NotePadProvider, que veremos en otra entrada. Basta decir por ahora que la línea 14 accede a la base de datos para abrir un cursor con las columnas especificadas por PROJECTION, que se define al comienzo de la clase NotesList:

   1: private static final String[] PROJECTION = new String[] {
   2:     NotePad.Notes._ID, NotePad.Notes.TITLE };
   3: private Cursor mCursor;

Como decíamos al principio, la actividad NotesList hereda de ListActivity, lo que le permite mostrar datos en una lista. Para ello creamos un adaptador ListAdapter, como se muestra en las líneas 16-19. La clase SimpleCursorAdaptor es un creador genérico de adaptadores, al que indicamos el layout que vamos a usar (en nuestro caso R.layout.noteslist_item), el cursor que hemos definido, los nombres de las columnas a mostrar (en este caso sólo el título), y los ids de los controles (tienen que ser del tipo TextView) que van a mostrar cada columna. ´

Finalmente el método setListAdapter asigna el ListAdapter a nuestra actividad para mostrar los datos.

Nos queda por ver el método setupListStripes:

   1: private void setupListStripes() {
   2:     Drawable[] lineBackgrounds = new Drawable[2];
   3:
   4:     lineBackgrounds[0] =
   5:         getResources().getDrawable(R.drawable.even_stripe);
   6:     lineBackgrounds[1] =
   7:         getResources().getDrawable(R.drawable.odd_stripe);
   8:
   9:     View view = getViewInflate().inflate(
  10:             android.R.layout.simple_list_item_1, null, null);
  11:     TextView v = (TextView)view.findViewById(android.R.id.text1);
  12:     v.setText("X");
  13:     v.measure(
  14:         View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.EXACTLY, 100),
  15:         View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, 0)
  16:         );
  17:     int height = v.getMeasuredHeight();
  18:     getListView().setStripes(lineBackgrounds, height);
  19: }
Este método es el encargado de dibujar líneas de colores distintos para el fondo de la ventana. Para ello definimos dos objetos Drawable, que no es más que una clase genérica para objetos dibujables, y a cada uno le asignamos un color (los colores se definen en el archivo colors.xml).
Las líneas 10 y 11 instancian una vista estándar para mostrar elementos de una lista, y la 12 obtiene el objeto TextView correspondiente al control que previamente hemos visto que mostrará la columna título de las notas.
Las líneas 12-16 hacen lo siguiente: escriben una ‘X’ en el control, luego le piden al control que decida las medidas necesarias para mostrar la información, definiendo un ancho máximo y dejando libertad al alto, para que ocupe lo que necesite. Luego obtenemos ese alto, que será el necesario para que el control muestre letras mayúsculas.
La línea 18 llama al método setStripes de la vista asociada a nuestra actividad, que se encarga de establecer un fondo con líneas alternas de los colores definidos al principio y la altura obtenida.
En la próxima entrada hablaremos del menú y de como interactuar con las notas mostradas.

Entradas anteriores:

Programando en Android – NotePad (I)
Programando en Android – Conceptos iniciales (II)
Programando en Android – Conceptos iniciales (I)
Programando en Android – Prólogo

Escrito por Javier Cancela

2 \02\UTC junio \02\UTC 2008 a 8:00

Escrito en Android

Etiquetado con , ,

Programando en Android – Conceptos iniciales (II)

Intents

Si las Activities son básicamente pantallas, las “intenciones” o Intents son la manera de invocar estas Activities. La definición breve de la documentación es: “Un intent es la descripción abstracta de una operación que se va a llevar a cabo”. O dicho de otro modo, un Intent es una clase que permite especificar una Activity a ejecutar, llamando a uno de los métodos de la clase Activity con ese Intent de parámetro. Parece fácil, pero he de confesar que en la documentación de Android el asunto me pareció un poco confuso, sobre todo por la cantidad de información que puede ir asociada a estas clases.

Dos formas de llamar a una Activity

Explicitamente o implicitamente. La forma explícita es simple de entender: creamos un Intent indicando el nombre de la clase correspondiente a la actividad y el paquete, llamamos a startActivity (o startSubActivity si queremos que nos notifiquen cuándo finaliza dicha actividad) y listo. El sistema busca la clase y crea la instancia, pasándo los datos que podamos haber añadido al Intent en el objeto Bundle del método onCreate de la nueva instancia.

// ClaseActividad1 es la clase de la actividad
//que queremos iniciar. El parámetro this indica
//el Context actual, para saber en qué
// package buscar esta clase
Intent i = new Intent(this, ClaseActividad1.class);
// Esta información se recuperará en el objeto Bundle de onCreate
i.putExtra("NombreParametro", valorParametro);
startActivity(i);

La invocación implícita de una actividad se realiza también a través de la clase Intent. Es implícita porque no se indica el nombre de la clase correspondiente a la actividad a invocar, sino que se establecen una serie de criterios, y se deja que el sistema elija una actividad que cumpla esos criterios.

Intenciones y criterios

A un Intent podemos asociarle una acción, unos datos y una categoría. Y aquí está el verdadero quid de esta clase.  Las actividades pueden declarar el tipo de acciones que pueden llevar a cabo y los tipos de datos que pueden gestionar. Las acciones son cadenas de texto estándar que describen lo que que la actividad puede hacer. Por ejemplo, android.intent.action.VIEW es una acción que indica que la actividad puede mostrar datos al usuario. Esta acción viene predefinida en la clase Intent, pero es posible definir nuevas acciones para nuestras actividades. La misma actividad puede declarar que el tipo de datos del que se ocupa es, por ejemplo, “vnd.android.cursor.dir/person”. También puede declarar una categoría, que básicamente indica si la actividad va a ser lanzada desde el lanzador de aplicaciones, desde el menú de otra aplicación o directamente desde otra actividad. En el AndroidManifest.xml quedaría algo así:

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/person" />
</intent-filter>

Así, para llamar implícitamente a una actividad a través de un intent, en vez de asignar el nombre de la clase le asignamos una de las acciones que esta puede llevar a cabo, con el tipo de datos adecuado. Las reglas exactas se indican en la documentación de la clase IntentFilter.

Conclusiones

Activities e Intents son los dos ejes sobre los que gira la arquitectura de las aplicaciones Android. Existen muchos más conceptos importantes, por supuesto, pero a partir de aquí lo mejor es verlo funcionando todo en una aplicación ejemplo. Pero eso será en la próxima entrada.

Entradas anteriores:
Programando en Android – Conceptos iniciales (I)
Programando en Android – Prólogo

Escrito por Javier Cancela

19 \19\UTC mayo \19\UTC 2008 a 8:00

Escrito en Uncategorized

Etiquetado con , ,

Nueva SDK de Symbian: S60 3rd Edition Feature Pack 2

Con una discreción que contrasta con el espectáculo montado alrededor de la SDK del iPhone, Nokia ha hecho pública la que parece ser la versión definitiva de la SDK de S60 3rd Ed. FP2, la última versión del exitoso sistema Symbian, que empezarán a incorporar algunos móviles de Nokia a partir de este año. Por lo que he leído (S60 3rd Edition FP2 SDK released, yo aún la estoy descargando) el paquete incluye SDK C++, Java, Open C y Web Runtime. Junto con la recién liberada versión 1.3 de Carbide.c++ consituye el conjunto de herramientas que Nokia pone a disposición de los desarrolladores.

El éxito de Nokia a la hora de vender dispositivos hace que me sorprenda su poco decidida estrategia para crear una comunidad de desarrolladores sobre su plataforma de smartphones. Documentación escasa, herramientas complejas y una plataforma muy cerrada muestran que la compañía finlandesa no considera importante que los desarrolladores opten por las alternativas de Microsoft, Google o Apple. Veremos si con el tiempo tienen que cambiar de opinión.

Escrito por Javier Cancela

19 \19\UTC marzo \19\UTC 2008 a 19:21

Escrito en Symbian

Etiquetado con , ,

Mostrando un mapa estático de Yahoo! en con la BlackBerry

Veíamos en un artículo anterior (Cómo mostrar imágenes de mapas en el móvil) distintas opciones para mostrar mapas estáticos (simples imágenes de mapas, nada de JavaScript). Vamos a ver con un poco de código cómo utilizar la Yahoo! Map Image API en BlackBerry (que con algunos cambios se podrá adaptar a cualquier aplicación Java ME).

La API acepta varios parámetros para elegir la ubicación del mapa a mostrar, y nosotros vamos a utilizar como parámetros la longitud y latitud del punto central, así como el nivel de zoom y el tamaño de la imagen:

String url = "http://local.yahooapis.com/MapsService/V1/mapImage?appid=mi_yahoo_appid"
+ "&latitude=" + latitude + "&longitude=" + longitude +
"&image_height=" + height + "&image_width=" + width + "&zoom=" + zoom;

El parámetro mi_yahoo_appid es nuestro id de aplicación de Yahoo!, que se puede obtener gratuitamente aquí: Yahoo! Application ID. Con la url formada podemos realizar la llamada a la API.

StreamConnection s = (StreamConnection)Connector.open(url);
HttpConnection httpConn = (HttpConnection)s;
int status = httpConn.getResponseCode();
if (status == HttpConnection.HTTP_OK)
{
try
{
DocumentBuilder doc = DocumentBuilderFactory.newInstance().newDocumentBuilder();
DataInputStream dis = s.openDataInputStream();
Document d = doc.parse(dis);
Element el = d.getDocumentElement();
url = el.getFirstChild().getNodeValue() ;
dis.close();
}
catch(SAXException e)
{
System.err.println(e.toString());
}
catch(ParserConfigurationException e)
{
System.err.println(e.toString());
}

Realizamos la conexión de forma normal, y si todo ha ido bien Yahoo! nos devolverá un documento XML:

<?xml version="1.0" encoding="UTF-8"?>
<Result xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

http://img.maps.yahoo.com/mapimage?MAPDATA=eJz6K.d6wXVM6myr2yRPfx6.kl.uMGgD3Tu4JtDQzr_33pFEsTT

SaosZ9OCtsiDrsLv9t65fzjz0CJm6JO2v_ZIHLflY9gto.xWMK9ovlRJVmrBLO4FoSsh3Ipsr
</Result>

Este documento contiene un único elemento, cuyo texto es otra url que contiene la imagen solicitada en formato PNG. Para acceder a esta url, en el package net.rim.device.api.xml.parsers encontramos la clase DocumentBuilder, que nos permite convertir un InputStream en un objeto Document de org.w3c.dom, a partir del cual accedemos fácilmente al valor del elemento. Con esta segunda url volvemos a abrir una conexión:

s = (StreamConnection)Connector.open(url);
httpConn = (HttpConnection)s;
status = httpConn.getResponseCode();
if (status == HttpConnection.HTTP_OK)
{
java.io.InputStream input = s.openInputStream();
byte[] data = new byte[1];
ByteVector bv = new ByteVector();
while ( -1 != input.read(data) )
{
bv.addElement(data[0]);
}
Bitmap bitmap = Bitmap.createBitmapFromPNG(bv.getArray(), 0, -1);
// Mostramos la imagen almacenada en el bitmap
}

Esta vez utilizamos el flujo de bytes devuelto para crear un Bitmap. No podemos utilizar el método getLength() de la conexión para averiguar el número de bytes total porque las cabeceras de la página no incluyen el campo Content-Length. Así que recorremos byte byte el stream para obtener un array de bytes con el que generar el Bitmap.
Ya tenemos nuestro Bitmap para mostrar. Bastará añadir, por ejemplo, un BitmapField y asignarle el Bitmap recién creado.

Escrito por Javier Cancela

14 \14\UTC marzo \14\UTC 2008 a 7:30

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.