Animación en Android En este tutorial cubriremos algunos aspectos básicos sobre el manejo de la pantalla, gráficos y animación en Android, para esto construiremos una Actividad que muestre en pantalla la animación de un elefante. Retomaremos las clases Elefante y Animacion que previamente desarrollamos en la sección de Applets ya que nos serán de gran utilidad para este tutorial, sin embargo realizaremos algunas modificaciones para que se adapten a nuestras necesidades en Android. Empezamos por crear un nuevo proyecto de Android para la plataforma 2.2, la Actividad principal la llamaremos TutorialElefante pero esta la retomaremos más adelante. Dentro del mismo paquete donde se encuentra la Actividad principal agregamos dos nuevas clases de Java, una para la clase Elefante y otra para la clase Animacion. Clase Elefante La clase Elefante define el modelo abstracto de un elefante que podemos animar y desplazar en la pantalla. Esta incluye cuatro miembros en la clase para el manejo de la posición en los ejes x y y, la dirección del movimiento y la animación del elefante y dos constantes que nos ayudarán a definir la dirección de movimiento del objeto Elefante. Iniciamos por definir la clase y los miembros y las constantes que la componen. public class Elefante { //Dirección del Elefante, Izquierda public static final int LEFT = - 1; //Dirección del Elefante, Derecha public static final int RIGHT = 1; //Posición del Elefante public int x, y; //Dirección actual del Elefante public int dir; //Animación del Elefante public Animacion animacion; Las constantes LEFT y RIGHT ayudan a determinar la dirección de movimiento del objeto Elefante. Los miembros x y y guardan la posición del objeto Elefante mientras que dir guarda la dirección en la cuál se mueve el objeto Elefante, finalmente animacion guarda un objeto Animacion con la animación de nuestro elefante. Cabe destacar que los miembros de la clase son públicos para dar acceso directo a estos valores en lugar de implementar los métodos get y set para cada miembro ya que de esta manera se tiene un menor costo de memoria y una mayor velocidad de acceso. Sin embargo, aunque es recomendable, no siempre es conveniente deshacerse de todos los métodos get y set, es importante tomar en cuenta el diseño de la aplicación antes de tomar esta decisión.
Ahora, en el método constructor de la clase Elefante se reciben como parámetro los valores para la posición en x y y y la animación del objeto. La dirección del movimiento del objeto se define por defecto hacia la derecha con el valor de RIGHT. * Método constructor de la clase Elefante * @param x es la posición en X del Elefante * @param y es la posición en Y del Elefante * @param animacion es la animación del Elefante public Elefante (int x, int y, Animacion animacion) { this.x = x; this.y = y; //Dirección por defecto del Elefante dir = RIGHT; this.animacion = animacion; Finalmente, definimos un último método llamado avanza() que se encarga de actualizar la posición del objeto en base a su dirección. * Método avanza actualiza la posición del Elefante en * base a su dirección public void avanza(){ //Avanza a la derecha if (dir == RIGHT) x += 5; //Avanza a la izquierda if (dir == LEFT) x - = 5; Clase Animacion La clase Animacion provee las herramientas necesarias para crear una animación cuadro por cuadro que podemos desplegar en pantalla. Esta incluye cuatro miembros que definen los cuadros de la animación, la duración de la animación, el índice del cuadro actual y el tiempo que ha transcurrido de animación. Iniciamos por importar las clases requeridas en la clase, definimos también la clase y los miembros de esta. import android.graphics.bitmap; import java.util.arraylist; import java.util.list;
public class Animacion { //Lista de objetos Cuadro, contiene los cuadros de la animación public List<Cuadro> cuadros; //Índice del cuadro actual public int indice; //Tiempo transcurrido public float tiempo; //Duración de la animación public float duracion; La lista de objetos Cuadro, cuadros, contiene todos los cuadros (imágenes) de la animación; la clase Cuadro es una clase interna de la clase Animacion, esta la definiremos más adelante. En indice guardamos el índice del cuadro que se esta mostrando cuando reproducir la animación, tiempo lleva la cuenta del tiempo que ha transcurrido desde que la animación inicio mientras que duracion guarda la duración total de la animación. En seguida creamos un método constructor vacío para nuestra clase con el cual crearemos animaciones vacías. Para esto creamos la lista de objetos Cuadro sin agregar ningún elemento a la misma, inicializamos duración en 0 y llamamos al método iniciar(), que más adelante definiremos, que simplemente se encarga de iniciar el tiempo transcurrido en 0 al igual que el índice para que la animación corra desde el primer cuadro cuando inicie. * Método constructor de la clase Animación * Crea una animación vacía public Animacion() { //Crea la lista de cuadros cuadros = new ArrayList<Cuadro>(); //Inicializa la duración de la animación en 0 duracion = 0; //Inicializa la animación iniciar(); Ahora definimos el método sumacuadro() que se encarga de añadir cuadros a la animación * Método sumacuadro * Añade un cuadro (imagen) a la animación con * la duración indicada (tiempo que se muestra el cuadro) * * @param imagen es la imagen del cuadro * @param duracion es el tiempo que se muestra el cuadro
public synchronized void sumacuadro(bitmap imagen, float duracion) { //Agrega la duración del cuadro a la duración de la animación this.duracion += duracion; //Crea un nuevo cuadro y lo agrega a la animación cuadros.add(new Cuadro(imagen, this.duracion)); El método recibe como parámetros la imagen del cuadro y un valor flotante que indica la duración del cuadro. Se añade la duración del cuadro a la duración de la animación. Creamos un nuevo objeto Cuadro utilizando la imagen que recibimos como parámetro y la duración de la animación y lo agregamos a la lista de objetos Cuadro. Continuamos por definir los métodos iniciar() y anima(). Como se mencionó anteriormente, el método iniciar() se encarga de inicializar el índice y el tiempo de la animación en 0 con el fin de que una vez que la animación se reproduzca esta inicie por desde el primer cuadro. Por su parte, el método anima() actualiza la animación en base al tiempo transcurrido desde la última vez que se actualizó la animación. * Método iniciar * Inicializa la animación en el primer cuadro con * tiempo 0 public synchronized void iniciar(){ tiempo = 0; indice = 0; * Método anima * Actualiza la animación en base al tiempo transcurrido desde * la última actualización * * @param tiempo es el tiempo transcurrido public synchronized void anima(float tiempo) { //Si la animación no esta vacía if (cuadros.size() > 1) { //Guarda el tiempo transcurrido this.tiempo += tiempo; //Si el tiempo transcurrido es mayor a la duración de la //animación if (this.tiempo >= duracion) { //Reinicia la animación this.tiempo = this.tiempo % duracion;
indice = 0; //Cambia el índice del cuadro de acuerdo al tiempo //transcurrido while (this.tiempo > cuadros.get(indice).tiempo) { indice ++; Como último método de esta clase definimos el método getcuadro() el cual simplemente se encarga de regresar la imagen del cuadro que se este desplegando en pantalla para poder desplegarlo. * Método getcuadro * Retorna la imagen del cuadro actual * * @return Regresa un Bitmap con la imagen del cuadro actual public synchronized Bitmap getcuadro(){ //Si la animación esta vacía if (cuadros.size() == 0) //Regresa nulo return null; else //Regresa la imagen del cuadro actual return cuadros.get(indice).imagen; Para finalizar con la clase Animacion definimos la clase interna Cuadro que nos permite crear objetos Cuadro para manejar la imagen y la duración de un cuadro de animación. La clase Cuadro este compuesta por dos miembros, imagen y tiempo que guardan respectivamente la imagen del cuadro y el tiempo de duración del mismo. En esta clase definimos dos métodos constructores, uno vacío y otro con parámetros con los cuales podremos crear ya sea un cuadro de animación vacío o uno con las características que nosotros necesitamos. * Clase Cuadro * Maneja la imagen y la duración de un cuadro de animación public class Cuadro { Bitmap imagen; //Imagen del cuadro float tiempo; //Duración del cuadro * Método constructor de la clase Cuadro
* Crea un cuadro vacío public Cuadro() { this.imagen = null; this.tiempo = 0; * Método constructor de la clase Cuadro * Crea un cuadro de animación con las imagen y el tiempo * recibidos como parámetros * * @param imagen es la imagen del cuadro * @param tiempo es la duración del cuadro public Cuadro(Bitmap imagen, float tiempo) { //Imagen del cuadro this.imagen = imagen; //Duración del cuadro this.tiempo = tiempo; Actividad TutorialElefante Ya que tenemos listas las clases Elefante y Animacion continuaremos por trabajar con nuestra Actividad que mostrará la animación del elefante. Sin embargo, antes de empezar a trabajar de lleno con nuestra actividad debemos de realizar algunas configuraciones a nuestro proyecto. Primero necesitamos asegurarnos de que nuestro proyecto contenga la carpeta Assets ya que en esta guardaremos los recursos que la Actividad utilizará. Si el proyecto no contiene esta carpeta será necesario crearla al mismo nivel de la carpeta Resources. Proyecto: -Assets -Resources - La razón por la cual utilizamos este directorio para guardar nuestros recursos en lugar de utilizar la carpeta Resources es que nos brinda una mayor libertad para organizar los recursos de acuerdo a nuestras necesidades a diferencia de la carpeta Resources que nos obliga a utilizar la estructura de organización que ya tiene predefinida. Por ejemplo, si nuestra Actividad manejará imágenes y sonidos y quisiéramos guardarlas en una carpeta de imágenes para las imágenes y otra de sonidos para los sonidos, dentro de la carpeta Assets podríamos
hacerlo sin problema alguno, por el contrario, en la carpeta de Resources no se podría implementar. Ahora definimos el archivo manifiesto de la aplicación <?xml version="1.0" encoding="utf- 8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jcj.testelefante" android:versioncode="1" android:versionname="1.0" android:installlocation="preferexternal"> <application android:label="@string/app_name" android:icon="@drawable/icon" android:debuggable="true"> <activity android:name="tutorialelefante" android:label="@string/app_name" android:configchanges="keyboard keyboardhidden orientation" android:screenorientation="portrait"> <intent- filter> <action android:name= "android.intent.action.main" /> <category android:name= "android.intent.category.launcher" /> </intent- filter> </activity> </application> <uses- permission android:name="android.permission.wake_lock"/> <uses- permission android:name= "android.permission.write_external_storage"/> <uses- sdk android:minsdkversion="3" android:targetsdkversion="8"/> </manifest> Analizemos que es lo que realiza cada elemento definido en el manifiesto y sus atributos. El elemento manifest inicia por definir un namespace de XML que en este caso es android el cual se implementa en el resto del archivo. El atributo package define el nombre del paquete raíz de la aplicación( en el caso de este ejemplo es com.jcj.testelefante pero en tu caso será el que tu hayas definido para el proyecto). Los atributos versioncode y versionname indican el número de versión de la aplicación, sin embargo son usados con diferentes propósitos, el primero es utilizado por el Android Market para llevar un registro de las versiones de la aplicación y el segundo es desplegado por el Android Market a los usuarios cuando revisan la aplicación. El atributo installlocation existe apartir de la versión 2.2 de Android y específica donde debe ser instalada la aplicación. En este caso el valor preferexternal indica la aplicación sea instalada en la tarjeta SD en caso de que haya una instalada.
El elemento application define las propiedades de la aplicación, inicia con el atributo label el cual define el nombre de la aplicación que será mostrado en el dispositivo una vez que sea instalada. El atributo icon define el icono de la aplicación mientras que el atributo debuggable indica si la aplicación puede debuggearse o no, para aplicaciones en desarrollo lo más común es definir este atributo en true. El elemento activity define las propiedades de una Actividad, si nuestra aplicación contiene más de una Actividad entonces será necesario definir este elemento para cada Actividad. En el atributo name se indica el nombre de la clase de la Actividad relativo al paquete definido en el elemento manifest. El atributo label despliega el texto definido en la barra de título de la Actividad si es que esta contiene una. Con configchanges le indicamos a Android que nosotros nos haremos cargo de alguno o algunos cambios de configuración que se pudieran presentar en el dispositivo como por ejemplo la orientación, en este caso indicamos que nos encargaremos de los cambios en la configuración del teclado del dispositivo así como también del cambio de orientación del teclado. Por último, el atributo screenorientation indica que deseamos fijar la vista de la aplicación en una sola orientación que en este caso será en orientación portrait. El elemento intent-filter que esta dentro del elemento activity ayuda a determinar la forma en que Android se comunicará con la Actividad. En el caso de este ejemplo, en el atributo action indicamos que la Actividad es la actividad principal de la aplicación y con el atributo category indicamos a Android que la Actividad sea añadida al application launcher de Android de tal manera que al presionar el botón de la aplicación en el dispositivo esta Actividad será llamada. El elemento uses-permission nos permite tener acceso a recursos del sistema como, la cámara, Internet, la tarjeta SD, etc. En esta actividad pediremos permiso al sistema para implementar el Wake Lock y tener acceso a la tarjeta SD para poder instalar nuestra aplicación en ella. El Wake Lock evita que el dispositivo entre en estado de reposo cuando no ha recibido ninguna entrada por parte del usuario. Finalmente, el elemento uses-sdk nos ayuda a definir los requerimientos mínimos de plataforma para que la aplicación pueda correr sin ningún problema. El atributo minsdkversion especifica la plataforma mínima con la cual es compatible la aplicación mientras que el atributo targetsdkversion especifica la versión de plataforma para la cual estamos desarrollando. En este tipo de casos es importante que en el desarrollo se tenga cuidado de no utilizar APIs que no existan en las versiones anteriores de Android para asegurar la compatibilidad de la aplicación. Existen muchos más elementos y atributos que podemos definir de acuerdo a los requerimientos de la aplicación sin embargo, para nuestra aplicación no necesitas definir ningún elemento o atributo extra.
Para nuestra aplicación no definiremos un layout en XML si no que lo haremos dentro del mismo código de la Actividad, por lo tanto ahora podemos ir directamente a trabajar con la Actividad. Dado a que en nuestra Actividad mostraremos una animación necesitaremos de una vista especial que nos ayude a realizar el trabajo de la manera más óptima posible. Para esto haremos nuestra propia implementación de la clase SurfaceView de Android la cual nos ofrece una vista que permite la posibilidad de trabajar en un thread independiente al de la Actividad. Además para asegurarnos de que nuestra animación se vea igual y mantenga su proporción independientemente del dispositivo en el que corra la aplicación utilizaremos un buffer virtual para dibujar la escena y finalmente proyectarla en la pantalla. Iniciamos nuestra Actividad importando las clases que estaremos implementando así como también por la definición de la misma y de sus miembros. import android.app.activity; import android.content.context; import android.content.res.assetfiledescriptor; import android.content.res.assetmanager; import android.content.res.configuration; import android.graphics.bitmap; import android.graphics.bitmap.config; import android.graphics.bitmapfactory; import android.graphics.canvas; import android.graphics.rect; import android.media.audiomanager; import android.media.soundpool; import android.os.bundle; import android.os.powermanager; import android.os.powermanager.wakelock; import android.view.surfaceholder; import android.view.surfaceview; import android.view.window; import android.view.windowmanager; import java.io.ioexception; import java.io.inputstream; public class TutorialElefante extends Activity { //Vista de la Actividad ElefanteFastRenderView renderview; //Candado para evitar que el dispositivo duerma WakeLock wakelock; //Objeto Elefante Elefante elefante; //Objeto Animacion Animacion elefanteanim;
//Colección de sonidos SoundPool pool; //Identificador de sonido int soundid = - 1; //Posiciones 'x' y 'y' int x, y; //Objetos Bitmap para el manejo de imágenes Bitmap cuadro, framebuffer; El miembro renderview de la clase ElefanteFastRenderView es la implementación de la clase SurfaceView que anteriormente mencionamos, esta es la vista con la cual estaremos trabajando en la Actividad. La clase ElefanteFastRenderView, la cual definiremos más adelante, esta definida como una clase interna de nuestra Actividad. El miembro wakelock de la clase WakeLock nos ayudará a configurar el dispositivo para evitar que este entre en estado de reposo después de cierto tiempo. Los miembros elefante y elefanteanim son objetos de las clases Elefante y Animacion respectivamente, estos dos miembros se integran para crear el elefante animado que se mostrará en la pantalla. El miembro pool de la clase SoundPool consiste en una colección de sonidos que podemos configurar y administrar para reproducir sonidos en la aplicación. Cuando trabajamos con audio debemos de tomar muy en cuenta el tamaño de el o los archivos de sonido con los cuales trabajaremos ya que debemos buscar la manera de optimizar el uso de la memoria para evitar que la aplicación se vuelva muy lenta. Cuando trabajamos con archivos de sonido cortos y que ocupan poca memoria, como por ejemplo cualquier efecto de sonido, implementamos la clase SoundPool la cual nos permitirá cargar los archivos a memoria para posteriormente trabajar con ellos. En el caso de archivos de sonido más largos y pesados no es conveniente cargarlos a memoria ya que consumirán una gran cantidad de recursos, para estos utilizaremos un método de streaming de audio el cual discutiremos más adelante en otro tutorial. El miembro soundid es un valor entero que implementaremos en conjunto con el miembro pool para poder reproducir nuestro efecto de sonido. Los miembros x y y nos ayudan a controlar la posición del elefante. Finalmente tenemos dos miembros de la clase Bitmap, cuadro y framebuffer; cuadro nos servirá de apoyo para cargar las imágenes de los cuadros de la animación del elefante mientras que framebuffer es el buffer virtual del cual hablamos antes que nos ayudará a dibujar de manera correcta nuestra vista en cualquier dispositivo en el cual probemos la aplicación. Continuamos ahora por definir el método oncreate() de nuestra Actividad. En este método configuraremos la Actividad para que se despliegue en full screen, implemente el wake lock para que el dispositivo no entre en reposo y tenga el control del volumen del contenido multimedia. Dentro de la misma creamos la colección de sonidos, cargamos los recursos (sonido e imágenes), creamos la animación del elefante y el objeto Elefante y finalmente asignamos nuestra implementación de la clase SurfaceView, renderview, como la vista de la Actividad.
* Método oncreate sobrescrito de la clase Activity * Llamado cuando la Actividad se crea por primera vez, en * este se realiza la configuración de la actividad * @param savedinstancestate es el estado de la última ejecución de la * Actividad @Override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); //Configuración para pantalla en fullscreen requestwindowfeature(window.feature_no_title); getwindow().setflags( WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //Determina la orientación del dispositivo y //crea un buffer en base a esta boolean islandscape = getresources().getconfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; //Ancho del buffer int framebufferwidth = islandscape? 480 : 320; //Alto del buffer int framebufferheight = islandscape? 320 : 480; //Crea el buffer framebuffer = Bitmap.createBitmap(frameBufferWidth, framebufferheight, Config.RGB_565); //Evita que el dispositivo se duerma PowerManager powermanager =(PowerManager) getsystemservice(context.power_service); wakelock = powermanager.newwakelock( PowerManager.FULL_WAKE_LOCK, "Test"); //Establece el control del volumen del sonido setvolumecontrolstream(audiomanager.stream_music); //Crea una colección de sonidos para reproducir pool = new SoundPool (1, AudioManager.STREAM_MUSIC, 0); //Crea un nuevo objeto Animacion elefanteanim = new Animacion(); try{ //Crea un administrador de contenidos para //cargar los contenidos
AssetManager assetmanager = getassets(); //Carga un efecto de sonido AssetFileDescriptor descriptor = assetmanager.openfd( "elephant.ogg"); //Agrega el efecto de sonido a la colección de sonidos soundid = pool.load(descriptor, 1); InputStream is; //Carga las imágenes de una animación y //se añaden a la animación for (int i = 0; i < 6; i++) { //Carga la imagen is = assetmanager.open("elefante" + i + ".png"); cuadro = BitmapFactory.decodeStream(is); is.close(); //Agrega la imagen a la animación elefanteanim.sumacuadro(cuadro, 0.1f); catch (IOException e) { // //Crea un nuevo objeto Elefante elefante = new Elefante(frameBufferWidth / 2 - elefanteanim.getcuadro().getwidth() / 2, framebufferheight / 2 - elefanteanim.getcuadro().getheight() / 2, elefanteanim); //Crea y establece la vista de la Actividad renderview = new ElefanteFastRenderView(this); setcontentview(renderview); El método inicia por configurar la pantalla en modo fullscreen. Posteriormente revisamos la orientación del dispositivo para determinar el ancho y el alto del buffer virtual que utilizaremos para dibujar. Estableceremos como base un buffer de dimensiones 480 x 320 (Landscape) ó 320 x 480 (Portrait) que posteriormente se ajustará al tamaño de la pantalla del dispositivo manteniendo esta proporción. Creamos un PowerManager que nos permitirá administrar las configuraciones de energía del dispositivo, en este caso lo utilizamos para crear el wake lock que evitará que el dispositivo entre en estado de reposo. Para establecer el control del volumen del sonido en la Actividad implementamos el método setvolumecontrolstream() al cual le indicamos que el stream que utilizaremos será el de la música (otro stream podría ser el del volumen de las llamadas). Al crear nuestra colección de sonidos le indicamos al constructor de la clase SoundPool le indicamos que solo podrá reproducir un máximo de un sonido a la vez y que esto lo realizará a través del stream de música del sistema ( el 0 que se envía como parámetro es un valor por defecto ya que actualmente este no tiene ninguna funcionalidad pero es requerido por el constructor). Después de esto creamos el objeto Animacion que contendrá la animación del
elefante y cargamos los recursos de nuestra aplicación. Esto lo realizamos dentro un try-catch para atrapar cualquier excepción que se produzca en caso de que haya algún error al cargar los recursos. Para cargar los recursos es necesario que, además de asegurarnos de que todos estén dentro de la carpeta Assets de nuestro proyecto, creemos un objeto AssetManager que nos brinda las herramientas para cargar y administrar los recursos. En el caso del sonido necesitamos generar un descriptor que nos ayude a identificar el archivo de audio, este descriptor lo guardamos en un objeto de la clase AssetFileDescriptor. Una vez que tenemos el descriptor lo cargamos en la colección de sonidos la cual nos retornará un valor entero que nos ayudará a identificar el sonido dentro de la colección. En el caso de las imágenes nos apoyamos en un InputStream para tener acceso a los archivos. Dado a que en el caso de este ejemplo las imágenes están en serie utilizamos un ciclo for para cargarlas. Con la ayuda del AssetManager abrimos las imágenes y las guardamos en el InputStream, después se lo pasamos al método BitmapFactory.decodeStream() el cuál creará un objeto Bitmap que finalmente utilizaremos para crear un nuevo cuadro de animación. Una vez que termina de cargar los recursos se crea el objeto Elefante para el cual definimos las posiciones en x y y y la animación. Para finalizar el método creamos una nueva vista utilizando nuestra implementación de la clase SurfaceView y la asignamos como la vista de la Actividad. Ahora pasamos a definir los métodos onpause() y onresume(). Debido a que nuestra vista la trabajaremos en un thread independiente al de la Actividad será muy importante mantener sincronizados los threads para que trabajen a la par y serán estos métodos los que nos ayudarán a lograrlo. * Método onpause sobrescrito de la clase Activity * Llamado cuando la Actividad pasa a segundo plano pero aun es * visible, * libera recursos usados por la Actividad @Override protected void onpause() { super.onpause(); //Pausa la vista de la Actividad renderview.pause(); //Libera el candado que protege la pantalla wakelock.release(); //Se liberan los efectos de sonido que actualmente se encuentran //en la colección pool.release(); * Método onresume sobrescrito de la clase Activity * Llamado cuando la Actividad vuelve a primer plano
@Override protected void onresume() { super.onresume(); //Reanuda la vista de la Actividad renderview.resume(); //Retoma el candado que protege a la pantalla wakelock.acquire(); En el método onpause() pausamos el thread de la vista llamando a su método pause(). Liberamos el wake lock para que el sistema o la actividad que se encuentre actualmente en primer plano pueda encargarse de manejarlo. También liberamos los recursos de la colección de sonidos para liberar memoria a través del método release() de la clase SoundPool. En el método onresume() simplemente reanudamos el thread de la vista llamando a su método resume() y retomamos el wake lock para evitar que el dispositivo entre en estado de reposo. Finalmente, definimos nuestra implementación de la clase SurfaceView, la clase ElefanteFastRenderView. La clase ElefanteFastRenderView extiende a la clase SurfaceView e implementa a la interfase Runnable lo cual permite que esta clase corra en su propio thread. Empezamos esta clase por definir sus miembros. * Clase ElefanteFastRenderView * Clase interna de la clase TutorialElefante * Maneja una vista y su ciclo de vida en un thread independiente public class ElefanteFastRenderView extends SurfaceView implements Runnable { //Thread de la vista Thread renderthread = null; //Contenedor de la vista SurfaceHolder holder; //Canvas para dibujar Canvas canvas; //Bandera para conocer el estado de la Activdad volatile boolean running = false; //Controladores de tiempo float tiempotick = 0, tick = 0.1f; El renderthread es el thread mismo de la vista el cual tendremos que sincronizar con el thread de la Actividad. El miembro holder de la clase SurfaceHolder es una superficie que contiene a la vista con la cual estamos trabjando. Con la ayuda del miembro canvas podremos dibujar sobre nuestro buffer virtual para posteriormente proyectarlo en la pantalla. El miembro
running nos indicará si el thread esta actualmente activo lo cual nos ayuda para la sincronización entre el thread de la vista y el thread de la Actividad. Los miembros tiempotick y tick nos indican el tiempo transcurrido y el tiempo que debe durar cada ciclo de actualización respectivamente. En el método constructor de la clase mandamos a llamar al constructor de la clase padre de nuestra implementación para que la vista sea creada de manera adecuada, a este constructor le enviamos el contexto en el cual estamos trabajando que en este caso es nuestra Actividad. Obtenemos también el contenedor de la vista ya que más adelante lo utilizaremos para proyectar en el nuestro buffer virtual. Creamos también un canvas con nuestro buffer virtual para poder dibujar en el. * Método contructor de la clase ElefanteFastRenderView * Llama al método constructor de la clase base al cual * le pasa el contexto como parámetro y crea los objetos * requeridos por la clase * @param context es el contexto desde el cual fue llamado el * método public ElefanteFastRenderView(Context context) { super(context); //Obtiene el contenedor holder = getholder(); //Crea un nuevo objeto canvas con el buffer creado en la //Actividad canvas = new Canvas(frameBuffer); Los métodos resume() y pause() de esta clase se encargan respectivamente de reanudar y pausar el thread de la vista así como también de informar a esta del estado del thread. * Método resume * Llamado cuando la Actividad vuelve a primer plano, * inicia el thread de la vista public void resume() { //La bandera indica que la Actividad esta en ejecución running = true; //Crea un nuevo thread para la vista renderthread = new Thread(this); //Iniciliza el thread de la vista renderthread.start();
* Método pause * Llamado cuando la Actividad pasa a segundo plano, * detiene el thread de la vista public void pause() { //La bandera indica que la Actividad no esta en ejecución running = false; //Espera a que el thread de la vista se detenga while(true) { try { renderthread.join(); break; catch (InterruptedException e) { // Ahora en el método run() de la clase es donde ocurre lo interesante. Es en este método donde mandamos a actualizar al objeto Elefante, dibujamos en nuestro buffer virtual y finalmente proyectamos el buffer virtual en la pantalla del dispositivo. * Método run sobrescrito de la clase Thread * Llamado cuando el thread de la vista esta en ejecución, * se encarga de actualizar la vista de la Actividad public void run() { //Objeto Rect crea un rectángulo Rect dstrect = new Rect(); //Obtiene el tiempo actual long tiempoi = System.nanoTime(); //El ciclo se ejecuta cuando la Actividad esta en ejecución while (running) { //Verifica que exista una vista válida if(!holder.getsurface().isvalid()) continue; //Calcula el tiempo transcurrido float tiempo = (System.nanoTime() - tiempoi) / 1000000000.0f; //Obtiene el tiempo actual tiempoi = System.nanoTime(); //Pinta un fondo amarillo canvas.drawrgb(255, 255, 0); //Actualiza el objeto Elefante en base al tiempo //transcurrido
actualizaelefante(tiempo); //Actualiza la animación en base al tiempo //transcurrido elefante.animacion.anima(tiempo); //Dibuja el objeto Elefante en el buffer canvas.drawbitmap(elefante.animacion.getcuadro(), elefante.x, elefante.y, null); //Crea un nuevo canvas con la vista actual Canvas pantalla = holder.lockcanvas(); //Determina la resolución de la pantalla pantalla.getclipbounds(dstrect); //Dibuja el buffer en la pantalla con el tamaño de la //pantalla pantalla.drawbitmap(framebuffer, null, dstrect, null); holder.unlockcanvasandpost(pantalla); El método run() trabaja de la siguiente manera. Primero crea un objeto Rect el cual se utilizará para obtener las dimensiones de la pantalla y proyectar el buffer virtual a este tamaño. Se obtiene el tiempo actual del sistema lo cuál nos ayudará a actualizar al objeto Elefante más adelante. Dentro de un ciclo que correrá mientras el estado del thread sea activo, verificamos que la vista con la cual estamos trabajando sea válida con el método holder.getsurface.isvalid(). Posteriormente calculamos el tiempo que ha transcurrido desde la última actualización y capturamos una vez más el tiempo actual para tenerlo como referencia para la próxima actualización. Con la ayuda del canvas que creamos con el buffer virtual pintamos un fondo amarilla en el buffer. Actualizamos al objeto Elefante con la ayuda del método AactualizaElefante() que más adelante definiremos, actualizamos también la animación del elefante y finalmente dibujamos el elefante en el buffer virtual. Por último capturamos el contenedor de la vista y con el creamos un canvas el cual usaremos para proyectar nuestro buffer virtual en la pantalla. Con el método getclipbounds() obtenemos las dimensiones del contenedor y las guardamos en el objeto Rect que previamente habíamos creado. Proyectamos el buffer virtual en la pantalla con la ayuda del método drawbitmap() y utilizando las medidas del contenedor que guardamos en el objeto Rect. Finalmente liberamos el contenedor para que muestre en pantalla la imagen del elefante. Para finalizar, definimos el método actualizaelefante() el cual se encarga de actualizar la posición del objeto Elefante en base al tiempo transcurrido. El método checa que el elefante no salga por las orillas de la pantalla y lo envía en la dirección opuesta cada vez que alcance alguna de ellas. Cuando el elefante choca con las orillas se reproduce el efecto de sonido que tenemos cargado en la colección de sonidos. para esto primero nos aseguramos de que el sonido no este en reproducción con la ayuda del método pool.stop() al cual le damos como
parámetro el identificador de nuestro sonido. Finalmente reproduce el sonido con la ayuda del método pool.play() el cual recibe como parámetros el identificador del sonido que queremos reproducir, el volumen del canal izquierdo, el volumen del canal derecho, la prioridad de reproducción, si el sonido tendrá loop o no, y la velocidad de reproducción. * Método actualizaelefante * Llamado para actualizar el objeto Elefante * @param tiempo es el tiempo transcurrido desde la última * vez que se actualizó private void actualizaelefante(float tiempo) { //Guarda el tiempo transcurrido tiempotick += tiempo; //Actualiza mientras el tiempo transcurrido sea mayor al //tiempo de actualización while(tiempotick > tick) { tiempotick - = tick; //Actualiza la posición del objeto Elefante elefante.avanza(); //Checa colisión con las orillas de la pantalla if(elefante.x + elefante.animacion.getcuadro().getwidth() > framebuffer.getwidth() elefante.x < 0){ //Cambia la dirección elefante.dir *= - 1; //Actualiza la posición del objeto Elefante elefante.avanza(); //Si el sonido ya estaba en reproducción lo //detiene pool.stop(soundid); //Reproduce el efecto de sonido pool.play(soundid, 1, 1, 0, 0, 1);