ANIMACIÓN EN ANDROID USANDO SPRITES PARA PROFANOSr

Existen varias formas de crear animaciones en Android, entre ellas tenemos declaraciones basadas en XML  o cargar cada frame como imágenes por separado y luego desplegarlas a intervalos regulares. Sin embargo estos métodos no son flexibles para el tipo de animación que busco en este pequeño tutorial.

En su lugar, usaré sprites y las funcionalidades proporcionadas por Android para dibujar cada cuadro de la animación. En otras palabras, la animación se construirá a partir de una imagen que  incluya todos los cuadros de tal manera que  mediante métodos proporcionados por Android, pueda extraer aquellos relevantes para la animación final. Así, el codigo fuente superpone los frames uno seguido de otro formando la animación.

Buscamos obtener una animación como la siguiente utilizando los 10 primeros frames:

Image

Para definir esta transicion de imágenes introducimos una clase que representa al sprite animado, HorseAnimatedSprite.java. Esta clase recorre una imagen donde están pintados todos los frames desde el primero hasta el último manteniendo un intervalo de tiempo entre cada transición. Lo hace como si tomara una foto por cada frame para despúes configurar la animacion. Como imagen vamos a tomar la conocida “Caballo en Movimiento” de Eadweard Muybridge. La llamaremos the_horse.png.

01 public class HorseAnimatedSprite {
02
03 private Bitmap mBitmap;
04 private Rect mSourceRect;
05 private int mNoOfFrames;
06 private int mCurrentFrame;
07 private long mFrameTimer;
08 private int mFramePeriod;
09 private int mSpriteWidth;
10 private int mSpriteHeight;
11 private int mColumns;
12 private int mXPos;
13 private int mYPos;
14 private int mOffsetX;
15 private int mOffsetY;
  • mBitmap es la variable almacena el bitmap que contiene la animación, la primera imágen de arriba
  • mSourceRect controla que parte de la imagen tomar para cada escena.
  • mNoOfFrames  número de frames que vamos a tomar de la imagen principal.
  • mCurrentFrame  el frame actual
  • mFrameTimer el tiempo transcurrido desde el último cambio de frame.
  • mFramePeriod milisegundos transcurridos entre cada frame, en otras palabras, el tiempo que se muestra en pantalla cada frame. Esto es, el peridodo. (1000/fps)
  • mSpriteWidth/mSpriteHeight  son el ancho y alto en pixeles del sprite, es decir,  de cada frame.
  • mOffsetX/mOffsetY  corresponde a un desplazamiento que utilizo para no mostrar los margenes en cada frame.
  • mXPos/mXPos coordenadas x e y del plano donde  se va a dibujar el sprite. Su centro está ubicado en la esquina superior izquierda.

Utilizo el constructor para inicializar los valores. Tiene varios argumentos, pero es bastante simple de entenderlos.

01 public HorseAnimatedSprite(Bitmap theBitmap, int x, int y, int Height, int Width, int offsetX, int offsetY, int theFPS, int theFrameCount, int columns) {
02 mBitmap = theBitmap;
03 mXPos = x;
04 mYPos = y;
05 mOffsetX = offsetX;
06 mOffsetY = offsetY;
07 mFrameTimer = 0;
08 mCurrentFrame = 0;
09 mSpriteHeight = Height;
10 mSpriteWidth = Width;
11 mSourceRect = new Rect(0, 0, mSpriteWidth, mSpriteHeight);
12 mFramePeriod = 1000 / theFPS;
13 mNoOfFrames = theFrameCount;
14 mColumns = columns;
15 }

mSourceRect se fija en los valores correspondientes a la esquina superior izquierda del rectángulo donde se va a extraer el primer cuadro de la imagen con alto y ancho correspondiente. En la siguiente figura se muestra como queda definida las coordenadas de mSourceRect en el constructor.

Otra variable en ser iniciada es mFramePeriod, que se fija en 1000/FPS de acuerdo a la conocida fórmula física del periodo en función de la frecuencia.   El valor 1000 es debido a que el tiempo proporcionado por java está dado en milisegundos.
La pregunta ahora es ¿cómo cambiar de frame?

Para ello se usa el método update,  que utiliza la variable GameTime para determinar la cantidad de tiempo real transcurrido y compararlo con el periodo para decidir si es el momento de cambiar de frame.

01 public void Update(long GameTime) {
02 if (GameTime > mFrameTimer + mFramePeriod) {
03 mFrameTimer = GameTime;
04 mCurrentFrame += 1;
05
06 if (mCurrentFrame >= mNoOfFrames) {
07 mCurrentFrame = 0;
08 }
09 }
10
11 mSourceRect.left = mCurrentFrame % mColumns * mSpriteWidth + mOffsetX;
12 mSourceRect.right = mSourceRect.left + mSpriteWidth;
13 mSourceRect.top = mCurrentFrame / mColumns * mSpriteHeight + mOffsetY;
14 mSourceRect.bottom = mSourceRect.top + mSpriteHeight;
15 }

El frame cambia si el tiempo transucurrido  (que se obtiene del tiempo del sistema) es mayor al tiempo en el que el frame fue actualizado previamente  (mFrameTimer) más el periodo de la siguiente actualización.

Luego se define en la variable mSourceRect en función de un contador de frames,  el número de columnas de la imagen base y el ajuste vertical y horizontal offset. Este rectángulo corresponde al frame origen esto es el area de la imagen de la cual se va a recortar  el frame.

mOffsetXmOffsetY son valores de ajuste para poder cortar de manera limpia el  frame origen. En la figura siguiente se observa en color verde como queda el  mSourceRect en el método update.

Ahora corresponde  mostrar el frame por pantalla.

1 public void draw(Canvas canvas) {
2 Rect dest = new Rect(getXPos(), getYPos(), getXPos() + mSpriteWidth / 3,
3 getYPos() + mSpriteHeight / 3);
4 canvas.drawBitmap(mBitmap, mSourceRect, dest, null);
5 }

Para ello se crea un rectángulo destino para ubicar el sprite en el canvas. En este caso se toman las posiciones x e y asignadas en el constructor que son las coordenadas de origen para el frame y se escala a 1/3 del tamaño (ancho y alto) con el fin de darle un usa más optimo al canvas.

La clase HorseView inicializa al objeto rocinante con un bitmap creado a partir de los recursos y la imagen original the_horse.png; la posición de origen 5,10. El constructor también pasa como argumento  el ancho y alto del sprite, los ajustes, el número de frames por segundo que se desean, la cantidad total de frames del ciclo, y para este caso, el número de columnas. Es de resaltar la importancia que tiene el valor de FPS pues determina la rapidez con la que aparecen los cuadros en la animación.

01 public HorseView(Context context) {</pre>
02 super(context);
03 getHolder().addCallback(this);
04 thread = new HorseLoopThread(getHolder(), this);
05 //creacion de la gráfica animada
06 rocinante = new HorseAnimatedSprite(
07 BitmapFactory.decodeResource(getResources(), R.drawable.the_horse)
08 , 5, 10 // posicion inicial donde va a quedar la escena
09 , 243, 374 // ancho y alto del sprite
10 , 18, 25 //offset_x y offset_y
11 , 6, 10 // FPS y número de frames de la animación
12 , 4); // número de columnas
13 }

Otro método en esta clase es onDraw() donde el objeto rocinante indica la instrucción para dibujar el frame. También se toma el tiempo del sistema para determinar cuánto ha transcurrido desde que se pintó el anterior frame y de acuerdo a ello determinar que frame dibujar por pantalla.

1 protected void onDraw(Canvas canvas) {
2 GameTime = System.currentTimeMillis(); // tomo el tiempo del sistema antes de dibujar el frame
3 rocinante.Update(GameTime); //reviso tiempo transcurrido y selecciono frame correspondiente
4 canvas.drawColor(Color.BLACK);
5 rocinante.draw(canvas);
6 }

La implementación de los métodos interface SurfaceHolder.Callback() se muestran a continuación. Ello permite que la clase sea un listener del SurfaceHolder.Callback(). Los métodos informan cuando la vista ha cambiado, ha sido creada y esté lista para hacer impresiones o es destruida. En esta implementación nos interesa los dos últimos métodos. En el método surfaceCreated hacemos que el thread arranque run indefinidamente gracias al parámetro true de setRunning.

01 <a class="_hootified" a="" href="http://twitter.com/#!/@Override" onclick="javascript:var e = document.createEvent("CustomEvent"); e.initCustomEvent("hootletEvent", true, true, {type: "userHandle", value: "@Override"});  document.body.dispatchEvent(e); return false;">@Override</a>
02 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
03 }
04
05 <a class="_hootified" a="" href="http://twitter.com/#!/@Override" onclick="javascript:var e = document.createEvent("CustomEvent"); e.initCustomEvent("hootletEvent", true, true, {type: "userHandle", value: "@Override"});  document.body.dispatchEvent(e); return false;">@Override</a>
06 public void surfaceCreated(SurfaceHolder holder) {
07 thread.setRunning(true);
08 thread.start();
09 }
10
11 <a class="_hootified" a="" href="http://twitter.com/#!/@Override" onclick="javascript:var e = document.createEvent("CustomEvent"); e.initCustomEvent("hootletEvent", true, true, {type: "userHandle", value: "@Override"});  document.body.dispatchEvent(e); return false;">@Override</a>
12 public void surfaceDestroyed(SurfaceHolder holder) {
13 boolean retry = true;
14 thread.setRunning(false);
15 while (retry) {
16 try {
17 thread.join();
18 retry = false;
19 } catch (InterruptedException e) {
20 }
21 }
22 }

