Tutoriales Android

Introducción

Una de las funciones más habituales de los modernos teléfonos móviles es su utilización como reproductores multimedia. Su uso más frecuente es como reproductores MP3, para la reproducción de vídeos y televisión a través de Internet.  El API de Android viene preparado con excelentes características de reproducción multimedia, permite la reproducción de gran variedad de formatos, tanto de audio como de vídeo. El origen de los datos puede ser tanto un fichero local como un stream obtenido desde Internet. Todo este trabajo lo realiza principalmente una clase MediaPlayer.

 Antes de comenzar la descripción de estos contenidos, comenzaremos el capítulo con un aspecto de vital importancia en el desarrollo de aplicaciones en Android, el ciclo de vida de una aplicación. Es decir, cómo las actividades son creadas, ejecutadas, puestas en espera y finalmente destruidas.       

Objetivos:

  • Comprender el ciclo de vida de una actividad Android.
  • Utilizar de forma correcta los diferentes eventos relacionados con el ciclo de vida.
  • Aprender cuándo y cómo guardar el estado de una actividad.
  • Repasar las facilidades multimedia disponibles en Android, que formatos soporta y las clases que hemos de utilizar.
  • Describir la clase MediaPlayer, utilizada para la reproducción de audio y video.
  • Dotar a la aplicación Asteroides de un control adecuado de su ciclo de vida y de varios efectos de audio.

 

     Ciclo de vida de una actividad

El ciclo de vida de una aplicación Android es bastante diferente al ciclo de vida de una aplicación en otros S.O., como Windows. La mayor diferencia es que, en Android el ciclo de vida es controlado principalmente por el sistema, en lugar de ser controlado directamente por el usuario.

video[Tutorial] Ciclo de vida de una aplicación en Android

Una aplicación en Android está formada por un conjunto de elementos básicos de interacción con el usuario, conocidos como actividades. Además de varias actividades, una aplicación también puede contener servicios. El ciclo de vida de los servicios se estudiará en el capítulo 8. Son las actividades las que realmente controlan el ciclo de vida de las aplicaciones, dado que el usuario no cambia de aplicación, sino de actividad. El sistema mantiene una pila con las actividades previamente visualizadas, de forma que el usuario puede regresar a la actividad anterior pulsando la tecla “retorno”.

Una aplicación Android corre dentro de su propio proceso Linux. Este proceso se crea con la aplicación y continuará vivo hasta que ya no sea requerido y el sistema reclame su memoria para asignársela a otra aplicación.

Una característica importante, y poco usual, de Android es que la destrucción de un proceso no es controlada directamente por la aplicación, sino que es el sistema el que determina cuándo destruir el proceso. Lo hace basándose en el conocimiento que tiene de las partes de la aplicación que están corriendo (actividades y servicios), en la importancia de dichas partes para el usuario y en cuánta memoria disponible hay en un determinado momento.

Si tras eliminar el proceso de una aplicación, el usuario vuelve a ella, se crea de nuevo el proceso, pero se habrá perdido el estado que tenía esa aplicación. En estos casos, será responsabilidad del programador almacenar el estado de las actividades, si queremos que cuando sean reiniciadas conserven su estado.

Como vemos, Android es sensible al ciclo de vida de una actividad; por lo tanto, necesitas comprender y manejar los eventos relacionados con el ciclo de vida si quieres crear aplicaciones estables.

Una actividad en Android puede estar en uno de estos cuatro estados:

Activa (Running): La actividad está encima de la pila, lo que quiere decir que es visible y tiene el foco. 

Visible (Paused): La actividad es visible pero no tiene el foco. Se alcanza este estado cuando pasa a activa otra actividad con alguna parte transparente o que no ocupa toda la pantalla. Cuando una actividad está tapada por completo, pasa a estar parada.

Parada (Stopped): Cuando la actividad no es visible. El programador debe guardar el estado de la interfaz de usuario, preferencias, etc.

Destruida (Destroyed): Cuando la actividad termina al invocarse el método finish(), o es matada por el sistema.

Cada vez que una actividad cambia de estado se van a generar eventos que podrán ser capturados por ciertos métodos de la actividad. A continuación se muestra un esquema que ilustra los métodos que capturan estos eventos.

Ciclo de vida de una actividad.

onCreate(Bundle): Se llama en la creación de la actividad. Se utiliza para realizar todo tipo de inicializaciones, como la creación de la interfaz de usuario o la inicialización de estructuras de datos. Puede recibir información de estado dela actividad (en una instancia de la clase Bundle), por si se reanuda desde una actividad que ha sido destruida y vuelta a crear.

onStart(): Nos indica que la actividad está a punto de ser mostrada al usuario.

onResume(): Se llama cuando la actividad va a comenzar a interactuar con el usuario. Es un buen lugar para lanzar las animaciones y la música.

onPause(): Indica que la actividad está a punto de ser lanzada a segundo plano, normalmente porque otra actividad es lanzada. Es el lugar adecuado para detener animaciones, música o almacenar los datos que estaban en edición.

onStop(): La actividad ya no va a ser visible para el usuario. Ojo si hay muy poca memoria, es posible que la actividad se destruya sin llamar a este método.

onRestart(): Indica que la actividad va a volver a ser representada después de haber pasado por onStop().

onDestroy(): Se llama antes de que la actividad sea totalmente destruida. Por ejemplo, cuando el usuario pulsa el botón de volver o cuando se llama al método finish(). Ojo si hay muy poca memoria, es posible que la actividad se destruya sin llamar a este método.

Ejercicio: ¿Cuándo se llama a los eventos del ciclo de vida en una actividad?

En este ejercicio vamos a implementar todos los métodos del ciclo de vida de la actividad principal de Asteroides y añadiremos un toast para mostrar cuando se ejecuta. De esta forma comprenderemos mejor cuando se llama a cada método.

1. Abre la actividad MainActivity del proyecto Asteroides o Mis Lugares.

2. Añade en el método onCreate() el siguiente código:

Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show();

3. Añade los siguientes métodos:

@Override protected void onStart() {
   super.onStart();
   Toast.makeText(this, "onStart", Toast.LENGTH_SHORT).show();
}
 
@Override protected void onResume() {
   super.onResume();
   Toast.makeText(this, "onResume", Toast.LENGTH_SHORT).show();
}
 
@Override protected void onPause() {
   Toast.makeText(this, "onPause", Toast.LENGTH_SHORT).show();
   super.onPause();
}
 
@Override protected void onStop() {
   Toast.makeText(this, "onStop", Toast.LENGTH_SHORT).show(); 
   super.onStop();
}
 
@Override protected void onRestart() {
   super.onRestart();
   Toast.makeText(this, "onRestart", Toast.LENGTH_SHORT).show();
}
 
@Override protected void onDestroy() {
   Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show(); 
   super.onDestroy();    
}

  

4. Ejecuta la aplicación y observa la secuencia de Toast.

5. Selecciona la opción Acerca de... y luego regresa a la actividad. Observa la secuencia de Toast.

6. Selecciona la opción Preferencias y luego regresa a la actividad. Observa la secuencia de Toast.

7. Sal de la actividad y observa la secuencia de Toast.

Preguntas de repaso y reflexión:

Ciclo de vida

 

 


¿Qué proceso se elimina?

Como hemos comentado, Android mantiene en memoria todos los procesos que quepan aunque estos no se estén ejecutando. Una vez que la memoria está llena y el usuario decide ejecutar una nueva aplicación, el sistema ha de determinar qué proceso de los que están en ejecución ha de ser eliminado. Android ordena los procesos en una lista jerárquica, asignándole a cada uno de ellos una determinada "importancia". Esta lista se confecciona basándose en los componentes de la aplicación que están corriendo (actividades y servicios) y el estado de estos componentes.

 

Para establecer esta jerarquía de importancia se distinguen los siguientes tipos de procesos:

Proceso de primer plano: (Foreground process) Hospeda una actividad en la superficie de la pantalla y con la cual el usuario está interactuando (su método onResume() ha sido llamado). Debería haber solo uno o unos pocos procesos de este tipo. Sólo serán eliminados cómo último recurso, si es que la memoria está tan baja que ni siquiera estos procesos pueden continuar corriendo.

Proceso visible: (Visible process) Hospeda una actividad que está visible en la pantalla, pero no en el primer plano (su método onPause() ha sido llamado). Considerado importante, no será eliminado a menos que sea necesario para mantener los procesos de primer plano.

Proceso de servicio: (Service process) Hospeda un servicio que ha sido inicializado con el métodostartService(). Aunque estos procesos no son directamente visibles al usuario, generalmente están haciendo tareas que para el usuario son importantes (tales como reproducir un archivo mp3 o mantener una conexión con un servidor de contenidos). El sistema siempre tratará de mantener esos procesos corriendo, a menos que los niveles de memoria comiencen a comprometer el funcionamiento de los procesos de primer plano o visibles.

Proceso de fondo: (Background process) Hospeda una actividad que no es actualmente visible al usuario (su método onStop() ha sido llamado). Si estos procesos son eliminados no tendrán un directo impacto en la experiencia del usuario. Como, hay muchos de estos procesos,  el sistema asegura que el último proceso visto por el usuario sea el último en ser eliminado.

Proceso vacío: (Empty process) No hospeda a ningún componente de aplicación activo. La única razón para mantener ese proceso es tener un "caché" que permita mejorar el tiempo de activación en la próxima vez que un componente de su aplicación sea ejecutado.

video[Tutorial] Ciclo de vida de los procesos en Android

video[Tutorial] Ciclo de vida de las aplicación en Android: Un ejemplo paso a paso

Práctica: Aplicando eventos del ciclo de vida en la actividad inicial.

Los conceptos referentes al ciclo de vida de una aplicación son imprescindible para el desarrollo de aplicaciones estables en Android. Para reforzar estos conceptos te proponemos el siguiente ejercicio en el que vamos a reproducir una música de fondo en la actividad principal.

1. Abre el proyecto Asteroides o Mis Lugares.

2. Busca un fichero de audio (en este capítulo se listan los formatos soportados por Android). Renombra este fichero a audio.xxx y cópialo a la carpeta res/raw.

NOTA: Cada vez que ejecutes el proyecto este fichero será añadido al paquete .apk. Si este fichero es muy grande la aplicación también lo será, lo que ralentizará su instalación. Para agilizar la ejecución te recomendamos un fichero muy pequeño, por ejemplo un .mp3 de corta duración o un fichero MIDI (.mid). Si no encuentras ninguno puedes descargar este: http://www.dcomg.upv.es/~jtomas/android/ficheros/audio.mid.

3. Abre la actividad MainActivity y declara el siguiente objeto:

MediaPlayer mp;
4. Añade las siguientes líneas en el método onCreate():
mp = MediaPlayer.create(this, R.raw.audio);
mp.start();

5. Ejecuta el proyecto y verifica que cuando sales de la actividad la música sigue sonando un cierto tiempo.

6. Utilizando los eventos del ciclo de vida queremos que cuando la actividad deje de estar activa el audio deje de escucharse. Puedes utilizar los métodos:

mp.pause();
mp.start();

7. Verifica que funciona correctamente.

Práctica: Aplicando eventos del ciclo de vida en la actividad inicial (II)

1. Tras realizar el ejercicio anterior, ejecuta la aplicación y abre la actividad Acerca de… La música ha de detenerse.

2. Nos interesa que mientras parte de esta actividad esté visible (como ha ocurrido en el punto anterior) la música se escuche. Es decir, utilizando los eventos del ciclo de vida queremos que cuando la actividad deje de estar visible el audio deje de escucharse.

3. Verifica que cuando abres la actividad Acerca de… la música continua reproduciéndose.

4. Pasa ahora a una actividad que ocupe la totalidad de la pantalla (por ejemplo  la actividad Juego o VistaLugarActivity). En teoría la música tendría que detenerse, dado que la actividad MainActivity ya no es visible y tendría que haber pasado a estado parada. Observa como la música acaba deteniéndose, pero es posible que tarde unos segundos. Esto se debe a que la llamada al método onStop() no es prioritaria, por lo que el sistema puede retardar su ejecución. En caso de tratarse de información visual, en lugar de acústica, este retardo no tendría una repercusión directa para el usuario, dado que la actividad no es visible.

5. Tras el problema detectado en el punto anterior, deshaz los cambios introducidos en esta práctica y deja la aplicación como se pedía en la práctica anterior.

Aplicando eventos del ciclo de vida en la actividad Juego de Asteroides

Ejercicio paso a paso: Aplicando eventos del ciclo de vida en la actividad Juego de Asteroides

Asteroides gestiona el movimiento de los objetos gráficos por medio de un thead que se ejecuta continuamente. Cuando la aplicación pasa a segundo plano, este thead continúa ejecutándose por lo que puede hacer que nuestro teléfono funcione más lentamente y además, gaste más batería.  Este problema aparece por una gestión incorrecta del ciclo de vida. El siguiente ejercicio veremos cómo solucionarlo:

1. Abre el proyecto Asteroides y ejecútalo en un terminal.

2.Pulsa el botón “Jugar” y cuando esté en mitad de la partida pulsa con el botón de Inicio (o Casa) para dejar a la actividad en estado parada. Las actividades en este estado no tendrían que consumir demasiados recursos. Sin embargo Asteroides sí que lo hace. Para verificarlos utiliza la aplicación “Administrador de tareas” (En las últimas versiones de Android, puedes abrirlo pulsando un segundo sobre el botón Casa y seleccionando el icono con forma de gráfico de tarta que aparece abajo a la izquierda).  El resultado puede ser similar al que se muestra a continuación.

NOTA: Si el administrador de tareas de tu terminal no te permite mostrar el porcentaje de uso de la CPU te recomendamos que instales un programa que te lo permita. Por ejemplo, OS Monitor.

Como puedes ver la aplicación Asteroides está consumiendo casi el 50% del uso de CPU. Evidentemente algo hemos hecho mal. No es lógico que cuando la actividad Juego esté en segundo plano se siga llamando a actualizaFisica().  En este ejercicio aprenderemos a solucionarlo.

3. Incluye la siguiente variable en la actividad Juego.

private VistaJuego vistaJuego;

4. Al final de onCreate añade:

vistaJuego = (VistaJuego) findViewById(R.id.VistaJuego);

5. Incorpora los siguientes métodos a la actividad:

@Override protected void onPause() {
   super.onPause();
   vistaJuego.getThread().pausar();
}
 
@Override protected void onResume() {
   super.onResume();
   vistaJuego.getThread().reanudar();
}
 
@Override protected void onDestroy() {
   vistaJuego.getThread().detener();
   super.onDestroy();
}

Lo que intentamos hacer con este código es poner en pausa el thread secundario cuando la actividad deje de estar activa y reanudarlo cuando la actividad recupere el foco. Además detener el thread cuando la actividad vaya a ser destruida.

NOTA: realmente el thread será destruido al destruirse la actividad que lo ha lanzado. No obstante puede resultar interesante hacerlo lo antes posible.

Observa como los métodos llamados por eventos del ciclo de vida solo pueden ser escritos un  Activity, por lo que no sería valido hacerlo en VistaJuego

6. Abre la clase VistaJuego y busca la definición de la clase ThreadJuego y reemplazala por la siguiente:

class ThreadJuego extends Thread {
   private boolean pausa,corriendo;
 
   public synchronized void pausar() {
          pausa = true;
   }
 
   public synchronized void reanudar() {
          pausa = false;
          notify();
   }
 