La clase HorseLoopThread implementa los métodos siguientes:

01 public void setRunning(boolean b) {
02 myThreadRun = b;
03 }
04
05 <a class="_hootified" a="" href="http://twitter.com/#!/@Override" onclick="javascript:var e = document.createEvent("CustomEvent"); e.initCustomEvent("hootletEvent", true, true, {type: "userHandle", value: "@Override"});  document.body.dispatchEvent(e); return false;">@Override</a>
06 public void run() {
07 while (myThreadRun) {
08 Canvas c = null;
09 try {
10 c = MySurfaceHolder.lockCanvas(null);
11 synchronized (MySurfaceHolder) {
12 MyHorseView.onDraw(c);
13 }
14 } finally {
15 if (c != null) {
16 MySurfaceHolder.unlockCanvasAndPost(c);
17 }
18 }
19 }
20 }

Nótese que el método run() bloquea el canvas para que nadie más lo use. Similarmente, Synchronized evita que otros Thread dibujen mientras nosotros llamamos a onDraw. Finalmente se desbloquea el canvas. Este método run se ejecuta de manera indefinida mientras la variable booleana esté asignada a verdadero.

Así finalmente tenemos en total 4 clases:

HorseActivity

HorseAnimatedSprite

HorseLoopThread

HorseView

Nota: debido a que la imágen “Caballo en Movimiento” (the_horse.png) es de densidad mdpi  fue copiada en /res/drawable-mdpi/ pues así Android la selecciona sin compensar pixeles.

Esto es todo y no dude en preguntar utilizando los comentarios.

Be Sociable, Share!

Deja un comentario




*

Categorías

Buscar


Entrecreativos.com Twitter: @entrecreativos Derechos reservados MenteDigital 2010