   public void detener() {
          corriendo = false;
          if (pausa) reanudar();
   }
  
   @Override public void run() {
          corriendo = true;
          while (corriendo) {
             actualizaFisica();
             synchronized (this) {
                while (pausa)
                   try {
                      wait();
                   } catch (Exception e) {
                   }
                }
             }
          }  
       }  
    }

Comenzamos declarando las variables pausa, corriendo. Estas pueden ser modificadas mediante los métodos pausar(), reanudar() y detener().

La palabra reservada synchronized impide el acceso de varios thread a una sección del código. En el capítulo anterior explicamos los hilos de ejecución.

El método run() se ha modificado de manera que en lugar de ser un bucle infinito, permitimos que termine poniendo la variable corriendo a false. Luego, tras llamar a actualiza Fisica(), se comprueba si se ha activado pausa. En tal caso, se entra en un bucle donde ponemos en espera el thread llamando al método wait(). Este quedará bloqueado hasta que se llame a notify(). Esta acción se realizará desde el método reanudar(). La llamada a wait() puede lanzar excepciones por lo que es obligatorio escribirla dentro de un bloque try {…} catch {…}.  

7. Dado que la variable thread es de tipo private, no puede ser manipulada desde fuera de VistaJuego. Para poder llamar a los métodos (pausar, reanudar,… ) de este objeto vamos a incluir un método getter. Para ello, sitúa el cursor justo antes de la última llave de VistaJuego. Pulsa con el botón derecho en el código y selecciona la opción Source / Generate Getters and Setters… Marca solo el método getThread() tal y como se muestra a continuación: 

Se insertará el siguiente código:

public ThreadJuego getThread() {
          return thread;

       }

8. Ejecuta de nuevo la aplicación y repite el segundo punto de este ejercicio. En este caso el resultado ha de ser similar al siguiente:

Práctica: Aplicando eventos del ciclo de vida en la actividad Juego para desactivar los sensores

En la unidad anterior aprendimos a utilizar los sensores para manejar la nave en Asteroides. El uso de sensores ha de realizarse con mucho cuidado dado su elevado consumo de batería. Resulta importante que cuando nuestra actividad quedara en un segundo plano se detuviera la lectura de los sensores, para que así no siga consumiendo batería.

1. Crea los métodos activarSensores() y desactivarSensores() en la clase VistaJuego.

2.  Mueve la línea de código que activa los sensores desde onCreate() al método activarSensores().

3.  Para desactivar los sensores hay que llamar al siguiente método:

mSensorManager.unregisterListener(SensorEventListener);

4. El parámetro de este método corresponde al objeto SensorEventListener del que queremos dejar de recibir eventos. En nuestro caso, nosotros mismos (this). Utiliza este método dentro de desactivarSensores().

5. Utiliza los métodos del ciclo de vida adecuados para activar o desactivar los sensores. Recuerda que estos métodos los recoge la activad, por lo que tendrás que incluirlos en la clase Juego.

@font-face {font-family:Wingdings; panose-1:5 0 0 0 0 0 0 0 0 0;} @font-face {font-family:"Cambria Math"; panose-1:2 4 5 3 5 4 6 3 2 4;} p.MsoNormal, li.MsoNormal, div.MsoNormal {margin-top:6.0pt; margin-right:0cm; margin-bottom:6.0pt; margin-left:0cm; text-align:justify; text-indent:17.0pt; font-size:10.0pt; font-family:"Arial","sans-serif";} h1 {mso-style-link:"Título 1 Car"; margin-top:66.0pt; margin-right:0cm; margin-bottom:60.0pt; margin-left:21.6pt; text-indent:-21.6pt; page-break-before:always; page-break-after:avoid; font-size:24.0pt; font-family:"Arial","sans-serif"; font-weight:normal;} h2 {mso-style-link:"Título 2 Car"; margin-top:24.0pt; margin-right:0cm; margin-bottom:12.0pt; margin-left:28.8pt; text-align:justify; text-indent:-28.8pt; page-break-after:avoid; font-size:14.0pt; font-family:"Arial","sans-serif";} h3 {mso-style-link:"Título 3 Car"; margin-top:18.0pt; margin-right:0cm; margin-bottom:6.0pt; margin-left:36.0pt; text-align:justify; text-indent:-36.0pt; page-break-after:avoid; font-size:12.0pt; font-family:"Arial","sans-serif";} h4 {mso-style-link:"Título 4 Car"; margin-top:12.0pt; margin-right:0cm; margin-bottom:8.0pt; margin-left:43.2pt; text-align:justify; text-indent:-43.2pt; page-break-after:avoid; font-size:11.0pt; font-family:"Arial","sans-serif";} p.MsoListParagraph, li.MsoListParagraph, div.MsoListParagraph {margin-top:6.0pt; margin-right:8.5pt; margin-bottom:3.0pt; margin-left:86.2pt; text-align:justify; text-indent:-18.0pt; font-size:10.0pt; font-family:"Arial","sans-serif";} span.MsoSubtleReference {font-variant:small-caps; color:#C0504D; text-decoration:underline;} span.Ttulo1Car {mso-style-name:"Título 1 Car"; mso-style-link:"Título 1"; font-family:"Arial","sans-serif";} span.Ttulo2Car {mso-style-name:"Título 2 Car"; mso-style-link:"Título 2"; font-family:"Arial","sans-serif"; font-weight:bold;} span.Ttulo3Car {mso-style-name:"Título 3 Car"; mso-style-link:"Título 3"; font-family:"Arial","sans-serif"; font-weight:bold;} span.Ttulo4Car {mso-style-name:"Título 4 Car"; mso-style-link:"Título 4"; font-family:"Arial","sans-serif"; font-weight:bold;} p.Cdigo, li.Cdigo, div.Cdigo {mso-style-name:Código; mso-style-link:"Código Car"; margin-top:2.0pt; margin-right:0cm; margin-bottom:2.0pt; margin-left:0cm; font-size:9.0pt; font-family:"Courier New"; color:#943634;} span.CdigoCar {mso-style-name:"Código Car"; mso-style-link:Código; font-family:"Courier New"; color:#943634;} p.Prrafonumerado, li.Prrafonumerado, div.Prrafonumerado {mso-style-name:"Párrafo numerado"; margin-right:8.5pt; margin-left:34.5pt; text-align:justify; text-indent:-34.5pt; font-size:10.0pt; font-family:"Arial","sans-serif";} p.Figuras, li.Figuras, div.Figuras {mso-style-name:Figuras; margin-top:6.0pt; margin-right:0cm; margin-bottom:10.0pt; margin-left:0cm; text-align:justify; text-indent:17.0pt; line-height:115%; font-size:9.0pt; font-family:"Arial","sans-serif"; font-style:italic;} p.ejercicio, li.ejercicio, div.ejercicio {mso-style-name:ejercicio; margin-top:0cm; margin-right:0cm; margin-left:46.4pt; text-indent:-18.0pt; line-height:115%; border:none; padding:0cm; font-size:10.0pt; font-family:"Arial","sans-serif";} p.Ejerciciotitulo, li.Ejerciciotitulo, div.Ejerciciotitulo {mso-style-name:"Ejercicio titulo"; margin-top:6.0pt; margin-right:0cm; margin-bottom:0cm; margin-left:0cm; margin-bottom:.0001pt; text-align:justify; text-indent:17.0pt; font-size:10.0pt; font-family:"Arial","sans-serif"; font-weight:bold; font-style:italic;} @page WordSection1 {size:595.3pt 841.9pt; margin:70.85pt 3.0cm 70.85pt 3.0cm;} div.WordSection1 {page:WordSection1;} ol {margin-bottom:0cm;} ul {margin-bottom:0c Guardando el estado de una actividad

Cuando el usuario ha estado utilizando una actividad, y tras cambiar a otras, regresa a la primera, lo habitual es que esta permanezca en memoria y continúe su ejecución sin alteraciones. Como hemos explicado, en situaciones de escasez de memoria, es posible que el sistema haya eliminado el proceso que ejecutaba la actividad. En este caso, el proceso será creado de nuevo, pero se habrá perdido su estado, es decir, se habrá perdido el valor de sus variables y el puntero de programa. Como consecuencia, si el usuario estaba a mitad de un proceso de edición o estaba reproduciendo un audio en un punto determinado perderá esta información. En este apartado estudiaremos un mecanismo sencillo que nos proporciona Android para resolver este problema.

NOTA: Cuando se ejecuta una actividad sensible a la inclinación del teléfono, es decir puede verse en horizontal o en vertical, se presenta un problema similar al anterior. La actividad es destruida y vuelta a construir con las nuevas dimensiones de pantalla y por lo tanto se llama de nuevo al método onCreate(). Antes de que la actividad sea destruida también resulta fundamental guardar su estado.

video[Tutorial] Guardar el estado de las actividades en  Android

Para guardar el estado de una actividad has de utilizar los siguientes dos métodos:

onSaveInstanceState(Bundle): Invoca el sistema cuando ha de destruir una actividad, que más adelante ha de restaurar (por cambiar su inclinación o por falta de memoria), para  permitir a la actividad guardar su estado.

onRestoreInstanceState(Bundle): Se invoca cuando se restaura la actividad para recuperar el estado guardado por onSaveInstanceState().

NOTA: Nunca utilices el método onSaveInstanceState() para guardar los datos generados por una actividad (como guardar un formulario en una base de datos). Este método es llamado cuando el sistema ha de destruir una actividad que va a ser recuperada en un futuro. Si esta actividad es destruida llamando a  finish() o pulsando el botón atrás, el método  onSaveInstanceState() no será llamado. Para guardar estos datos utiliza onPause() o onStop().

 

Veamos un ejemplo de lo sencillo que resulta guardar la información de una variable tipo cadena de caracteres y entero.

String var;
int pos;
 
@Override
protected void onSaveInstanceState(Bundle guardarEstado) {
   super.onSaveInstanceState(guardarEstado);
   guardarEstado.putString("variable", var);
   guardarEstado.putInt("posicion", pos);
}
 
@Override
protected void onRestoreInstanceState(Bundle recEstado) {
   super.onRestoreInstanceState(recEstado);
   var = recEstado.getString("variable");
   pos = recEstado.getInt("posicion");
}

 

Práctica: Guardando el estado enla actividad inicial.

1. Ejecuta el proyecto Asteroides o Mis Lugares.

2. Cambia de orientación el teléfono. Observarás como la música se reinicia cada vez que lo haces.

3. Utilizando los métodos para guardar el estado de una actividad, trata de que cuando se voltea el teléfono, el audio continúe en el mismo punto de reproducción. Puedes utilizar los siguientes métodos:

int pos = mp.getCurrentPosition();
mp.seekTo(pos);

4. Verifica el resultado.

Solución: A continuación se muestra una posible solución al ejercicio :

@Override protected void onSaveInstanceState(Bundle estadoGuardado){
   super.onSaveInstanceState(estadoGuardado);
   if (mp != null) {
      int pos = mp.getCurrentPosition();
      estadoGuardado.putInt("posicion", pos);   
   }
}
 
@Override protected void onRestoreInstanceState(Bundle estadoGuardado){
   super.onRestoreInstanceState(estadoGuardado);
   if (estadoGuardado != null && mp != null) {
      int pos = estadoGuardado.getInt("posicion");
      mp.seekTo(pos);
   }
}

Preguntas de repaso: Guardar estado

Utilizando multimedia en Android

La integración de contenido multimedia en nuestras aplicaciones resulta muy sencilla gracias a la gran variedad de facilidades que nos proporciona el API.

Concretamente podemos reproducir audio y vídeo desde orígenes distintos:

  • Desde un fichero almacenado en el dispositivo.
  • Desde un recurso que está incrustado en el paquete de la aplicación (fichero .apk).
  • Desde un stream que es leído desde una conexión de red. En este punto admite dos posibles protocolos (http:// y rstp://)

También resulta sencilla la grabación de audio y vídeo, siempre que el hardware del dispositivo lo permita.

En la siguiente lista se muestran las clases de Android que nos permitirán acceder a los servicios Multimedia:

MediaPayer: Reproducción de audio/vídeo desde ficheros o streams.

MediaControler: Visualiza controles estándar para  mediaPlayer (pausa, stop...).

VideoView: Vista que permite la reproducción de vídeo.

MediaRecorder: Permite grabar audio y vídeo.

AsyncPlayer: Reproduce lista de audios desde un thread secundario. 

AudioManager: Gestiona varias propiedades del sistema (volumen, tonos…).

AudioTrack: Reproduce un búfer de audio PCM directamente por hardware.

SoundPool: Maneja y reproduce una colección de recursos de audio.

JetPlayer: Reprocuce audio y video interactivo creado con JetCreator.

Camera: Cómo utilizar la cámara para tomar fotos y vídeo.

FaceDetector: Identifica la cara de la gente en un bitmap.

La plataforma Android soporta una gran variedad de formatos, muchos de los cuales pueden ser tanto decodificados como codificados. A continuación, mostramos una tabla con los formatos multimedia soportados. No obstante algunos modelos de móviles pueden soportar formatos adicionales que no se incluyen en la tabla, como por ejemplo DivX.

Cada desarrollador es libre de usar los formatos incluidos en el núcleo del sistema o aquellos que solo se incluyen en algunos dispositivos.

Tipo

Formato

Codifica

Decodifica

Detalles

fichero soportado

Audio

AAC LC/LTP

X

X

Mono/estéreo con cualquier combinación estándar de frecuencia > 160 Kbps y ratios de muestreo de 8 a 48kHz

3GPP (.3gp)
MPEG-4(.mp4)
No soporta raw AAC (.aac)
MPEG-TS (.ts)

HE-AACv1

a partir 4.1

X

HE-AACv2

 

X

AAC ELD

a partir 4.1

a partir 4.1

Mono/estéreo,
16-8kHz

AMR-NB

X

X

4.75 a 12.2 Kbps muestreada a @ 8kHz

3GPP (.3gp)

AMR-WB

X

X

9 ratios de 6.60 Kbps a 23.85 Kbps a @ 16kHz

3GPP (.3gp)

MP3

 

X

Mono/estéreo de 8 a 320 Kbps, bit rate constante (CBR) o variable (VBR)

MP3 (.mp3)

MIDI

 

X

MIDI tipo 0 y 1. DLS v1 y v2. XMF y XMF móvil. Soporte para tonos de llamada RTTTL / RTX, OTA y iMelody.

Tipo 0 y 1 (.mid, .xmf, .mxmf).
RTTTL / RTX
(.rtttl, .rtx),
OTA (.ota)
iMelody (.imy)

Ogg Vorbis

 

X

 

Ogg (.ogg)

Matroska (.mkv
a partir 4.0)

FLAC

 

a partir 3.1 mono/estereo
(no multicanal)

FLAC (.flac)

PCM/WAVE

a partir 4.1

X

8 y 16 bits PCM lineal (frecuencias limitadas por el hardware)

WAVE (.wav)

Imagen

JPEG

X

X

Base + progresivo

JPEG (.jpg)

GIF

 

X

 

GIF (.gif)

PNG

X

X

 

PNG (.png)

BMP

 

X

 

BMP (.bmp)

WEBP

a partir 4.0

a partir 4.0

 

WebP (.webp)

Video

H.263

X

X

 

3GPP (.3gp)

MPEG-4 (.mp4)

H.264 AVC

a partir 3.0

X

 Baseline Profile (BP)

3GPP (.3gp)
MPEG-4 (.mp4)

MPEG-4 SP

 

X

 

3GPP (.3gp)

VP8

a partir 4.3

a partir 2.3.3

 Streaming a partir 4.0

WebM (.webm)

Matroska (.mkv)

Formatos multimedia soportados en Android.

video[Tutorial Multimedia en Android

Preguntas de repaso : Multimedia

La vista VideoView

La forma más fácil de incluir un vídeo en tu aplicación es incluir una vista de tipo VideoView. Veamos cómo hacerlo en el siguiente ejercicio    

Ejercicio paso a paso: Reproducir un vídeo con VideoView

1. Crea una nueva aplicación.

2. Reemplaza el fichero ref/layout/activity_main.xml por

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <VideoView
        android:id="@+id/surface_view"
        android:layout_width="320px"
        android:layout_height="240px"/> 
</LinearLayout>

3. Reemplaza el siguiente código en la clase MainActivity:

public class videoView extends Activity {
private VideoView mVideoView;

@Override public void onCreate(Bundle savedInstanceState){
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       mVideoView =(VideoView)findViewById(R.id.surface_view);
       //de forma alternativa si queremos un streaming usar
       //mVideoView.setVideoURI(Uri.parse(URLstring));
       mVideoView.setVideoPath("/mnt/sdcard/video.mp4");
       mVideoView.start();
       mVideoView.requestFocus();
}
}

En el parámetro del método setVideoPath() estamos indicando un fichero local.

4. Busca un fichero de video en codificación mp4. Renombra este fichero a video.mp4.

5. Es necesario almacenar un fichero de video en el simulador. Para ello, utiliza la opción del menú Window > Show View >Others… > Android > File Explorer. Te mostrará el sistema de ficheros del emulador.

6. Selecciona la carpeta mnt/sdcard y utiliza el botón donde aparece un teléfono con una flecha para almacenar un nuevo fichero. Indica el fichero video.mp4.

NOTA: el dispositivo ha de disponer de almacenamieno externo.

7. Ejecuta la aplicación y observa el resultado. Recuerda que con Ctrl-F11 puedes cambiar la orientación. Si no funciona trata de usar otro tipo de emulador.

8. Modifica el fichero xml para que el vídeo aparezca centrado y ocupe toda la pantalla del teléfono, tal y como se muestra en la imagen de arriba

9. Añade la siguiente línea antes de la llamada al método start();

mVideoView.setMediaController(new MediaController(this));

De esta forma permitimos que el usuario pueda controlar la reproducción del vídeo mediante el objeto MediaController.

10. Observa el resultado.

NOTA: Algunos emuladores pueden dar problemas a la hora de reprocucir vídeos.

La clase MediaPlayer

La reproducción multimedia en Android se lleva a cabo principalmente por medio de la claseMediaPlayer. Veremos a continuación las características más importantes de esta clase y cómo podemos sacarle provecho.

Un objeto MediaPlayer va a poder pasar por gran variedad de estados: inicializados sus recursos (initialized), preparando la reproducción (preparing), preparado para reproducir (prepared), reproduciendo (started), en pausa (paused), parado (stopped), reproducción completada (playback completed), finalizado (end) y con error (error). Resulta importante conocer en qué estado se encuentra dado que muchos de los métodos solo pueden ser llamados desde ciertos estados. Por ejemplo, no podemos ponerlo en reproducción (método start()) sino está en estado preparado. O no podremos ponerlo en pausa (pause()), si está parado. Si llamamos a un método no admitido para un determinado estado se producirá un error de ejecución.

La siguiente tabla permite conocer los métodos que podemos invocar desde cada uno de los estados y cuál es el nuevo estado al que iremos tras invocarlo. Existen dos tipos de métodos, los que no están subrayados representan métodos llamados de forma síncrona desde nuestro código, mientras que los que están subrayados representan métodos llamados de forma asíncrona por el sistema.

   

Estado salida

Estado entrada

Idle

Initialized

Preparing

Prepared

Started

Paused

Stopped

Playback Completed

Initialized

setDataSource

 

 

 

 

 

 

 

Preparing

 

prepareAsync

 

 

 

 

prepareAsync

 

Prepared

 

prepare

onPrepared

seekTo

 

 

prepare

 

Started

 

 

 

start

seekTo
start

start

 

start

Paused

 

 

 

 

pause

seekTo
pause

 

 

Stopped

 

 

 

stop

stop

stop

stop

stop

Playback Completed

 

 

 

 

onCompletion

 

 

seekTo

End

Release

release

release

release

release

release

release

release

Error

onError

onError

onError

onError

onError

onError

onError

onError

Tabla 6: Transiciones entre estados de la clase MediaPlayer

function comprobarRespuesta(correcta,radio,sicorrecto,sinocorrecto,iconoTick,iconoWrong){ var tmpChoice = radio; var c = radio.length; var correct=0;for (i = 0; i < c; i++) { if (tmpChoice[i].checked == true) { if (tmpChoice[i].value == correcta.value) {sicorrecto.style.visibility = 'visible'; sinocorrecto.style.visibility = 'hidden'; iconoWrong.style.visibility = 'hidden'; iconoTick.style.visibility = 'visible';} else{sinocorrecto.style.visibility = 'visible'; sicorrecto.style.visibility = 'hidden'; iconoWrong.style.visibility = 'visible'; iconoTick.style.visibility = 'hidden'}}}}; Reproducción de audio con MediaPlayer

Si el audio o vídeo se va a reproducir siempre en nuestra aplicación resulta interesante incluirlo en el paquete .apk y tratarlo como un recurso. Este uso ya ha sido ilustrado al comienzo del capítulo. Recordemos como se hacía:

1.     Crea una nueva carpeta con nombre raw dentro de la carpeta res.

2.     Arrastra a su interior el fichero de audio. Por ejemplo audio.mp3.

3.     Añade las siguientes líneas de código:

 MediaPlayer mp = MediaPlayer.create(this, R.raw.audio);
     mp.start();

Si deseas parar la reproducción tendrás que utilizar el método stop() . Si a continuación quieres volver a reproducirlo utiliza el método prepare() y a continuación start(). También puedes usar pause() ystart() para ponerlo en pausa y reanudarlo.

Si en lugar de un recurso prefieres reproducirlo desde un fichero utiliza el siguiente código. Observa como en este caso es necesario llamar al método prepare(). En el caso anterior no ha sido necesario dado que esta llamada se hace desde create().

    MediaPlayer mp = new MediaPlayer();
    mp.setDataSource(RUTA_AL_FICHERO);
    mp.prepare();
    mp.start();

    Un reproductor multimedia paso a paso

En el siguiente ejercicio vamos a profundizar en el objeto MediaPlayer por medio de un ejemplo, donde trataremos de realizar un reproductor de vídeos personalizado.

Ejercicio paso a paso: Un reproductor multimedia pasó a paso

1. Crea una nueva aplicación con los siguientes datos:

Project name: VideoPlayer

Application name: VideoPlayer

Package name: org.example.videoplayer

Create Activity: VideoPlayer

Min SDK Version: 3

2. En la carpeta res/drawable  arrastra los cuatro ficheros de iconos: play.png, pause.png, stop.png y log.png Los puedes encontrar en [1].

play.png pause.png stop.png log.png

3. Reemplaza el fichero el fichero res/layout/activity_main.xml por:

<RelativeLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_height="match_parent"
       android:layout_width="match_parent">
    <LinearLayout
            android:id="@+id/ButonsLayout"
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:orientation="horizontal"
            android:layout_alignParentTop="true">
        <ImageButton android:id="@+id/play"
                     android:layout_height="wrap_content"
                     android:layout_width="wrap_content"
                     android:src="@drawable/play"/>
        <ImageButton android:id="@+id/pause"
                     android:layout_height="wrap_content"
                     android:layout_width="wrap_content"
                     android:src="@drawable/pause"/>
        <ImageButton android:id="@+id/stop"
                     android:layout_height="wrap_content"
                     android:layout_width="wrap_content"
                     android:src="@drawable/stop"/>
        <ImageButton android:id="@+id/logButton"
                     android:layout_height="wrap_content"
                     android:layout_width="wrap_content"
                     android:src="@drawable/log"/>
        <EditText    android:id="@+id/path"
                     android:layout_height="match_parent"
                     android:layout_width="match_parent"
                     android:text="/data/video.3gp"/>
    </LinearLayout>
    <VideoView android:id="@+id/surfaceView"
                android:layout_height="match_parent"
                android:layout_width="match_parent"
                android:layout_below="@+id/ButonsLayout"/>
    <ScrollView android:id="@+id/ScrollView"
                android:layout_height="100px"
                android:layout_width="match_parent"
                android:layout_alignParentBottom="true">
        <TextView    android:id="@+id/Log"
                     android:layout_height="wrap_content"
                     android:layout_width="match_parent"
                     android:text="Log:"/>
    </ScrollView>
</RelativeLayout>

La apariencia del Layout anterior se muestra a continuación:

4. Reemplaza el código de la clase VideoPlayer por:

public class VideoPlayer extends Activity implements 
                    OnBufferingUpdateListener, OnCompletionListener, 
                    MediaPlayer.OnPreparedListener, SurfaceHolder.Callback {
       private MediaPlayer mediaPlayer;
       private SurfaceView surfaceView;
       private SurfaceHolder surfaceHolder;
       private EditText editText;
       private ImageButton bPlay, bPause, bStop, bLog;
       private TextView logTextView;
       private boolean pause;
       private String path;
       private int savePos = 0;
 
       public void onCreate(Bundle bundle) {
             super.onCreate(bundle);
             setContentView(R.layout.activity_main);
             surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
             surfaceHolder = surfaceView.getHolder();
             surfaceHolder.addCallback(this);
             surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
             editText = (EditText) findViewById(R.id.path);
             editText.setText( 
                    "http://personales.gan.upv.es/~jtomas/video.3gp");
             No tengo los derechos, pero es un tráiler, creo que no hace falta

             logTextView = (TextView) findViewById(R.id.Log);
             bPlay = (ImageButton) findViewById(R.id.play);
             bPlay.setOnClickListener(new OnClickListener() {
                 public void onClick(View view) {
                    if (mediaPlayer != null) {
                        if (pause) {
                            mediaPlayer.start();
                        } else {
                            playVideo();
                        }
                     }
                  }
             });
             bPause = (ImageButton) findViewById(R.id.pause);
             bPause.setOnClickListener(new OnClickListener() {
                  public void onClick(View view) {
                     if (mediaPlayer != null) {
                         pause = true;
                         mediaPlayer.pause();
                     }
                  }
             });
             bStop = (ImageButton) findViewById(R.id.stop);
             bStop.setOnClickListener(new OnClickListener() {
                    public void onClick(View view) {
                       if (mediaPlayer != null) {
                           pause = false;
                           mediaPlayer.stop();
                      }
                  }
             });
             bLog = (ImageButton) findViewById(R.id.logButton);
             bLog.setOnClickListener(new OnClickListener() {
                    public void onClick(View view) {
                       if (logTextView.getVisibility()==TextView.VISIBLE) {
                             logTextView.setVisibility(TextView.INVISIBLE);
                       } else {
                             logTextView.setVisibility(TextView.VISIBLE);
                       }
                   }
             });
             log("");
       }

Como puedes ver la aplicación extiende la clase Activity. Además implementamos cuatro interfaces que corresponden a varios escuchadores de eventos. Luego continúa la declaración de variables. Las primeras corresponden a diferentes elementos de la aplicación y su significado resulta obvio. La variable pause nos indica si el usuario ha pulsado el botón correspondiente, la variable path nos indica dónde está el vídeo en reproducción y la variable savePos almacena la posición de reproducción.

5. Añade:

private void playVideo() {
         try {
             pause = false;
             path = editText.getText().toString();
             mediaPlayer = new MediaPlayer();
             mediaPlayer.setDataSource(path);
             mediaPlayer.setDisplay(surfaceHolder);
             mediaPlayer.prepare();
             // mMediaPlayer.prepareAsync(); Para streaming
             mediaPlayer.setOnBufferingUpdateListener(this);
             mediaPlayer.setOnCompletionListener(this);
             mediaPlayer.setOnPreparedListener(this);
             mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
             mediaPlayer.seekTo(savePos);
          } catch (Exception e) {
             log("ERROR: " + e.getMessage());
          }
       }

El código continúa con la definición del método playVideo(). Este método se encarga de obtener la ruta de reproducción, crear un nuevo objeto MediaPlayer, luego se le asigna la ruta y la superficie de visualización, a continuación se prepara la reproducción del vídeo. En caso de querer reproducir un stream desde la red, esta función puede tardar bastante tiempo, en tal caso es recomendable utilizar en su lugar el método prepareAsync() que permite continuar con la ejecución del programa, aunque sin esperar a que el vídeo esté preparado. Las siguientes tres líneas asignan a nuestro objeto varios escuchadores de eventos que serán descritos más adelante. Tras preparar el tipo de audio, se sitúa la posición de reproducción a los milisegundos indicados en la variable savePos. Si se trata de una nueva reproducción, esta variable será cero.

6.  Añade el código:

public void onBufferingUpdate(MediaPlayer arg0, int percent) {
             log("onBufferingUpdate percent:" + percent);
       }
 
       public void onCompletion(MediaPlayer arg0) {
             log("onCompletion called");
       }

 Los métodos anteriores implementan los interfaces OnBufferingUpdateListener y OnCompletionListener. El primero irá mostrando el porcentaje de obtención de búfer de reproducción, mientras que el segundo será invocado cuando el vídeo en reproducción llegue al final.

7. Añade el código:

public void onPrepared(MediaPlayer mediaplayer) {
    log("onPrepared called");
    int mVideoWidth = mediaPlayer.getVideoWidth();
    int mVideoHeight = mediaPlayer.getVideoHeight();
    if (mVideoWidth != 0 && mVideoHeight != 0) {
        surfaceHolder.setFixedSize(mVideoWidth, mVideoHeight);
        mediaPlayer.start();
    }
 }

El método anterior implementa la interfaz OnPreperedListener. Es invocado una vez que el vídeo ya está preparado para su reproducción. En este momento podemos conocer el alto y el ancho del vídeo y ponerlo en reproducción.

8. Añade el código:

public void surfaceCreated(SurfaceHolder) {
          log("surfaceCreated called");
          playVideo);
       }
 
       public void surfaceChanged(SurfaceHolder surfaceholder, 
                                                   int i, int j, int k) {
             log("surfaceChanged called");
       }
 
       public void surfaceDestroyed(SurfaceHolder surfaceholder) {
             log("surfaceDestroyed called");
       }

Los métodos anteriores implementan la interfaz SurfaceHolder.Callback. Se invocarán cuando nuestra superficie de visualización se cree, cambie o se destruya. Los métodos que siguen corresponden a acciones del ciclo de vida de una actividad:

9.  Añade el código:

@Override protected void onDestroy() {
         super.onDestroy();
         if (mediaPlayer != null) {
             mediaPlayer.release();
             mediaPlayer = null;
         }
                 }

Este método se invoca cuando la actividad va a ser destruida. Dado que un objeto de la clase MediaPlayer consume muchos recursos, resulta interesante liberarlos lo antes posible.

.10.  Añade el código:

@Override public void onPause() {
   super.onPause();
   if (mediaPlayer != null & !pause) {
       mediaPlayer.pause();
   }
}
@Override public void onResume() {
   super.onResume();
   if (mediaPlayer != null & !pause) {
       mediaPlayer.start();
   }
}

 Los dos métodos anteriores se invocan cuando la actividad pasa a un segundo plano y cuando vuelve a primer plano. Dado que queremos que el vídeo deje de reproducirse y continúe reproduciéndose en cada uno de estos casos, se invocan a los métodos pause() y start(), respectivamente. No hay que confundir esta acción con la variable pause que lo que indica es que el usuario ha pulsado el botón correspondiente.

      11.  Añade el código:

@Override
  protected void onSaveInstanceState(Bundle guardarEstado) {
     super.onSaveInstanceState(guardarEstado);
     if (mediaPlayer != null) {
         int pos = mediaPlayer.getCurrentPosition();
         guardarEstado.putString("ruta", path);
         guardarEstado.putInt("posicion", pos);
     }
   }
 
   @Override
   protected void onRestoreInstanceState(Bundle recEstado) {
      super.onRestoreInstanceState(recEstado);
      if (recEstado != null) {
          path = recEstado.getString("ruta");
          savePos = recEstado.getInt("posicion");
      }
   }

Cuando este sistema necesita memoria, puede decidir eliminar nuestra actividad. Antes de hacerlo llamará al método onSaveInstanceState para darnos la oportunidad de guardar información sensible. Si más adelante el usuario vuelve a la aplicación, esta se volverá a cargar, invocándose el método onRestoreInstanceState, donde podremos restaurar el estado perdido. En nuestro caso la información a guardar son las variables path y savePos, que representan el vídeo y la posición que estamos reproduciendo.

Ocurre el mismo proceso cuando el usuario cambia la posición del teléfono. Es decir, cuando el teléfono se voltea las actividades son destruidas y vueltas a crear, por lo que también se invocan estos métodos.

12. Añade el código:

 private void log(String s) {
         logTextView.append(s + "\n");
   }
}

El último método es utilizado por varios escuchadores de eventos para mostrar información sobre lo que está pasando. Esta información puede visualizarse o no, utilizando el botón correspondiente.

13. Solicita  el permiso para accede a Internet añadiendo en AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>

Introduciendo efectos de audio con SoundPool

Hemos aprendido a utilizar la clase MediaPlayer para reproducir audio y video. En un primer ejercicio vamos a aprender a utilizarla para introducir efectos de audio en Asteroides. Como veremos esta clase no resulta adecuada para este uso. A continuación se introducirá la clase SoundPool y se mostrará mediante un ejercicio como esta sí que resulta eficiente para nuestro juego.

Ejercicio paso a paso: Introduciendo efectos de audio con MediaPlayer en Asteroides.

1. Abre el proyecto Asteroides.

2. Copia los siguientes ficheros a la carpeta res/raw  disparo.mp3  y explosion.mp3. [1].

3. Abre la clase VistaJuego y añade las siguientes variables:

MediaPlayer mpDisparo, mpExplosion;

4. En el constructor de la clase inicialízalas de la siguiente forma:.

mpDisparo = MediaPlayer.create(context, R.raw.disparo);

      mpExplosion = MediaPlayer.create(context, R.raw.explosion);

5. Añade en el método ActivaMisil() de VistaJuego:

    mpDisparo.start();

6.  Añade en el método destruyeAsteroide() de VistaJuego:

    mpExplosion.start();

7.  Ejecuta el programa y verifica el resultado. ¿Qué pasa cuando disparamos o destruimos un asteroide? ¿El sonido se oye de forma inmediata?

La clase SoundPool maneja y reproduce de forma rápida recursos de audio en las aplicaciones.

Un SoundPool es una colección de pistas de audio que se pueden cargar en la memoria desde un recurso (dentro de la APK) o desde el sistema de archivos. SoundPool utiliza el servicio de la clase MediaPlayer para descodificar el audio en un formato crudo (PCM de16 bits), lo que después permite reproducirlo rápidamente por el hardware sin tener que decodificarlas cada vez.

Los sonidos pueden repetirse en un bucle una cantidad establecida de veces, definiendo el valor al reproducirlo, o dejarse reproduciendo en un bucle infinito con -1. En este caso, será necesario detenerlo con el método stop().

La velocidad de reproducción también se puede cambiar. El rango de velocidad de reproducción va de 0.5 a 2.0. Una tasa de reproducción de 1.0 hace que el sonido se reproduzca en su frecuencia original. Una tasa de reproducción de 2.0 hace que el sonido se reproduzca al doble de su frecuencia original, y una tasa de reproducción de 0.5 hace que se reproduzca a la mitad de su frecuencia original.

Cuando se crea un SoundPool hay que establecer en un parámetro del constructor el máximo de pistas que se pueden reproducir simultáneamente. Este parámetro no tiene por qué coincidir con el número de pistas cargadas. Cuando se pone una pista en reproducción (método play()) hay que indicar una prioridad. Esta prioridad se utiliza para decidir que se hará cuando el número de reproducciones activas exceda el valor máximo establecido en el constructor. En este caso, se detendrá el flujo con la prioridad mas baja, y en caso de que haya varios, se detendrá el más antiguo. En caso de que el nuevo flujo sea el de menor prioridad, este no se reproducirá.

Una lisita de todos los métodos de esta clase la puedes encontrar en el siguiente link:

http://developer.android.com/reference/android/media/SoundPool.html

 

Ejercicio paso a paso: Introduciendo efectos de audio con SoundPool en Asteroides

1.  En proyecto Asteroides elimina todo el código introducido a partir del punto 3, del ejercicio anterior.

2.  Abre la clase VistaJuego y añade las siguientes variables:

// //// MULTIMEDIA //////

SoundPool soundPool;

int idDisparo, idExplosion;

3.  En el constructor de la clase inicialízalas de la siguiente forma:

soundPool = new SoundPool( 5, AudioManager.STREAM_MUSIC , 0);

idDisparo = soundPool.load(context, R.raw.disparo, 0);

idExplosion = soundPool.load(context, R.raw.explosion, 0);

En el constructor de SoundPool hay que indicar tres parámetros. El primero corresponde al máximo de reproducciones simultáneas. El segundo es el tipo de stream de audio (normalmente STREAM_MUSIC). El tercero es la calidad de reproducción, aunque actualmente no se implementa.

Cada una de las pistas ha de ser cargada mediante el método load(). Existen muchas sobrecargas para este método. La versión utilizada en el ejemplo permite cargar las pistas desde los recursos. El último parámetro corresponde a la prioridad, aunque actualmente no tiene ninguna utilidad.

4. Añade en el método ActivaMisil() de VistaJuego:

soundPool.play(idDisparo, 1, 1, 1, 0, 1);

El método play() permite reproducir una pista. Hay que indicarle el identificador de pista; el volumen para el canal izquierdo y derecho (0.0 a 1.0); La prioridad; El número de repeticiones (-1= siempre, 0=solo una vez, 1=repetir una vez, …  )  y el ratio de reproducción, con el que podremos modificar la velocidad o pitch (1.0 reproducción normal, rango: 0.5 a 2.0)

5.  Añade en el método destruyeAsteroide() de VistaJuego:

       soundPool.play(idExplosion, 1, 1, 0, 0, 1);

Los parámetros utilizados para la explosión son similares, solo hemos introducido una prioridad menor. La consecuencia será que si ya hay un total de 5 (ver constructor) pistas reproduciéndose y se pide la reproducción de un nuevo disparo. El sistema detendrá la reproducción de la explosión más antigua, por tener esta menos prioridad.

7 Ejecuta el programa y verifica el resultado. ¿Qué pasa cuando disparamos o destruimos un asteroide? ¿El sonido se oye de forma inmediata?

8.  Modifica algunos de los parámetros del método play() y verifica el resultado.

Grabación de audio

Las APIs de Android disponen de facilidades para capturar audio y video, permitiendo su codificación en diferentes formatos. La clase MediaRecorder te permitirá de forma sencilla integrar esta funcionalidad en tu aplicación.

La mayoría de dispositivos disponen de micrófono para capturar audio, sin embargo esta facilidad no ha sido integrada en el emulador. Por lo tanto, has de probar los ejercicios de este apartado en un dispositivo real.

La clase MediaRecorder dispone de una serie de métodos que podrás utilizar para configurar la grabación:

setAudioSource(int audio_source) – Dispositivo que se utilizará como fuente del sonido. Normalmente MediaRecorder.AudioSource.MIC. También se pueden utilizar otras constantes como DEFULT, CAMCORDER, VOICE_CALL, VOICE_COMUNICATION, …

setOutputFile (String fichero) – Nombre del fichero de salida.

setOutputFormat(int output_forma) – Formato del fichero de salida. Se pueden utilizar constantes de la clase MediaRecorder.OutputFormat: DEFAULT, AMR_NB, AMR_WB, RAW_AMR (ARM), MPEG_4(MP4) y THREE_GPP (3GPP).

setAudioEncoder(int audio_encoder) – Codificación del audio. Cuatro posibles constantes de la clase MediaRecorder.AudioEncoder: AAC, AMR_NB, AMR_WB y DEFAULT.

setAudioChannels(int numeroCanales) – Especificamos el número de canales 1: mono y 2: estéreo.

setAudioEncodingBitRate (int bitRate) (desde nivel de API 8) – Especificamos los bits por segundo (bps) a utilizar en la codificación.

setAudioSamplingRate (int samplingRate)  (desde nivel de API 8) – Especificamos el número de muetras por segundo a utilizar en la codificación.

setMaxDuration (int max_duration_ms) (desde nivel de API 3) – Indicamos una duración máxima para la grabación. Tras este tiempo se detendrá.

setMaxFileSize (long max_filesize_bytes) (desde nivel de API 3) – Indicamos un tamaño máximo para el fichero. Al alcanzar el tamaño se detendrá.

prepare() – Prepara la grabación para la captura de datos. Antes de llamarlo hay que configurar la grabación y después ya podemos invocar al método start().

start() – Comienza la captura

stop() – Finaliza la captura

reset() – Reinicia el objeto como si lo acabáramos de crear. Hay que volver a configurarlo.

release() – Libera todos los recursos utilizados de forma inmediata. Si no llamas al método se liberarán cuando el objeto sea destruido.

La clase MediaRecorder también dispone de métodos que podrás utilizar para configurar la grabación de video. Más información en: http://developer.android.com/reference/android/media/MediaRecorder.html

La siguiente tabla muestra los diferentes métodos que pueden ser ejecutados en cada estado y el estado que alcanzaremos tras la llamada:

 

Estado salida

 

 

Estado entrada

 

Initial

Initialized

DataSource Configured

Prepared

Recording

Error

 

Initial

 

reset

reset

reset

reset
stop

reset

 

Initialized

setAudioSource
setVideoSource

setAudioSource
setVideoSource

 

 

 

 

 

DataSource Configured

 

setOutputFormat

setAudioEncoder
setVideoEncoder
setOutputFile
setVideoSize
setVideoFrameRate
setPreviewDisplay

 

 

 

 

Prepared

 

 

prepare

     

 

Recording

 

 

 

start

   

 

Released

releas

 

 

     

 

Error

llamada incorrecta

llamada incorrecta

llamada 
incorrecta

llamada incorrecta

llamada incorrecta

   

   Tabla 7: Transiciones entre estados de la clase MediaRecorder

 

   Ejercicio paso a paso: Grabación de audio utilizando MediaRecoder

1. Crea un nuevo proyecto con nombre Grabadora. El nombre del paquete ha de ser org.example.grabadora y como actividad principal GrabadoraActivity.

2. Reemplaza el layout activity_main.xml por el siguiente código:

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

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="horizontal">

    <Button

        android:id="@+id/bGrabar"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="Grabar"

        android:onClick="grabar"/>

    <Button

        android:id="@+id/bDetener"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="Detener Grabacion"

        android:onClick="detenerGrabacion"/>

    <Button

        android:id="@+id/bReproducir"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="Reproducir"

        android:onClick="reproducir"/>

</LinearLayout>

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

public class GrabadoraActivity extends Activity { 

private static final String LOG_TAG = "Grabadora";         
private MediaRecorder mediaRecorder;
private MediaPlayer mediaPlayer;

private static String fichero = Environment.                 
getExternalStorageDirectory().getAbsolutePath()+
"/audio.3gp";

   
@Override public void onCreate(Bundle savedInstanceState) {

   super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

 }

 

 public void grabar(View view) {

    mediaRecorder = new MediaRecorder();

    mediaRecorder.setOutputFile(fichero);

    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

    mediaRecorder.setOutputFormat(MediaRecorder.
                                          OutputFormat.
THREE_GPP);

    mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.
                                                     AMR_NB); 

    try {

        mediaRecorder.prepare();

    } catch (IOException e) {

        Log.e(LOG_TAG, "Fallo en grabación");

    }

    mediaRecorder.start();

  }

 

  public void detenerGrabacion(View view) {

     mediaRecorder.stop();

     mediaRecorder.release();

  }

 
 
public void reproducir(View view) {

     mediaPlayer = new MediaPlayer();

     try {

         mediaPlayer.setDataSource(fichero);

         mediaPlayer.prepare();

         mediaPlayer.start();

     } catch (IOException e) {

         Log.e(LOG_TAG, "Fallo en reproducción");

     }

   }

}

Comenzamos declarando dos objetos de las clase MediaRecorder y MediaPlayer que serán usados para la grabación y reproducción respectivamente. También se declaran dos String: La constante LOG_TAG será utilizada como etiqueta para identificar nuestra aplicación en el fichero de Log. La  variable ficheroidentifica en nombre del fichero donde se guardará la grabación. Este fichero se almacenará en una carpeta especialmente creada para nuestra aplicación. NOTA: En la unidad 8 se introducirá la gestión de ficheros. En el método onCreate() no se realiza ninguna acción especial.

A continuación se han introducido tres métodos que serán ejecutados al pulsar los botones de nuestro Layout. El significado de cada uno de los métodos que se invocan acaba de ser explicado.

4.  Abre AndroidManifest.xml y en la pestaña Permissions pulsa el botón Add… selecciona Uses Permissions. En el desplegable Name indica RECORD_AUDIO.  Repite este proceso para usar el permiso WRITE_EXTERNAL_STORAGE.

5.  Ejecuta de nuevo la aplicación y verifica el resultado.

Ejercicio paso a paso: Grabación de audio utilizando MediaRecoder (II)

La aplicación anterior resulta algo confusa de utilizar. Sería más sencillo si los botones que no pudiéramos utilizar en un determinado momento estuvieran deshabilitados. Para conseguirlo vamos a utilizar el método Button.setEnabled(boolean). Veamos como conseguirlo:

1. Declara las siguientes tres variables al principio de la Actividad:

private Button bGrabar, bDetener, bReproducir;

2. En el método onCreate() inicializa estos tres botones. Además comenzaremos deshabilitando dos de los botones que al principio de la aplicación no deben ser pulsados:

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

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

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

bDetener.setEnabled(false);

bReproducir.setEnabled(false);

3.  En el método grabar() añade el siguiente código:

bGrabar.setEnabled(false);

bDetener.setEnabled(true);

bReproducir.setEnabled(false);

4.  En el método detenerGrabacion() añade el siguiente código:

bGrabar.setEnabled(true);

bDetener.setEnabled(false);

bReproducir.setEnabled(true);

5. Ejecuta de nuevo la aplicación y verifica el resultado.

Preguntas de repaso y reflexión:

Multimedia