Desarrollo de Aplicaciones Para Android II
May 12, 2017 | Author: eltiotass | Category: N/A
Short Description
Descripción: Desarrollo de Aplicaciones Para Android...
Description
COLECCIÓN AULA MENTOR
SERIE PROGRAMACIÓN
CamSp
SGALV
Desarrollo de aplicaciones para Android II
Ministerio de Educación, Cultura y Deporte
Desarrollo de Aplicaciones para Android II Programación
Catálogo de publicaciones del Ministerio: www.educacion.gob.es Catálogo general de publicaciones oficiales: www.publicacionesoficiales.boe.es
Autor David Robledo Fernández
Coordinación pedagógica Hugo Alvarez
Edición y maquetación de contenidos Hugo Alvarez
Diseño gráfico e imagen Almudena Bretón
NIPO: 030-14-019-1 ISBN: 978-84-369-5541-5
ÍNDICE
Pág.
Unidad 0. Introducción ����������������������������������������������������������������������������������������������� 11 1. ¿Por qué un curso avanzado de Android?................................................................11 2. Cambios en las últimas versiones de Android .......................................................11 3. La simbiosis de Android y Linux..............................................................................13 4. Instalación del Entorno de Desarrollo ....................................................................16 4.1 ¿Qué es Eclipse?������������������������������������������������������������������������������������������������������������������� 16 4.2 Instalación de Java Development Kit ( JDK)���������������������������������������������������������������������� 16 4.3 Instalación de Eclipse ADT������������������������������������������������������������������������������������������������� 18 5. Añadir versiones y componentes de Android...........................................................23 6. Definición del dispositivo virtual de Android..........................................................26 Unidad 1. Multimedia y Gráficos en Android�������������������������������������������������������������� 33 1. Introducción.............................................................................................................33 2. Android Multimedia.................................................................................................33 3. Librerías de reproducción y grabación de audio.....................................................36 3.1 Clase SoundPool������������������������������������������������������������������������������������������������������������������ 36 3.2 Clase MediaPlayer���������������������������������������������������������������������������������������������������������������� 37 3.3 Clase MediaRecorder����������������������������������������������������������������������������������������������������������� 39 3.3.1 Ejemplo de reproducción y grabación de audio������������������������������������������������������� 40 3.4 Cómo habilitar USB Debugging en Android 4.2 y superior Jelly Bean���������������������������� 49 3.5 Librería de reproducción de vídeo������������������������������������������������������������������������������������� 50 3.5.1 Ejemplo de reproducción de vídeo���������������������������������������������������������������������������� 50 4. Conceptos básicos de gráficos en Android...............................................................59 4.1 Definición de colores en Android�������������������������������������������������������������������������������������� 59 4.2 Clases de dibujo en Android����������������������������������������������������������������������������������������������� 60 4.2.1 Clase Paint�������������������������������������������������������������������������������������������������������������������� 60 4.2.2 Clase Rectángulo���������������������������������������������������������������������������������������������������������� 60 4.2.3 Clase Path��������������������������������������������������������������������������������������������������������������������� 60 4.2.4 Clase Canvas���������������������������������������������������������������������������������������������������������������� 61 4.2.4.1 Obtener tamaño del Canvas:��������������������������������������������������������������������������������61 4.2.4.2 Dibujar figuras geométricas:�������������������������������������������������������������������������������� 61 4.2.4.3 Dibujar líneas y arcos:����������������������������������������������������������������������������������������� 62 4.2.4.4 Dibujar texto:������������������������������������������������������������������������������������������������������� 62 4.2.4.5 Colorear todo el lienzo Canvas:������������������������������������������������������������������������� 62 4.2.4.6 Dibujar imágenes:������������������������������������������������������������������������������������������������ 62 4.2.4.7 Definir un Clip (área de selección):������������������������������������������������������������������ 62 4.2.4.8 Definir matriz de transformación (Matrix):������������������������������������������������������ 63
4.2.5 Definición de dibujables (Drawable)�������������������������������������������������������������������������� 66 4.2.5.1 Dibujable de tipo bitmap (BitmapDrawable)��������������������������������������������������� 67 4.2.5.2 GradientDrawable (Gradiente dibujable)����������������������������������������������������������� 67 4.2.5.3 ShapeDrawable (Dibujable con forma)�������������������������������������������������������������� 68 4.2.5.4 AnimationDrawable (Dibujable animado)���������������������������������������������������������� 68 5. Animaciones de Android..........................................................................................70 5.1 Animaciones Tween������������������������������������������������������������������������������������������������������������ 70 5.1.1 Atributos de las transformaciones Tween ����������������������������������������������������������������� 71 5.2 API de Animación de Android�������������������������������������������������������������������������������������������� 74 5.2.1 Clases principales de la API de animación���������������������������������������������������������������� 74 5.2.1.1 Animator��������������������������������������������������������������������������������������������������������������� 75 5.2.1.2 ValueAnimator������������������������������������������������������������������������������������������������������ 75 5.2.1.3 ObjectAnimator���������������������������������������������������������������������������������������������������� 76 5.2.1.4 AnimatorSet���������������������������������������������������������������������������������������������������������� 76 5.2.1.5 AnimatorBuilder��������������������������������������������������������������������������������������������������� 77 5.2.1.6 AnimationListener������������������������������������������������������������������������������������������������ 77 5.2.1.7 PropertyValuesHolder������������������������������������������������������������������������������������������ 78 5.2.1.8 Keyframe�������������������������������������������������������������������������������������������������������������� 78 5.2.1.9 TypeEvaluator������������������������������������������������������������������������������������������������������ 78 5.2.1.10 ViewPropertyAnimator�������������������������������������������������������������������������������������� 79 5.2.1.11 LayoutTransition������������������������������������������������������������������������������������������������� 80 5.3 Animación de Actividad������������������������������������������������������������������������������������������������������ 80 5.4 Interpolators (Interpoladores)��������������������������������������������������������������������������������������������� 89 6. Vista de tipo Superficie (ViewSurface) ....................................................................92 6.1 Arquitectura de Gráficos en Android��������������������������������������������������������������������������������� 93 6.2 ¿Qué es la clase ViewSurface?��������������������������������������������������������������������������������������������� 93 7. Gráficos en 3D en Android..................................................................................... 101 7.1 OpenGL ��������������������������������������������������������������������������������������������������������������������������� 102 7.1.1 Conceptos básicos de geometría������������������������������������������������������������������������������ 102 7.1.2 Conceptos básicos de OpenGL��������������������������������������������������������������������������������� 104 7.2 Gráficos en 2D������������������������������������������������������������������������������������������������������������������� 107 7.3 Gráficos en 3D con movimiento��������������������������������������������������������������������������������������� 117 7.4 Gráficos en 3D con textura y movimiento����������������������������������������������������������������������� 125 8. Resumen................................................................................................................. 134 Unidad 2. Interfaz de usuario avanzada�������������������������������������������������������������������� 136 1. Introducción........................................................................................................... 136 2. Estilos y Temas en las aplicaciones de Android..................................................... 136 2.1 Cómo crear un Tema��������������������������������������������������������������������������������������������������������� 137 2.2 Atributos personalizados��������������������������������������������������������������������������������������������������� 138 2.3 Definición de recursos dibujables (Drawable)����������������������������������������������������������������� 140 2.3.1 Recurso de color�������������������������������������������������������������������������������������������������������� 140 2.3.2 Recurso de dimensión����������������������������������������������������������������������������������������������� 141 2.3.3 Gradiente Drawable (Gradiente dibujable)�������������������������������������������������������������� 141 2.3.4 Selector Drawable (Selector dibujable)�������������������������������������������������������������������� 142 2.3.5 Nine-patch drawable con botones���������������������������������������������������������������������������� 143 2.4 Atributos de los temas������������������������������������������������������������������������������������������������������� 144 2.5 Carga dinámica de Temas������������������������������������������������������������������������������������������������� 145 3. Implementación de Widgets en la pantalla principal............................................. 147 3.1 Tipos de Widgets y sus limitaciones��������������������������������������������������������������������������������� 148 3.2 Ciclo de vida de un Widget���������������������������������������������������������������������������������������������� 149
3.3 Ejemplo de Creación de un Widget���������������������������������������������������������������������������������� 150 3.4 Ejemplo de implementación de un Widget���������������������������������������������������������������������� 150 3.4.1 Fichero de configuración del widget: ��������������������������������������������������������������������� 151 3.4.2 Clase que define el Widget: ������������������������������������������������������������������������������������� 152 3.4.3 Servicio que actualiza el Widget: ����������������������������������������������������������������������������� 154 3.4.4 Interfaz de la Actividad de configuración del Widget: ������������������������������������������� 157 3.4.5 Actividad de configuración de las preferencias: ����������������������������������������������������� 158 3.4.6 Definición de la aplicación:�������������������������������������������������������������������������������������� 161 3.5 Colecciones de Vistas en Widgets ����������������������������������������������������������������������������������� 164 3.6 Activando Widgets en la pantalla de Bloqueo ���������������������������������������������������������������� 165 4. Creación de fondos de pantalla animados............................................................. 166 4.1 Ejemplo de Creación de un fondo de pantalla animado������������������������������������������������ 166 4.2 Ejemplo de implementación de un fondo animado�������������������������������������������������������� 167 4.2.1 Fichero de configuración del fondo animado:��������������������������������������������������������� 167 4.2.2 Servicio que implementa el fondo animado:����������������������������������������������������������� 167 4.2.3 Interfaz de la Actividad de configuración del fondo animado:������������������������������ 172 4.2.4 Actividad de configuración de las preferencias:������������������������������������������������������ 173 4.2.5 Actividad principal del usuario: ������������������������������������������������������������������������������� 174 4.2.6 Definición de la aplicación: ������������������������������������������������������������������������������������� 174 5. Fragmentos............................................................................................................ 179 5.1 Cómo se implementan los Fragmentos���������������������������������������������������������������������������� 180 5.2 Ciclo de vida de un Fragmento���������������������������������������������������������������������������������������� 192 5.2.1 Cómo guardar el estado de un Fragmento�������������������������������������������������������������� 193 5.2.2 Cómo mantener los Fragmentos cuando la Actividad se recrea automáticamente193 5.2.3 Cómo buscar Fragmentos ���������������������������������������������������������������������������������������� 194 5.2.4 Otras operaciones sobre Fragmentos (Transacciones)�������������������������������������������� 194 5.2.5 Cómo Gestionar la pila (Back Stack) de Fragmentos ��������������������������������������������� 195 5.2.6 Cómo utilizar Fragmentos sin layout������������������������������������������������������������������������ 197 5.2.6.1 Comunicación entre Fragmentos y con la Actividad��������������������������������������� 197 5.2.7 Recomendaciones a la hora de programar Fragmentos������������������������������������������ 198 5.2.8 Implementar diálogos con Fragmentos ������������������������������������������������������������������� 199 5.2.9 Otras clases de Fragmentos��������������������������������������������������������������������������������������� 202 5.3 Barra de Acción (Action Bar)����������������������������������������������������������������������������������������� 202 5.3.1 Cómo integrar pestañas en la Barra de acción�������������������������������������������������������� 207 6. Nuevas Vistas: GridView, Interruptor (Switch) y Navigation Drawer..................... 211 6.1 Grid View��������������������������������������������������������������������������������������������������������������������������� 211 6.2 Interruptores (Switches)����������������������������������������������������������������������������������������������������� 215 7. Navigation Drawer (Menú lateral deslizante)......................................................... 217 8. Resumen................................................................................................................. 229 Unidad 3. Sensores y dispositivos de Android ��������������������������������������������������������� 231 1. Introducción........................................................................................................... 231 2. Introducción a los sensores y dispositivos............................................................ 231 2.1 Gestión de Sensores de Android�������������������������������������������������������������������������������������� 232 2.1.1 Cómo se utilizan los Sensores����������������������������������������������������������������������������������� 234 2.1.2 Sistema de Coordenadas de un evento de sensor��������������������������������������������������� 239 3. Simulador de sensores de Android........................................................................ 240 3.1 Instalación del Simulador de Sensores����������������������������������������������������������������������������� 241 3.2 Cómo utilizar el Simulador de Sensores��������������������������������������������������������������������������� 243 3.2.1 Ejemplo de desarrollo de aplicación con el Simulador de Sensores���������������������� 247 3.2.2 Grabación de escenario de simulación con un dispositivo real����������������������������� 251
4. Dispositivos de Android......................................................................................... 253 4.1 Módulo WIFI���������������������������������������������������������������������������������������������������������������������� 253 4.2 Módulo Bluetooth�������������������������������������������������������������������������������������������������������������� 261 4.3 Cámara de fotos����������������������������������������������������������������������������������������������������������������� 267 4.3.1 Ejemplo de cámara mediante un Intent������������������������������������������������������������������� 268 4.3.2 Ejemplo de cámara mediante API de Android��������������������������������������������������������� 269 4.4 Módulo GPS����������������������������������������������������������������������������������������������������������������������� 281 5. Uso de sensores en un juego .............................................................................. 293 5.1 Desarrollo de un Juego en Android��������������������������������������������������������������������������������� 293 6. Resumen................................................................................................................. 315 Unidad 4. Bibliotecas, APIs y Servicios de Android�������������������������������������������������� 317 1. Introducción........................................................................................................... 317 2. Uso de Bibliotecas en Android............................................................................... 317 2.1 Ejemplo de Biblioteca de Android����������������������������������������������������������������������������������� 318 3. APIs del teléfono: llamadas y SMS......................................................................... 327 3.1 TelephonyManager ����������������������������������������������������������������������������������������������������������� 327 3.2 SMSManager����������������������������������������������������������������������������������������������������������������������� 328 3.3 Ejemplo de utilización de la API de telefonía������������������������������������������������������������������ 328 3.3.1 Clase Loader��������������������������������������������������������������������������������������������������������������� 339 4. Calendario de Android........................................................................................... 343 4.1 API Calendario de Android����������������������������������������������������������������������������������������������� 343 4.2 Tabla Calendarios�������������������������������������������������������������������������������������������������������������� 345 4.3 Tabla Eventos/Citas����������������������������������������������������������������������������������������������������������� 347 4.4 Tabla Invitados������������������������������������������������������������������������������������������������������������������� 350 4.5 Tabla Recordatorios����������������������������������������������������������������������������������������������������������� 351 4.6 Tabla de instancias������������������������������������������������������������������������������������������������������������ 351 4.7 Intenciones de Calendario de Android���������������������������������������������������������������������������� 352 4.8 Diferencias entre Intents y la API del Calendario������������������������������������������������������������ 354 4.9 Ejemplo de uso de Intents de la API del Calendario������������������������������������������������������ 354 5. Gestor de descargas (Download manager)............................................................ 366 5.1 Ejemplo de utilización del Gestor de descargas�������������������������������������������������������������� 367 6. Cómo enviar un correo electrónico........................................................................ 371 6.1 OAuth 2.0 de Gmail����������������������������������������������������������������������������������������������������������� 371 6.2 Intent del tipo message/rfc822����������������������������������������������������������������������������������������� 371 6.3 Biblioteca externa JavaMail API���������������������������������������������������������������������������������������� 371 6.4 Ejemplo sobre cómo envíar un correo electrónico��������������������������������������������������������� 372 7. Servicios avanzados de Android............................................................................. 382 7.1 Teoría sobre servicios de Android������������������������������������������������������������������������������������ 382 7.2 Servicios propios��������������������������������������������������������������������������������������������������������������� 383 7.3 Intent Service��������������������������������������������������������������������������������������������������������������������� 385 7.4 Ejemplo de uso de IntentService�������������������������������������������������������������������������������������� 385 7.5 Comunicación con servicios��������������������������������������������������������������������������������������������� 392 7.6 Ejemplo de uso de AIDL��������������������������������������������������������������������������������������������������� 393 8. Servicios SOAP en Android.................................................................................... 398 8.1 Instalación de bibliotecas SOAP en Eclipse ADT������������������������������������������������������������ 399 8.2 Desarrollo de un servidor SOAP en Eclipse ADT������������������������������������������������������������ 404 8.3 Ejemplo de uso de servidor SOAP en Android���������������������������������������������������������������� 412 8.4 Petición / Respuesta compleja SOAP en Android������������������������������������������������������������ 420 9. Resumen................................................................................................................. 423
Unidad 5. Utilidades avanzadas��������������������������������������������������������������������������������� 425 1. Introducción........................................................................................................... 425 2. Portapapeles de Android........................................................................................ 425 2.1 Ejemplo de portapapeles�������������������������������������������������������������������������������������������������� 426 3. Drag and Drop (Arrastrar y soltar)........................................................................ 431 3.1 Proceso de Arrastrar y soltar��������������������������������������������������������������������������������������������� 431 3.2 Ejemplo de Arrastrar y soltar�������������������������������������������������������������������������������������������� 432 4. Gestión del toque de pantalla................................................................................ 436 4.1 Ejemplo de gestión de toque de pantalla������������������������������������������������������������������������ 438 5. Tamaños de pantalla de los dispositivos Android.................................................. 448 5.1 Android y tamaños de pantalla����������������������������������������������������������������������������������������� 449 5.2 Densidades de pantalla����������������������������������������������������������������������������������������������������� 450 5.3 Buenas prácticas de diseño de interfaces de usuario������������������������������������������������������ 452 6. Internacionalización de aplicaciones Android....................................................... 453 6.1 Ejemplo del uso de Internacionalización������������������������������������������������������������������������� 454 7. Desarrollo rápido de código Android.................................................................... 459 8. Resumen................................................................................................................. 461
U0 Introducción
Unidad 0. Introducción
1. ¿Por qué un curso avanzado de Android? Android es un sistema operativo multidispositivo, inicialmente diseñado para teléfonos móviles. En la actualidad se puede encontrar también en múltiples dispositivos, como ordenadores, tabletas, GPS, televisores, discos duros multimedia, mini ordenadores, cámaras de fotos, etcétera. Incluso se ha instalado en microondas y lavadoras. Está basado en Linux, que es un núcleo de sistema operativo libre, gratuito y multiplataforma. Este sistema operativo permite programar aplicaciones empleando una variación de Java llamada Dalvik, y proporciona todas las interfaces necesarias para desarrollar fácilmente aplicaciones que acceden a las funciones del teléfono (como el GPS, las llamadas, la agenda, etcétera) utilizando el lenguaje de programación Java. Su sencillez, junto a la existencia de herramientas de programación gratuitas, es principalmente la causa de que existan cientos de miles de aplicaciones disponibles, que amplían la funcionalidad de los dispositivos y mejoran la experiencia del usuario. Este sistema operativo está cobrando especial importancia debido a que está superando al sistema operativo por excelencia: Windows. Los usuarios demandan cada vez interfaces más sencillas e intuitivas en su uso; por esto, entre otras cosas, Android se está convirtiendo en el sistema operativo de referencia de facto. El tiempo dirá si se confirman las perspectivas.
El objetivo de este curso avanzado es que el alumno o alumna perfeccione la programación en este sistema operativo tratando materias no estudiadas en el curso de iniciación. Así, podrá desarrollar aplicaciones más complejas utilizando contenidos multimedia, 3D, sensores del dispositivo, servicios, etcétera.
2. Cambios en las últimas versiones de Android
1.5 Cupcake
1.6 Donut
2.0/2.1 Eclair
2.2 Froyo
2.3 Gingerbread 3.0/3.1 Honeycomb
...IceCream Sandwich
11
Aula Mentor
Quien esté familiarizado con el sistema operativo Android ya sabrá que los nombres de sus diferentes versiones tienen el apodo de un postre. A continuación, vamos a comentar la evolución de las diferentes versiones indicando las mejoras y funcionalidades disponibles en cada una. Partiremos de la versión 3.0 ya que las versiones anteriores a ésta se tratan en el curso de Iniciación de Android de Mentor. - Android 3.0 (API 15) Esta versión se diseñó pensando en las tabletas, que disponen de un hardware mucho más potente. Entre sus nuevas funcionalidades podemos encontrar: • Soporte para grandes pantallas, como las tabletas. • Inclusión del concepto de Fragmento (en inglés, Fragment). • Nuevos elementos de interfaz como las barras de acción (action bars) y el arrastrar y soltar (drag-and-drop). • Instalación de un nuevo motor OpenGL 2.0 para animación en 3D. Esta versión de Android se diseñó exclusivamente para ser utilizada en tabletas. En otros dispositivos, como los teléfonos, era necesario seguir utilizando la versión 2.3.7 disponible en ese momento.
12
- Android 4.0 (API 16) A partir de esta versión se unifica el sistema operativo para que pueda utilizarse tanto en tabletas como en otros dispositivos, como teléfonos móviles. Así, se unifica la experiencia de usuario en todos los dispositivos. Entre sus nuevas funcionalidades podemos destacar: • Optimización en las notificaciones al usuario. • Permite al usuario cambiar el tamaño de los widgets. • Añade diferentes formas de desbloquear la pantalla del dispositivo. • Corrector ortográfico integrado. • NFC (Near Field Communication) • Wi-Fi Direct para compartir archivos entre dispositivos. • Encriptación total del dispositivo. • Nuevos protocolos de Internet como RTP (Real-time Transport Protocol) para que el dispositivo accede en tiempo real a contenidos de audio y vídeos. • MTP (Media Transfer Protocol) que permite conectar el dispositivo al ordenador por USB de forma más simple. • Gestión de derechos de autor mediante Digital Rights Management (DRM). - Android 4.2 (API 17) Esta versión no supone un salto en cuanto a las posibilidades que ofrece desde el punto de vista del desarrollador. Sin embargo, es una versión estable y madura. Entre sus nuevas funcionalidades podemos destacar: • Soporte multiusuario. • Posibilidad e inclusión de Widgets en la ventana de bloqueo. • Mejoras de interfaz y de cámara de fotos. - Android 4.3 (API 18) De igual forma que en la versión anterior, esta versión no supone un cambio radical en funcionalidades disponibles al desarrollador. Sin embargo, es una versión más estable y madura sin ninguna duda. Entre sus nuevas funcionalidades podemos destacar: • Bluetooth Low Energy (Smart Ready) y modo Wi-Fi scan-only que optimizan el consumo de batería de estos dispositivos. • Inclusión de la librería OpenGL ES 3.0 que permite mejorar en gráficos 3D.
U0 Introducción
• Definición de perfiles de usuario limitados que, desde el punto de vista del desarrollador, implican una gestión de las Intenciones implícitas (Implicit Intent) para comprobar si el usuario tiene permisos para acceder a ese tipo de Intención. • Mejoras en la gestión multimedia y de codecs de archivos de vídeo. Además, permite crear un vídeo de una Superficie dinámica. • Nuevos tipos de sensores relacionados con juegos. • Nueva Vista ViewOverlay que permite añadir elementos visuales encima de otros ya existentes sin necesidad de incluir en un Layout. Útil para crear animaciones sobre la interfaz de usuario. • Nuevas opciones de desarrollo como revocar el acceso a la depuración USB de todos los ordenadores o mostrar información del uso de la GPU del dispositivo. • Notification Listener es un nuevo servicio que permite que las aplicaciones reciban notificaciones del sistema operativo y sustituye al servicio Accessibility APIs.
Este curso está basado en la última versión de Android disponible que es la 4.3 y todos los ejemplos y aplicaciones son compatibles con ésta. De todas formas, no debe haber ningún problema en utilizar este código fuente en versiones futuras de Android.
Importante Los contenidos de este curso están diseñados para alumnos que están familiarizados con el entorno de desarrollo Eclipse / Android / Emulador de Android. Por ello, los alumnos deben conocer y manejar con soltura Vistas básicas, Actividades, Menús, Diálogos, Adaptadores, sistema de ficheros, Intenciones, Notificaciones, Content Providers y utilización de SQLite. Todos estos conceptos básicos de desarrollo en este sistema operativo se tratan en el curso de Iniciación a Android de Mentor.
3. La simbiosis de Android y Linux
13
Aula Mentor
Como sabes, Android está basado en Linux para los servicios base del sistema, como seguridad, gestión de memoria, procesos y controladores. El diagrama de la arquitectura de Android tiene este aspecto:
14
Antes del año 2005, Linux estaba disponible en servidores web, aplicaciones de escritorio de algunas empresas y administraciones, así como en ordenadores de programadores y entusiastas. Sin embargo, con el despegue de Android, Linux empieza a estar instalado en nuestros móviles y tabletas de forma masiva. En este apartado vamos a ver por qué es tan importante la simbiosis Android y Linux. El desarrollo de Linux empezó el año 1991 de la mano del famoso estudiante finlandés Linus Torvalds que crea la primera versión de este sistema operativo con el fin de implementar una versión libre de licencias (Open Source) de Unix que cualquier programador pudiera modificar o mejorar a su antojo. Al poco tiempo, grandes compañías como Intel e IBM advirtieron su potencial frente a Windows e invirtieron grandes cantidades de dinero. Su objetivo principal era no depender de Microsoft y, de paso, obtener un sistema operativo sin tener que empezar de cero. En la actualidad, los sistemas operativos basados en Linux son sinónimo de estabilidad, seguridad, eficiencia y rendimiento. Sin embargo, hasta la aparición de Android, a Linux le faltaba el éxito entre el gran pú-
U0 Introducción
blico quedando casi relegado a los servidores. Desde entonces, cada nuevo proyecto basado en Linux ha tenido como objetivo el gran público. Ubuntu, con una interfaz muy sencilla e intuitiva y teniendo en cuenta al usuario como primera prioridad, es hasta ahora la distribución de escritorio más popular de la historia del sistema operativo, gracias a que sus desarrolladores crearon una instalación automática de drivers y códecs. Además, su interfaz actual, llamada Unity, aplica conceptos del entorno móvil, y, de hecho, ya hay una versión preliminar de Ubuntu para teléfonos. A pesar de todo esto, sin embargo, a Linux le faltan los programas comerciales más importantes, por lo que únicamente el 1% de los PCs del mundo funcionan con Linux. En el año 2005 surge Android, que, debido a su carácter abierto, empleó el kernel (núcleo) de Linux como base. Técnicamente, Android no es una distribución de Linux, ya que la cantidad de modificaciones realizadas al código hace que se considere un sistema operativo independiente, aunque gran parte del código se comparte con el Linux “normal” de escritorio. Pero, ¿por qué ha conseguido Google llegar a tal cantidad de dispositivos en todo el mundo? La respuesta es simple: ha colaborado con los fabricantes. Para que Android (o cualquier sistema operativo) pueda ejecutarse en un dispositivo móvil, son necesarios los drivers. Los drivers son programas integrados en una librería que indica al sistema operativo cómo controlar las distintas partes de hardware. Por ejemplo, para poder utilizar la red WiFi, Android necesita conocer cómo indicar al chip las instrucciones que necesita mediante los drivers. Dado que los drivers incluyen información sobre cómo funciona el hardware físicamente, los fabricantes son siempre reacios a publicar su información por temor a que los competidores los copien. Google consiguió garantizar a los fabricantes la independencia de sus tecnologías al mismo tiempo que aprovechaba la filosofía abierta de Linux para fabricar un sistema alrededor de ellos. Por esta razón, puedes descargarte Android de Internet pero realmente no puedes ejecutarlo en tu móvil sin obtener los drivers antes y compilarlos. A lo largo de este tiempo, la relación Android/Linux ha tenido unos cuantos altibajos ya que Google ha exigido cambios en Linux para mejorar Android sin tener en cuenta que Linux es un proyecto global. Con todo, la historia de Android y Linux no ha terminado, ni mucho menos. De hecho, se podría decir que acaba de empezar. Algunos analistas de Internet hablan de que al final Linux sí vencerá al otrora omnipotente Windows, pero será a través de Android. El tiempo dirá.
15
Aula Mentor
4. Instalación del Entorno de Desarrollo 4.1 ¿Qué es Eclipse?
16
Como sabes, Eclipse es un entorno de software multi-lenguaje de programación que incluye un entorno de desarrollo integrado (IDE). Inicialmente, se diseñó pensando principalmente en el lenguaje de programación Java y se puede utilizar para desarrollar aplicaciones en este lenguaje. En la web oficial de Eclipse (www.eclipse.org), se define como “An IDE for everything and nothing in particular” (un IDE para todo y para nada en particular). Eclipse es, en realidad, un armazón (workbench) sobre el que se pueden instalar herramientas de desarrollo para cualquier lenguaje, mediante la implementación de los plugins adecuados. El término plugin procede del inglés to plug, que significa enchufar. Es un software que permite cambiar, mejorar o agregar funcionalidades. La arquitectura de plugins de Eclipse permite, además de integrar diversos lenguajes sobre un mismo IDE, introducir otras aplicaciones accesorias que pueden resultar útiles durante el proceso de desarrollo, tales como herramientas UML (modelado de objetos), editores visuales de interfaces, ayuda en línea para librerías, etcétera. Si has realizado el curso de Iniciación de Android de Mentor habrás utilizado ya Eclipse y tendrás soltura en su uso.
Google ha simplificado todo el proceso de instalación del entorno de desarrollo preparando en un único archivo todos los archivos necesarios. Este entorno se denomina ADT (Android Developer Tools) que denominaremos en el curso Eclipse ADT. Además, el nuevo entorno ya es compatible con Java 1.7.
4.2 Instalación de Java Development Kit ( JDK) Es muy importante tener en cuenta que, para poder ejecutar el entorno de desarrollo Eclipse ADT, es necesario tener instaladas en el ordenador las librerías de desarrollo de Java. La última versión 1.7 ya es compatible con Eclipse ADT. Podemos descargar la versión correcta del JDK de Java en: http://www.oracle.com/technetwork/es/java/javase/downloads/index.html
U0 Introducción
17 Si haces clic en el enlace anterior indicado, puedes encontrar un listado con todos los JDK de Java:
Aula Mentor
Nota: en el caso de Linux o Mac, es posible también instalar Java usando los programas habituales del sistema operativo que permiten la actualización de paquetes. Nota: si vas a instalar Eclipse ADT en Linux, lee las notas que se encuentran en “Preguntas y Respuestas” de esta Introducción en la mesa del curso.
4.3 Instalación de Eclipse ADT La instalación es muy sencilla. Simplemente accedemos a la página web: http://developer.android.com/intl/es/sdk/index.html
Si vamos a instalar Eclipse ADT en Windows, podemos hacer clic directamente en el enlace “Download the SDK”. En caso contrario debemos hacer clic en el enlace “DOWNLOAD FOR OTHER PLATFORMS” y seleccionar el sistema operativo correspondiente.
18
Hay que tener en cuenta que debemos descargar la versión 32 bits o 64 bits en función del sistema operativo de que dispongamos. En el caso de Windows podemos ver el tipo de sistema operativo haciendo clic con el botón derecho del ratón en el icono “Equipo” o Mi PC del Escritorio y haciendo clic de nuevo en “Propiedades”:
U0 Introducción
En el caso de Linux, desde la línea de comandos podemos ejecutar el siguiente comando para saber si el sistema operativo es de 64bits: $ uname -m x86_64
En el caso de Apple Mac, desgraciadamente, sólo está disponible Eclipse ADT si estás utilizando un kernel de 64 bits. Para saber si tu Mac ejecuta el sistema operativo de 64 bits sigue estas instrucciones: - En el menú Apple ( ), selecciona Acerca de este Mac y a continuación, haz clic en “Más información”:
- En el panel “Contenido”, selecciona “Software”. - Si Extensiones y kernel de 64 bits está configurada como Sí, estás utilizando un kernel de 64 bits. Cuando hayamos descargado el fichero correspondiente, lo copiamos a un directorio o carpeta del ordenador y descomprimimos este fichero. Es recomendable usar un directorio sencillo que podamos recordar fácilmente, por ejemplo
19
Aula Mentor
C:\cursosMentor\adt. Además, es muy importante que los nombres de los directorios no contengan espacios, pues Eclipse ADT puede mostrar errores y no funcionar correctamente. Una vez descomprimido el fichero, Eclipse ADT está listo para ser utilizado; no es necesario hacer ninguna operación adicional.
Recomendamos que conviene hacer un acceso directo del archivo C:\cursosMentor\adt\eclipse\ eclipse.exe en el Escritorio del ordenador para arrancar rápidamente el entorno de programación Eclipse ADT.
20
Si arrancamos Eclipse ADT haciendo doble clic sobre el acceso directo que hemos creado anteriormente, a continuación, Eclipse pedirá que seleccionemos el “workspace”, es decir, el directorio donde queremos guardar los proyectos.
U0 Introducción
Seleccionaremos un directorio sencillo y fácil de recordar.
Importante: Recomendamos usar el directorio C:\cursosMentor\proyectos como carpeta personal.
Finalmente hacemos clic en OK para abrir Eclipse ADT:
21
Aula Mentor
Si cerramos la pestaña abierta, podemos ver ya el entorno de desarrollo que deberías conocer si has hecho del curso de inciación:
22
Ahora vamos a comprobar en las preferencias que la versión de Java en Eclipse ADT es correcta para compilar proyectos de Android. Para ello, hacemos clic en la opción del menú “Window-> Preferences...”, hacemos clic en el panel izquierdo sobre “Java->Installed JREs” y seleccionamos “jre7” en el campo “Installed JREs”:
U0 Introducción
Para finalizar, en esta ventana hay que seleccionar la versión de Java utilizada para compilar los proyectos de Android. Para ello hacemos clic en “Java->Compiler” y elegimos “1.6” en el campo “Compiler compliance settings”:
23
Es muy importante comprobar la versión de java de compilación
5. Añadir versiones y componentes de Android Aunque Eclipse ADT incluye ya la última versión del SDK Android, el último paso de la configuración consiste en descargar e instalar los componentes restantes del SDK que utilizaremos en este curso. El SDK utiliza una estructura modular que separa las distintas versiones de Android, complementos, herramientas, ejemplos y la documentación en un único paquete que se puede instalar por separado. En este curso vamos a usar la versión 4.3, por ser la última en el
Aula Mentor
momento de redacción de la documentación. No obstante, vamos a emplear sentencias compatibles y recompilables en otras versiones. Para añadir esta versión hay que hacer clic en la opción “Android SDK Manager” del menú principal “Window” de Eclipse:
Eclipse ADT también dispone de un botón de acceso directo:
24
Si lo hacemos, se abrirá la ventana siguiente:
U0 Introducción
Para instalar la versión 4.3 (si no lo está ya), seleccionamos los paquetes que se muestran en la siguiente ventana:
25
Nota: la revisión de las versiones de Android puede ser superior cuando al alumno o alumna instale el SDK. Una vez hemos pulsado el botón “Install 4 packages”, aparece esta ventana y seleccionamos la opción “Accept All” y, después, hacemos clic en “Install”:
Aula Mentor
26
El instalador tarda un rato (10-20 minutos) en descargar e instalar los paquetes. Una vez acabado se indicará que la instalación ha finalizado correctamente.
6. Definición del dispositivo virtual de Android Para poder hacer pruebas de las aplicaciones Android que desarrollemos sin necesidad de disponer de un teléfono Android, el SDK incluye la posibilidad de definir un Dispositivo Virtual de Android (en inglés, AVD, Android Virtual Device). Este dispositivo emula un terminal con Android instalado. Antes de crear el AVD, es recomendable instalar el acelerador por hardware de Intel del AVD llamado Intel Hardware Accelerated Execution Manager. Así, conseguiremos que el AVD se ejecute con mayor rapidez y eficiencia. Si abres el explorador de ficheros en el directorio: Debes ejecutar el archivo C:\cursosMentor\adt\sdk\extras\intel\Hardware_Accelerated_Execution_ Manager\IntelHaxm.exe:
U0 Introducción
Es recomendable dejar que el instalador elija la memoria por defecto utilizada:
27
A continuación, pulsamos el botón “Next”:
Aula Mentor
Si pulsamos el botón “Install”, se instalará el la utilidad: 28
Atención: Este acelerador de hardware sólo está disponible en algunos procesadores de Intel que disponen de tecnología de virtualización (VT=Virtualization Technology). Además, sólo está disponible para el sistema operativo Windows.
U0 Introducción
Independientemente de que hayas podido instalar el acelerador, continuamos definiendo el AVD. A continuación, hacemos clic en la opción “Android AVD Manager” del menú principal “Window” de Eclipse:
Aparecerá la siguiente ventana: 29
Aula Mentor
Hacemos clic en el botón “New” de la ventana anterior y la completamos como se muestra en la siguiente ventana:
30
U0 Introducción
Si no has podido instalar el acelerador del emulador, debes seleccionar el campo CPU/ABI siguiente:
31
La opción “Snapshot-> Enabled” permite guardar el estado del dispositivo de forma que todos los cambios que hagamos, como cambiar la configuración de Android o instalar aplicaciones, queden guardados. Así, la próxima vez que accedamos al emulador, se recupera automáticamente el último estado.
Importante: En el curso hemos creado un dispositivo virtual que no guarda el estado porque puede producir problemas de ejecución con Eclipse ADT. En todo caso, el alumno o alumna puede usar la opción “Edit” del AVD cuando crea necesario que los últimos cambios sean almacenados para la siguiente sesión de trabajo.
Aula Mentor
Para acabar, basta con hacer clic en “OK”:
32
Puedes encontrar el vídeo “Cómo instalar Eclipse ADT”, que muestra de manera visual los pasos seguidos en las explicaciones anteriores.
U1 Multimedia y Gráficos en Android
Unidad 1. Multimedia y Gráficos en Android
1. Introducción En esta Unidad vamos a explicar cómo diseñar aplicaciones multimedia Android para oír música, grabar con el micrófono y cargar vídeos desde una tarjeta SD. Algunas aplicaciones Android deben mostrar un aspecto dinámico o representar algún dato en forma gráfica para que el usuario visualice mejor la información que se le está ofreciendo. Como hemos comentado anteriormente en el curso, una aplicación Android tiene éxito si está bien programada internamente y, además, si tiene una apariencia atractiva exteriormente. Para poder desarrollar aplicaciones que incluyan estas funcionalidades es necesario adquirir previamente los Conceptos básicos de gráficos en Android. Los gráficos 2D/3D y las animaciones suelen ser muy útiles para presentar visualmente información al usuario. Para adquirir estas destrezas como programador Android, aprenderemos a animar imágenes de forma sencilla utilizando la API de animaciones de Android. Después, veremos qué es una Vista de tipo Superficie (ViewSurface) y sus aplicaciones más interesantes. Finalmente, estudiaremos cómo aplicar a proyectos Android la conocidísima librería OpenGL para crear gráficos en 2D y 3D, aplicarles colores, animarlos y permitir que el usuario interaccione con ellos.
2. Android Multimedia Hoy en día, los dispositivos móviles han sustituido a muchos antiguos aparatos que utilizábamos para escuchar música, grabar conversaciones, ver vídeos, etcétera. En este apartado vamos a ver cómo diseñar aplicaciones multimedia Android y reproducir este tipo de archivos de audio y vídeo. Mediante ejemplos prácticos expondremos una explicación detallada de las funciones propias del SDK que permitirán implementar una aplicación multimedia. La integración de contenido multimedia en aplicaciones Android resulta muy sencilla e intuitiva gracias a la gran variedad de clases que proporciona su SDK. En concreto, podemos reproducir audio y vídeo desde: - Un fichero almacenado en el dispositivo, normalmente en la tarjeta externa SD. - Un recurso que está embutido en el paquete de la aplicación (fichero .apk). - Mediante el streaming: distribución de multimedia a través de una red de manera que el usuario accede al contenido al mismo tiempo que se descarga. Los protocolos admitidos son dos: http:// y rtp://. También es posible grabar audio y vídeo, siempre y cuando el hardware del dispositivo lo permita.
33
Aula Mentor
A continuación, se muestra un listado de las clases de Android que nos permiten acceder a estos servicios Multimedia: - MediaPlayer: reproduce audio y vídeo desde ficheros o de streamings. - MediaController: representa los controles estándar para MediaPlayer (botones de reproducir, pausa, stop, etcétera). - VideoView: Vista que permite la reproducción de vídeo. - MediaRecorder: clase que permite grabar audio y vídeo. - AsyncPlayer: reproduce una lista de archivos de tipo audio desde un hilo secundario. - AudioManager: gestor del sonido del sistema operativo de varias propiedades como son el volumen, los tonos de llamada/notificación, etcétera. - AudioTrack: reproduce un archivo de audio PCM escribiendo un búfer directamente en el hardware. PCM son las siglas de Pulse Code Modulation, que es un procedimiento de modulación utilizado para transformar una señal analógica en una secuencia de bits. - SoundPool: gestiona y reproduce una colección de recursos de audio de corta duración. - JetPlayer: reproduce audio y video interactivo creado con SONiVOX JetCreator. - Camera: clase para tomar fotos y video con la cámara. - FaceDetector: clase para identificar la cara de las personas en una imagen de tipo bitmap. El sistema operativo Android soporta una gran cantidad de tipos de formatos multimedia, la mayoría de los cuales pueden ser tanto decodificados como codificados. A continuación, mostramos una tabla con los formatos nativos multimedia soportados por Android. Hay que tener en cuenta que algunos modelos de dispositivos pueden incluir formatos adicionales que no se incluyen en esta tabla, como DivX. 34
Tipo
Formato
Información
Extensión fichero
Sí
Sí
3GPP (.3gp) MPEG-4 (.mp4)
H.264 AVC
a partir Android 3.0
Sí
Baseline Profile (BP)
3GPP (.3gp) MPEG-4 (.mp4)
MPEG-4 SP
Sí
3GPP (.3gp)
a partir Android 2.3.3
Streaming a partir de Android 4.0
WebM (.webm) Matroska (.mkv)
WP8
Imagen
Decodifica
H.263
Video
Tipo
Codifica
Formato
Codifica
Decodifica
Información
Extensión fichero
JPEG GIF PNG BMP
Sí Sí
Sí Sí Sí Sí
Base + progresivo
JPEG (.jpg) GIF (.gif) PNG (.png) BMP (.bmp)
WEBP
a partir Android 4.0
a partir Android 4.0
WebP (.webp)
U1 Multimedia y Gráficos en Android
Tipo
Formato
Codifica
Decodifica
Información
Extensión fichero
AAC LC/LTP
Sí
Sí
HE-AACv1
a partir Android 4.1
Sí
HE-AACv2
Sí
AAC ELD
a partir Android 4.1
a partir Android 4.1
Mono/estéreo, 16-8kHz
AMR-NB
Sí
Sí
4.75 a 12.2 Kbps muestreada a @ 8kHz
3GPP (.3gp)
Sí
9 ratios de 6.60 Kbps a 23.85 Kbps a @ 16kHz
3GPP (.3gp)
Sí
Mono/estéreo de 8 a 320 Kbps, frecuencia de muestreo constante (CBR) o variable (VBR)
MP3 (.mp3)
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 (.ogg) Matroska (.mkv a partir 4.0)
AMR-WB
MP3
Sí
Audio
MIDI
Ogg Vorbis
Sí
Mono/estéreo con cualquier combinación estándar de frecuencia > 160 Kbps y ratios de muestreo de 8 a 48kHz
Sí
FLAC
a partir Android 3.1
mono/estereo (no multicanal)
PCM/WAVE
a partir Android 4.1
Sí
8 y 16 bits PCM lineal (frecuencias limitadas por el hardware)
3GPP (.3gp) MPEG-4(. mp4) No soporta raw AAC (.aac) ni MPEG-TS (.ts)
35
FLAC (.flac) WAVE (.wav)
Aunque el listado anterior pueda parecer muy complicado y amplio, te recomendamos que le eches un vistazo a la Wikipedia donde se explica los distintos Formatos de archivo de audio.
Aula Mentor
3. Librerías de reproducción y grabación de audio Android incluye distintos tipos de flujos de audio con sus respectivos volúmenes de sonido dependiendo del propósito de estos audios: música, tono de notificación, tono de llamada, alarma, etcétera. Para obtener información sobre cómo ha configurado el usuario el volumen de los diferentes flujos de audio, debemos utilizar la clase AudioManager que permite acceder a la configuración de sonidos del sistema. Mediante la llamada al método getSystemService(AUDIO_ SERVICE) se obtiene una instancia a este gestor de Audio. Entre sus métodos, podemos destacar: - getStreamVolume(int streamType): obtiene el volumen definido por el usuario para el
tipo de flujo indicado como parámetro.
- getStreamMaxVolume(int streamType): obtiene el volumen máximo que se puede defi-
nir para este tipo de flujo.
- isMusicActive(): indica si se está reproduciendo música. - getRingerMode(): devuelve el modo de sonidos del dispositivo; puede tomar las contantes RINGER_MODE_NORMAL, RINGER_MODE_SILENT, o RINGER_MODE_VIBRATE.
36
Existen métodos adicionales que permiten conocer si el audio se reproduce a través de un dispositivo Bluetooth, ajustar el volumen de un tipo de audio, etcétera. Te recomendamos que le eches un vistazo a la documentación oficial. Para establecer el volumen del audio que vamos a reproducir en esa actividad debemos utilizar el método setVolumeControlStream() en el evento onCreate() de la Actividad dependiendo del propósito: - Volumen para música o vídeo: this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
- Permite, además, que el usuario utilice los botones del dispositivo para subir y bajar su volumen. - Volumen para tono de llamada del teléfono this.setVolumeControlStream(AudioManager.STREAM_RING);
- Volumen de alarma
this.setVolumeControlStream(AudioManager.STREAM_ALARM);
- Volume de notificación
this.setVolumeControlStream(AudioManager.STREAM_NOTIFICATION);
- Volumen del sistema
this.setVolumeControlStream(AudioManager.STREAM_SYSTEM);
- Volumen de llamada por voz
this.setVolumeControlStream(AudioManager.STREAM_VOICECALL);
El SDK de Android dispone de dos APIs principales que permiten reproducir ficheros de tipo audio: SoundPool y MediaPlayer.
3.1 Clase SoundPool La clase SoundPool permite reproducir sonidos de forma rápida y simultáneamente. Es recomendable utilizar la primera API SoundPool para reproducir pequeños archivos de audio que no deben exceder 1 MB de tamaño, por lo que es el mecanismo ideal para reproducir efectos de sonido como en los juegos. Con la clase SoundPool podemos crear una colección de sonidos que se cargan en la
U1 Multimedia y Gráficos en Android
memoria desde un recurso (dentro de la APK) o desde el sistema de archivos. SoundPool utiliza el servicio de la clase MediaPlayer, que estudiaremos a continuación, para descodificar el audio en un formato crudo (PCM de 16 bits) y mantenerlo cargado en memoria; así, el hardware lo reproduce rápidamente sin tener que decodificarlas cada vez. La clase SoundPool realiza esta carga en memoria de los archivos multimedia de forma asíncrona, es decir, el sistema operativo lanzará el sonido con el listener OnLoadCompleteListener cuando se haya completado la carga de cada uno de los archivos. Es posible repetir los sonidos en un bucle tantas veces como sea necesario, definiendo un valor de repetición al reproducirlo, o mantenerlo reproduciendo en un bucle infinito con el valor -1. En este último caso, es necesario detenerlo con el método stop(). También podemos establecer la velocidad de reproducción del sonido, cuyo rango puede estar entre 0.5 y 2.0. Una velocidad de reproducción de 1.0 indica que el sonido se reproduce en su frecuencia original. Si definimos una velocidad de 2.0, el sonido se reproduce al doble de su frecuencia original y, por el contrario, si fijamos una velocidad de 0.5, lo hará lentamente a la mitad de la frecuencia original. Cuando se crea un objeto del tipo SoundPool hay que establecer mediante un parámetro el número máximo de sonidos que se pueden reproducir simultáneamente. Este parámetro no tiene por qué coincidir con el número de sonidos cargados. Además, cuando se reproduce un sonido con su método play(), hay que indicar su prioridad. Así, cuando el número de reproducciones activas supere el valor máximo establecido en el constructor, esta prioridad permite que el sistema detenga el flujo con la prioridad más baja y, si todos tienen la misma prioridad, se parará el más antiguo. Sin embargo, en el caso de que el nuevo flujo sea el de menor prioridad, éste no se reproducirá. En el ejemplo práctico vamos a estudiar los métodos más importantes de esta clase. 37
3.2 Clase MediaPlayer La segunda API es la más importante de Android y realiza la reproducción multimedia mediante la clase básica MediaPlayer (reproductor multimedia) que permite reproducir audio de larga duración. A continuación, estudiaremos las características más importantes de esta clase y cómo podemos sacarle partido.
La diferencia entre utilizar la clase SoundPool y MediaPlayer está en la duración y tamaño del archivo de sonido. Para sonidos cortos, debemos utilizar la primera clase, dejando la segunda para reproducciones largas como canciones de música.
Un objeto MediaPlayer puede estar en uno de los siguientes estados: - Initialized: ha inicializado sus recursos internos, es decir, se ha creado el objeto. - Preparing: se encuentra preparando o cargando la reproducción de un archivo multimedia. - Prepared: preparado para reproducir un recurso. - Started: reproduciendo un contenido. - Paused: en pausa. - Stopped: parado. - Playback Completed: reproducción completada. - End: finalizado. - Error: indica un error.
Aula Mentor
Es importante conocer en qué estado se encuentra el reproductor multimedia, ya que muchos de sus métodos únicamente se pueden invocar desde determinados estados. Por ejemplo, no podemos cambiar al modo en reproducción (con su método start()) si no se encuentra en el estado preparado. Lógicamente, tampoco podremos cambiar al modo en pausa (con su método pause()) si ya está parado. Ocurrirá un error de ejecución si invocamos un método no admitido para un determinado estado. El siguiente esquema permite conocer los métodos que podemos invocar desde cada uno de sus estados y cuál es el nuevo estado al que cambiará el objeto tras invocarlo:
38
U1 Multimedia y Gráficos en Android
Existen dos tipos de métodos: - Asíncronos: onPrepared(), onError(), onCompletion(). Los lanza el sistema cuando ha acabado una tarea. - Síncronos: el resto de métodos que se ejecutan de forma continua cuando se invocan, es decir, no hay que esperar. Mediante un ejemplo práctico vamos a estudiar los métodos más importantes de esta clase.
3.3 Clase MediaRecorder La API de Android ofrece también la posibilidad de capturar audio y vídeo, permitiendo su codificación en diferentes formatos. La clase MediaRecorder permite, de forma sencilla, integrar esta funcionalidad a una aplicación. La mayoría de los dispositivos Android disponen de un micrófono que puede capturar audio. La clase MediaRecorder dispone de varios métodos que puedes utilizar para configurar la grabación: - setAudioSource(int audio_source): dispositivo que se utilizará como fuente del sonido, es decir, el micrófono. Normalmente, indicaremos MediaRecorder.AudioSource.MIC. Si bien, es posible utilizar otras constantes como DEFAULT (micrófono por defecto), CAMCORDER (micrófono que tiene la misma orientación que la cámara), VOICE_CALL (micrófono para llamadas), VOICE_COMUNICATION (micrófono para VoIP), etcétera. - setOutputFile (String fichero): permite indicar el nombre del fichero donde se guardará la información. - setOutputFormat(int output_format): establece el formato del fichero de salida. Se pueden utilizar las constantes siguientes de la clase MediaRecorder.OutputFormat: DEFAULT, AMR_NB, AMR_WB, RAW_AMR (ARM), MPEG_4 (MP4) y THREE_GPP (3GPP). - setAudioEncoder(int audio_encoder): permite seleccionar la codificación del audio. Podemos indicar cuatro posibles constantes de la clase MediaRecorder.AudioEncoder: AAC, AMR_NB, AMR_WB y DEFAULT. - setAudioChannels(int numeroCanales): especifica el número de canales de la grabación: 1 para mono y 2 para estéreo. - setAudioEncodingBitRate(int bitRate): indica los bits por segundo (bps) utilizados en la codificación (desde nivel de API 8). - setAudioSamplingRate(int samplingRate): permite indicar el número de muestreo por segundo empleados en la codificación (desde nivel de API 8). - setProfile(CamcorderProfile profile): permite elegir un perfil de grabación de vídeo. - setMaxDuration(int max_duration_ms): indica la duración máxima de la grabación. Pasado este tiempo, ésta se detendrá. - setMaxFileSize(long max_filesize_bytes): establece el tamaño máximo para el fichero de salida. Si se alcanza este tamaño, la grabación se detendrá. - prepare(): prepara la grabación para la captura del audio o vídeo. - start(): inicia la grabación. - stop(): finaliza la grabación. - reset(): reinicia el objeto como si lo acabáramos de crear por lo que debemos configurarlo de nuevo. - release(): libera todos los recursos utilizados del objeto MediaRecorder. Si no invocas este método, los recursos se liberan automáticamente cuando el objeto se destruya.
39
Aula Mentor
Adicionalmente, la clase MediaRecorder dispone de métodos que puedes utilizar para configurar la grabación de video. Tal y como ocurre con la clase MediaPlayer, para poder invocar los diferentes métodos de la clase MediaRecorder debemos estar en un estado determinado. El siguiente esquema permite conocer los métodos que podemos invocar desde cada uno de sus estados y cuál es el nuevo estado al que cambiará el objeto tras invocarlo:
40
En el ejemplo práctico vamos a aprender los métodos más importantes de esta clase.
3.3.1 Ejemplo de reproducción y grabación de audio Es recomendable abrir el Ejemplo 1 de esta Unidad para seguir la explicación siguiente. La aplicación de este ejemplo muestra tres botones: el primero permite reproducir un tono utilizando la clase SoundPool, el segundo botón reproduce un archivo largo de audio y el último botón, graba una conversación utilizando el micrófono del dispositivo. Para los dos últimos botones usamos la clase MediaPlayer. En la parte de debajo de la Actividad hemos incluido una vista de tipo TextView desplazable que muestra las acciones del usuario cuando
U1 Multimedia y Gráficos en Android
pulsa en un botón. En código del layout activity_main.xml se incluye el diseño de la Actividad principal:
42
Una vez expuesto el sencillo diseño de la Actividad, veamos la lógica de ésta en el fichero MainActivity.java: public class MainActivity extends Activity { // Objetos de las clases SoundPool y MediaPlayer private SoundPool sPool; private MediaPlayer mPlayer; // Guarda los IDs de sonidos que se deben reproducir por SoundPool private int soundID1=-1, soundID2=-1; // Vistas de la Actividad private TextView logTextView; private ScrollView scrollview; // Grabador de audio private MediaRecorder recorder; // Fichero donde guardamos el audio private File audiofile = null; // Botones de la Actividad private Button boton_spool1, boton_spool2; private Button boton_mplayer; private Button boton_mrecorder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); // Localizamos las Vistas del layout logTextView = (TextView) findViewById(R.id.Log);
U1 Multimedia y Gráficos en Android
scrollview = ((ScrollView)findViewById(R.id.ScrollView));
// Establecemos el tipo de flujo de audio que deseamos this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
// Cargamos el tono con SoundPool indicando el tipo de flujo // STREAM_MUSIC sPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
// Cuando la carga del archivo con SoundPool se completa... sPool.setOnLoadCompleteListener(new OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { // Mostramos un log log(“Tono “ + sampleId + “ cargado con SoundPool”); } }); // Cargamos los archivos para SoundPool y guardamos su ID para // poder reproducirlo soundID1 = sPool.load(this, R.raw.bigben, 1); soundID2 = sPool.load(this, R.raw.alarma, 1);
// Definimos el mismo evento onClick de los botones SoundPool y // los distinguimos por su propiedad Tag View.OnClickListener click = new View.OnClickListener() { public void onClick(View v) { // Obtenemos acceso al gestor de Audio para obtener // información AudioManager audioManager = (AudioManager)getSystemService(AUDIO_SERVICE); // Buscamos el volumen establecido para el tipo // STREAM_MUSIC float volumenMusica = (float) audioManager .getStreamVolume(AudioManager.STREAM_MUSIC); // Obtenemos el vólumen máx para el tipo STREAM_MUSIC float volumeMusicaMax = (float) audioManager .getStreamMaxVolume(AudioManager.STREAM_MUSIC); // Vamos a reducir el volumen del sonido float volumen = volumenMusica / volumeMusicaMax; // ¿Qué botón se ha pulsado? ¿Se ha cargado el tono? if (v.getTag().toString().equals(“1”) && soundID1>-1) // Reproducimos el sonido 1 sPool.play(soundID1, volumen, volumen, 1, 0, 1f); else if (v.getTag().toString().equals(“2”) && soundID2>-1) // Reproducimos el sonido 2 sPool.play(soundID2, volumen, volumen, 1, 0, 1f); } }; // end onClick botón
// Buscamos los botones de SoundPool y asociamos su evento // onClick boton_spool1 = (Button) findViewById(R.id.soundpool1); boton_spool2 = (Button) findViewById(R.id.soundpool2); boton_spool1.setOnClickListener(click);
43
Aula Mentor
boton_spool2.setOnClickListener(click);
// Buscamos el botón que reproduce MediaPlayer y definimos su // evento onClick boton_mplayer = (Button) findViewById(R.id.mediaplayer); boton_mplayer.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Si ya estamos reproduciendo un sonido, lo paramos if (mPlayer!=null && mPlayer.isPlaying()) { mPlayer.stop(); // Cambiamos los botones y mostramos log boton_mplayer.setText(“Reproducir Audio con Mediaplayer”); boton_spool.setEnabled(true); boton_mrecorder.setEnabled(true); log(“Cancelada reproducción MediaPlayer”); } else // Si no, iniciamos la reproducción { // Cambiamos los botones y hacemos log boton_mplayer.setText(“Cancelar”); boton_spool.setEnabled(false); boton_mrecorder.setEnabled(false); log(“Reproduciendo Audio con MediaPlayer”);
// Creamos el objeto MediaPlayer asociándole la canción mPlayer = MediaPlayer.create(MainActivity.this, R.raw.beethoven_para_elisa); // Iniciamos la reproducción mPlayer.start();
44
// Definimos el listener que se lanza cuando la canción // acaba mPlayer.setOnCompletionListener(new OnCompletionListener() { public void onCompletion(MediaPlayer arg0) { // Hacemos log y cambiamos botones log(“Fin Reproducción MediaPlayer”); boton_spool.setEnabled(true); boton_mrecorder.setEnabled(true); } }); // end setOnCompletionListener } } } ); // end onClick botón
// Buscamos el botón que graba con MediaRecorder y definimos su // evento onClick boton_mrecorder = (Button) findViewById(R.id.mediarecorder); boton_mrecorder.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Si estamos grabando sonido if (boton_mrecorder.getText().equals(“Parar grabación”)) { // Paramos la grabación, liberamos los recursos y la // añadimos recorder.stop();
U1 Multimedia y Gráficos en Android
recorder.release(); addRecordingToMediaLibrary(); // Refrescamos interfaz usuario boton_mrecorder.setText(“Grabar conversación”); boton_spool.setEnabled(true); boton_mplayer.setEnabled(true); // Log de la acción log(“Parada grabación MediaRecorder”); } else { // Cambiamos los botones y hacemos log boton_mrecorder.setText(“Parar grabación”); boton_spool.setEnabled(false); boton_mplayer.setEnabled(false); log(“Grabando conversación”);
// Obtenemos el directorio de tarjeta SD File directorio = Environment.getExternalStorageDirectory(); try { // Definimos el archivo de salida audiofile = File.createTempFile(“sonido”, “.3gp”, directorio); } catch (IOException e) { Log.e(“ERROR”, “No se puede acceder a la tarjeta SD”); return; } // Creamos el objeto MediaRecorder recorder = new MediaRecorder(); // Establecemos el micrófono recorder.setAudioSource(MediaRecorder.AudioSource.MIC); // Tipo de formato de salida recorder.setOutputFormat( MediaRecorder.OutputFormat.THREE_GPP); // Codificación de la salida recorder.setAudioEncoder( MediaRecorder.AudioEncoder.AMR_NB); // Fichero de salida recorder.setOutputFile(audiofile.getAbsolutePath()); try { // Preparamos la grabación recorder.prepare(); } catch (IllegalStateException e) { Log.e(“ERROR”, “Estado incorrecto”); return; } catch (IOException e) { Log.e(“ERROR”, “No se puede acceder a la tarjeta SD”); return; } // Iniciamos la grabación recorder.start(); } // end else } }
45
Aula Mentor
); // end onClick botón log(“”); }
46
// Método que añade la nueva grabación a la librería // multimedia del dispositivo. Para ello, vamos a // utilizar un Intent del sistema operativo protected void addRecordingToMediaLibrary() { // Valores que vamos a pasar al Intent ContentValues values = new ContentValues(4); // Obtenemos tiempo actual long tiempoActual = System.currentTimeMillis(); // Indicamos que queremos buscar archivos de tipo audio values.put(MediaStore.Audio.Media.TITLE, “audio” + audiofile.getName()); // Indicamos la fecha sobre la que deseamos buscar values.put(MediaStore.Audio.Media.DATE_ADDED, (int)(tiempoActual / 1000)); // Tipo de archivo values.put(MediaStore.Audio.Media.MIME_TYPE, “audio/3gpp”); // Directorio destino values.put(MediaStore.Audio.Media.DATA, audiofile.getAbsolutePath()); // Utilizamos un ContentResolver ContentResolver contentResolver = getContentResolver(); // URI para buscar en la tarjeta SD Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Uri newUri = contentResolver.insert(base, values); // Enviamos un mensaje Broadcast para buscar el nuevo contenido de // tipo audio sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, newUri)); Toast.makeText(this, “Se ha añadido archivo “ + newUri + “ a la librería multimedia.”, Toast.LENGTH_LONG).show(); } // end addRecordingToMediaLibrary // Método que añade a la etiqueta Log un nuevo evento private void log(String s) { logTextView.append(s + “\n”); // Movemos el Scroll abajo del todo scrollview.post(new Runnable() { @Override public void run() { scrollview.fullScroll(ScrollView.FOCUS_DOWN); } }); } // end log } // end clase
Repasemos ahora con cuidado el código Java anterior. Puedes ver que el constructor de la clase SoundPool es el siguiente: SoundPool(int maxStreams , int streamType , int srcQuality)
U1 Multimedia y Gráficos en Android
Donde sus parámetros son: - maxStreams: indica el número de sonidos que puede reproducir al mismo tiempo. - streamType: marca el tipo de flujo de audio que usaremos. - srcQuality: indica la calidad. Este atributo no tiene uso en la API de Android. La siguiente sentencia establece el tipo de flujo a música, lo que permite que el usuario utilice los botones de subida y bajada de volumen del dispositivo: this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
Por último, debemos precargar con el objeto SoundPool los archivos de audio con el método siguiente: SoundPool.load(Context context, int resId, int priority). Donde resId es la Id de nuestro archivo de música. El parámetro priority permite seleccionar la prioridad de este sonido frente a otro en caso de que se llegue al máximo número de sonidos simultáneos establecidos en el constructor de la clase. Mediante el listener OnLoadCompleteListener el sistema operativo avisará cada vez que complete la carga de un archivo de sonido. Para reproducir un sonido debemos usar el método play (int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) cuyos parámetros indican: - soundID: ID del sonido que ha indicado el método load() al cargarlo. - leftVolume: volumen del altavoz izquierdo (rango de 0.0 a 1.0) - rightVolume: volumen del altavoz derecho (rango de 0.0 a 1.0) - priority: prioridad del sonido (0 es la más baja) - loop: modo en bucle si establecemos el valor -1. - rate: velocidad de reproducción (1.0 = normal, rango de 0.5 a 2.0) En el siguiente bloque de código hemos utilizado la clase Mediaplayer para reproducir una pista de audio mediante su método start() y pararla con el método stop(). Por simplificación, en este ejemplo hemos utilizado un recurso que se incluye en la carpeta /res/raw/. En una aplicación real no haríamos esto ya que el fichero mp3 se empaqueta con la aplicación y hace que ésta ocupe mucho espacio. Si queremos reproducir una canción desde el sistema de ficheros externo debemos escribir las siguientes sentencias: - MediaPlayer mPlayer = new MediaPlayer(); - mPlayer.setDataSource(RUTA+NOMBRE_FICHERO); - mPlayer.prepare(); - mPlayer.start(); Observa que, en este caso, hay que invocar previamente el método prepare() para cargar el archivo de audio. En el ejemplo del curso no es necesario hacerlo ya que esta llamada se hace desde el constructor create(). El último bloque de código realiza una grabación empleando la clase MediaRecorder. Hemos definido la variable audiofile para guardar la grabación. Para iniciar la grabación utilizamos los métodos setAudioSource() que establece el micrófono de entrada; setOutputFormat() selecciona el formato de salida; setAudioEncoder() indica la codificación del audio; setOutputFile() establece el fichero de salida y start() inicia la grabación. A la hora de parar la grabación, simplemente debemos invocar los métodos stop() y release() que libera los recursos del sistema. Para finalizar con el código Java, hemos desarrollado el método local addRecordingToMediaLibrary() que añade la nueva grabación a la librería multimedia del dispositivo. Para ello, vamos a utilizar un Intent del tipo ACTION_MEDIA_SCANNER_SCAN_FILE y enviar un mensaje Broadcast al sistema operativo para buscar el nuevo contenido multimedia de tipo
47
Aula Mentor
audio con la orden sendBroadcast(). Por último, para poder ejecutar esta aplicación es necesario que tenga permisos de grabar audio y acceso a la tarjeta SD del dispositivo. Para ello, hay que incluir en el fichero AndroidManifest.xml las siguientes etiquetas:
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 1 (Audio) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la API de Audio de Android.
Si ejecutas en Eclipse ADT este Ejemplo 1 en el AVD, verás que se muestra la siguiente aplicación:
48
Para poder oír en tu AVD los sonidos, debes encender los altavoces de tu ordenador. Prueba a ejecutar sonidos mediante SoundPool simultáneamente, incluso si se está reproduciendo música con el MediaPlayer. Sin embargo, la funcionalidad de grabación de audio no está integrada en el AVD y, para poder
U1 Multimedia y Gráficos en Android
probar esta funcionalidad del Ejemplo debes instalarlo en un dispositivo real.
Para poder usar un dispositivo real desde Eclipse ADT es necesario conectar este dispositivo mediante un cable al ordenador y modificar Ajustes del dispositivo en las opciones siguientes: En “Opciones del desarrollador”, marcar “Depuración de USB”. En “Seguridad”, señalar “Fuentes desconocidas”.
3.4 Cómo habilitar USB Debugging en Android 4.2 y superior Jelly Bean A partir de la versión de Android Jelly Bean 4.2, Google esconde la opción de Desarrollo (“Developer”) en los Ajustes (“Settings”) del dispositivo. Para que aparezca esta opción debes dar los pasos siguientes: - Abre Opciones->Información del teléfono/Tablet. - Haz clic repetidamente en la opción “Número de compilación” (“Build Number”) hasta 7 veces seguidas. Eso es todo, aparecerá un mensaje de que “Ya eres un developer” y verás que aparece la nueva opción “Opciones de desarrollo” (“Developer”) y dentro encontrarás USB Debugging. Fíjate en las siguientes capturas de pantalla: 49
Aula Mentor
3.5 Librería de reproducción de vídeo
50
La clase VideoView permite al programador incluir vídeos en las aplicaciones y abarca una gran cantidad de métodos para hacerlo. Los métodos más utilizados son los siguientes: - setVideoPath(String path): especifica el directorio y archivo de vídeo que se reproduce. Podemos indicar tanto una URL para vídeos en Internet como un archive local en el dispositivo. - setVideoUri(Uri uri): de igual forma que el método anterior, establece la fuente del vídeo en formato URI. - start(): inicia la reproducción del vídeo. - stopPlayback(): para la reproducción del vídeo. - pause(): pausa la reproducción del vídeo. - isPlaying(): devuelve true o false indicando así si se está reproduciendo un vídeo o no. - setOnPreparedListener(MediaPlayer.OnPreparedListener): define un método callback que se invoca cuando el vídeo está preparado para reproducirse. - setOnErrorListener(MediaPlayer.OnErrorListener): establece el método callback que el sistema invocará en caso de un error en reproducción del vídeo. Este método es muy útil cuando el vídeo está mal codificado u ocurre un error de conexión a Internet al reproducir un vídeo remoto. - setOnCompletionListener(MediaPlayer.OnCompletionListener): permite definir un método callback para detectar que la reproducción del vídeo ha terminado. - getDuration(): indica la duración del vídeo. Devuelve siempre -1 salvo que lo ejecutemos dentro del evento OnPreparedListener(). - getCurrentPosition(): devuelve la posición actual de reproducción del vídeo. - setMediaController(MediaController): establece el objeto MediaController que veremos a continuación. Si reproducimos un vídeo utilizando directamente la clase VideoView, el usuario no podrá controlar la reproducción, que continuará hasta que finalice el vídeo. Para permitir que el usuario gestione la visualización del vídeo podemos definir una interfaz ad hoc para la aplicación o emplear una instancia de la clase MediaController que asignaremos al VideoView. La clase MediaController muestra un conjunto de controles que permiten al usuario gestionar la reproducción del vídeo, por ejemplo, hacer una pausa, buscar hacia atrás y hacia adelante, etcétera. Estos controles aparecerán brevemente cuando el usuario toca en la vista VideoView a la que están asignados. Entre sus métodos más importantes podemos destacar: - setAnchorView(View view): indica la Vista de la Actividad donde aparecerán estos controles. - show() : muestra los controles. - show(int timeout): muestra los controles durante el tiempo indicado como parámetro en milisegundos. - hide(): oculta los controles. - isShowing():devuelve true o false indicando así si se están mostrando los controles o no.
3.5.1 Ejemplo de reproducción de vídeo En el Ejemplo 2 de esta Unidad hemos desarrollado un reproductor sencillo de vídeo. Es recomendable abrirlo en Eclipse ADT para seguir la explicación siguiente.
U1 Multimedia y Gráficos en Android
La interfaz de la aplicación muestra una barra de herramientas en la parte superior con botones que permiten al usuario controlar la reproducción del vídeo. En la parte central hemos incluido un objeto heredado de la clase VideoView. En la parte de abajo de la Actividad hemos incluido una vista de tipo TextView desplazable que muestra las acciones del usuario. En código del Layout activity_main.xml se incluye el diseño de la Actividad principal:
Puedes observar que en lugar de utilizar directamente la clase VideoView hemos indicado la clase heredada de ésta es.mentor.unidad3.eje2.video.CustomVideoView. Esto se hace así porque hemos redefinido los eventos onPlay(), onPause() y onTimeBarSeekChanged() y, para hacerlo, es necesario extender la clase VideoView. Una vez expuesto el sencillo diseño de la Actividad, veamos la lógica de ésta en el fichero MainActivity.java: 52
public class MainActivity extends Activity
{
// Vistas de la interfaz de usuario de la Actividad private ImageButton bPlay, bPause, bStop, bLog, bControls; private TextView logTextView; ScrollView scrollview; // VideoView extendido y mejorado con nuevos eventos (listeners) private CustomVideoView visorVideo; // Control del vídeo por parte del usuario private MediaController mediaC; // Posición actual de reproducción del vídeo que usaremos // cada vez que el usuario vuelva a la aplicación private int posActual=0; @Override protected void onCreate(Bundle estado) { super.onCreate(estado); setContentView(R.layout.main_layout); // Buscamos las Vistas scrollview = ((ScrollView)findViewById(R.id.ScrollView)); visorVideo = (CustomVideoView) findViewById(R.id.videoView); // Definimos la URI del vídeo Uri uri = Uri.parse(“android.resource://” + getPackageName() + “/”+R.raw.video); visorVideo.setVideoURI(uri); // Si se encuentra en el sistema de ficheros deberíamos // haber escrito //visorVideo.setVideoPath(“sdcard/video.mp4”); // Creamos el controlador del vídeo y lo asignamos al visor // de vídeo y viceversa mediaC = new MediaController(this);
U1 Multimedia y Gráficos en Android
mediaC.setMediaPlayer(visorVideo); visorVideo.setMediaController(mediaC); // Definimos el listener cuando el usuario reproduce, para o // cambia la posición del vídeo visorVideo.setPlayPauseListener(new CustomVideoView.PlayPauseListener() { // Reproduce vídeo @Override public void onPlay() { // Hacemos log y deshabilitamos botones log(“REPRODUCIENDO VIDEO”); bPause.setEnabled(true); bStop.setEnabled(true); bPlay.setEnabled(false); } @Override public void onPause() { // Hacemos log y deshabilitamos botones log(“VIDEO EN PAUSA”); bPause.setEnabled(false); bPlay.setEnabled(true); } @Override public void onTimeBarSeekChanged(int currentTime) { // Hacemos log log(“CAMBIO POSICION VIDEO: “+currentTime); // Si se ha parado el vídeo lo indicamos if (currentTime==0 && !visorVideo.isPlaying()) { bStop.setEnabled(false); log(“PARADA VIDEO”); } } }); // end setPlayPauseListener
// Definimos el listener cuando finaliza la reproducción del // vídeo visorVideo.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { // Hacemos log y deshabilitamos botones log(“FIN DE VIDEO”); bPause.setEnabled(false); bStop.setEnabled(false); bPlay.setEnabled(true); } });
// Definimos el listener cuando el visor ha preparado // la reproducción del vídeo visorVideo.setOnPreparedListener(new OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // Hacemos log. Sólo se puede llamar a // getDuration() desde este evento
53
Aula Mentor
log(“VIDEO PREPARADO. Duración: “ + visorVideo.getDuration()+” msegundos”);
});
}
// Definimos el listener cuando el usuario toca la pantalla del // vídeo visorVideo.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { log(“CLIC VIDEOVIEW: MUESTRA CONTROLADOR”); return false; } }); // Definimos el listener cuando ocurre un error en la reproducción visorVideo.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { log(“ERROR AL REPRODUCIR VIDEO”); return false; } });
54
logTextView = (TextView)findViewById(R.id.Log); // Buscamos el botón Reproducir y definimos su evento onClick bPlay = (ImageButton)findViewById(R.id.play); bPlay.setOnClickListener(new OnClickListener() { public void onClick(View view) { // Pedimos el foco del vídeo y lo reproducimos visorVideo.requestFocus(); visorVideo.start(); } }); // Buscamos el botón Pausa y definimos su evento onClick bPause = (ImageButton)findViewById(R.id.pause); bPause.setOnClickListener(new OnClickListener() { public void onClick(View view) { // Pausamos la reproducción visorVideo.pause(); } }); // Buscamos el botón Parada y definimos su evento onClick bStop = (ImageButton)findViewById(R.id.stop); bStop.setOnClickListener(new OnClickListener() { public void onClick(View view) { // Pausamos la reproducción y vamos al principio del // vídeo visorVideo.pause(); visorVideo.seekTo(0); } }); // Buscamos el botón Controles y definimos su evento onClick bControls = (ImageButton)findViewById(R.id.controls);
U1 Multimedia y Gráficos en Android
bControls.setOnClickListener(new View.OnClickListener() { public void onClick(View arg0) { // Mostramos los controles de usuario del vídeo mediaC.show(); } }); // Buscamos el botón Log y definimos su evento onClick bLog = (ImageButton)findViewById(R.id.logButton); bLog.setOnClickListener(new OnClickListener() { public void onClick(View view) { if (scrollview.getVisibility()==TextView.VISIBLE) { scrollview.setVisibility(TextView.INVISIBLE); } else { scrollview.setVisibility(TextView.VISIBLE); } } }); // Inicializamos Vistas log(“”); bPause.setEnabled(true); bStop.setEnabled(true); } // end onCreate // Debemos guardar la posición de reproducción en el evento onPause() // porque en el evento onSaveInstanceState ya está parado el vídeo y // es tarde. @Override protected void onPause() { super.onPause(); posActual = visorVideo.getCurrentPosition(); } // Cuando vuelve a estar activa la ACtividad volvemos a reproducir // donde lo dejamos @Override public void onResume() { super.onResume(); Log.d(“POS “, “onResume called”); // Volvemos a la posición guardada visorVideo.seekTo(posActual); // Continuamos la reproducción del vídeo visorVideo.resume(); } // Método que añade a la etiqueta Log un nuevo evento private void log(String s) { logTextView.append(s + “\n”); // Movemos el Scroll abajo del todo scrollview.post(new Runnable() { @Override public void run() { scrollview.fullScroll(ScrollView.FOCUS_DOWN); } }); } // end log } // end clase
55
Aula Mentor
En el código fuente anterior puedes ver que la aplicación extiende la clase Activity. Además, implementa varias interfaces que corresponden a varios eventos. Después, se sigue con la declaración de los diferentes elementos de la aplicación. La variable posActual almacena la posición de reproducción. En lugar de utilizar directamente la clase VideoView, la hemos extendido en la nueva clase CustomVideoView para poder definir los eventos onPlay(), onPause() y onTimeBarSeekChanged() ya que la clase base no los incluye y es necesario redefinir sus métodos start(), pause() y seekTo() respectivamente para poder detectar cuándo el usuario realiza una de estas acciones. Con el método setVideoURI() hemos indicando el fichero local del paquete de la aplicación. Por simplificación, en este ejemplo hemos utilizado un recurso que se incluye en la carpeta /res/raw/. En una aplicación real no haríamos esto ya que el fichero mp4 se empaqueta con la aplicación y hace que ésta ocupe muchísimo espacio. Si queremos reproducir vídeo desde el sistema de ficheros externo debemos escribir las siguientes sentencias: videoView.setVideoURI(Uri.parse(“file://” + Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_MOVIES) + “/video.mp4”));
56
A continuación, invocamos el método setMediaController() para que el usuario pueda dirigir la reproducción del vídeo mediante el objeto MediaController. El código continúa asignando al objeto CustomVideoView varios escuchadores (listeners) de eventos. Veamos su funcionalidad: - setPlayPauseListener(): método de la clase extendida definido para controlar la reproducción, parada y movimiento del vídeo. En estos casos, se realiza un log y se habilitan o deshabilitan los botones correspondientes en la interfaz de usuario. - OnCompletionListener(): implementa la interfaz onCompletion() que será invocada cuando el vídeo llegue al final. Igualmente, hacemos un log y gestionamos los botones. - setOnPreparedListener(): implementa la interfaz onPrepared() que será invocada cuando el vídeo esté preparado para su reproducción. Igualmente, hacemos un log indicando la duración del vídeo con el método getDuration(). - setOnTouchListener(): implementa la interfaz onTouch() que será invocada cuando el usuario toca la pantalla del vídeo. Hacemos también un log de la acción. - setOnErrorListener(): implementa la interfaz onError() que será invocada cuando se produce un error en la reproducción del vídeo. Hacemos también un log de la acción. Posteriormente, definimos los eventos onClick() de los botones de la interfaz de usuario. Es sencillo y auto explicativo ver en el código de arriba cómo hemos usado los distintos métodos de la clase VideoView para reproducir, parar y pausar el vídeo. Hemos definido también los métodos onPause() y onResume() de la Actividad que se invocan cuando ésta pasa a un segundo plano y cuando vuelve de nuevo a primer plano. El vídeo debe parar la reproducción y continuar en la posición anterior en cada uno de estos casos, por lo que se invocan a sus métodos pause() y start() respectivamente. El último método log() 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. Una vez expuesto la lógica de la Actividad principal, veamos la implementación de la clase extendida de VideoView en el fichero CustomVideoView.java: // Para poder definir los eventos onPlay, onPause y // onTimeBarSeekChanged es necesario redefinir la clase // VideoView extendiendo sus métodos
U1 Multimedia y Gráficos en Android
public class CustomVideoView extends VideoView { private PlayPauseListener mListener;
// Definimos los constructores de la clase public CustomVideoView(Context context) { super(context); } public CustomVideoView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomVideoView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); }
// Método que establece el listener public void setPlayPauseListener(PlayPauseListener listener) { mListener = listener; }
// Redefinimos los método internos de la clase VideoView @Override public void pause() { // Primero llamamos al método de la clase original super.pause(); if (mListener != null) { // Si el listener está asignado, ejecutamos el método mListener.onPause(); } } @Override public void start() { // Primero llamamos al método de la clase original super.start(); if (mListener != null) { // Si el listener está asignado, ejecutamos el método mListener.onPlay(); } } @Override public void seekTo(int msec) { // Primero llamamos al método de la clase original super.seekTo(msec); if (mListener != null) { // Si el listener está asignado, ejecutamos el método mListener.onTimeBarSeekChanged(msec); } } // Definimos las interfaces de la nueva clase que deben
57
Aula Mentor
// implementarse interface PlayPauseListener { void onPlay(); void onPause(); void onTimeBarSeekChanged(int currentTime); } } // end clase
Como puedes ver, la clase anterior es muy sencilla: hemos redefinimos los método internos start(), pause() y seekTo() de la clase VideoView para poder invocar dentro de ésta los métodos que se definen en la interfaz PlayPauseListener: onPlay(), onPause() y onTimeBarSeekChanged() respectivamente. Mediante su método setPlayPauseListener() podemos establecer el listener correspondiente.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 2 (Vídeo) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la API de Vídeo de Android.
Si ejecutas en Eclipse ADT este Ejemplo 2 en el AVD, verás que se muestra la siguiente aplicación: 58
U1 Multimedia y Gráficos en Android
La reproducción de un vídeo en el AVD se realiza mediante software, por lo que puedes observar saltos o paradas al visualizarlo. Es recomendable utilizar un dispositivo real Android que reproduzca el vídeo mediante el hardware.
4. Conceptos básicos de gráficos en Android Antes de comenzar a trabajar con gráficos conviene que el alumno o alumna se familiarice con las clases básica de Android que permiten crear y manipular gráficos en Android.
4.1 Definición de colores en Android Android representa un color utilizando un número entero de 32 bits. Estos bits se dividen en 4 campos de 8 bits: alfa, rojo, verde y azul (ARGB, si usamos las iniciales en inglés). Dado que cada componente de color consta de 8 bits, podrá tomar 256 valores diferentes. Las componentes rojo, verde y azul son utilizadas para definir un color y la componente alfa define su grado de transparencia con respecto al fondo (capa inferior). Un valor de 255 significa un color opaco y, a medida que reduzcamos este valor, el color se irá haciendo más transparente. Podemos definir un color de diferentes maneras. - Definiendo un color estándar de Android:
59
int color = Color.RED;
- Indicando sus valores en el modo ARGB:
color = Color.argb(127, 255, 0, 0);
- Especificando sus valores en hexadecimal: color = 0xFFFF0000;
// Rojo opaco // Rojo transparente // Rojo
- Utilizando un recurso de la aplicación: color = getResources().getColor(R.color.color_rojo); Para conseguir una óptima separación entre la programación Java y el diseño de la interfaz de usuario, se recomienda utilizar la última opción, es decir, no definir directamente los colores en el código fuente, sino utilizar el fichero de recursos res/values/colors.xml del proyecto: #ffff0000
Así, si deseamos cambiar los colores de la aplicación, únicamente debemos modificar este archivo de recursos.
Aula Mentor
4.2 Clases de dibujo en Android A continuación, expondremos las clases más importantes que se utilizan para dibujar en Android:
4.2.1 Clase Paint
60
La clase Paint se emplea para definir el pincel que utilizaremos para pintar. Podemos definir su color, su tipo de trazo, transparencia, etcétera. Veamos los métodos de esta clase más utilizados por el programador: - setColor(int color): indica el color del pincel utilizando una de las definiciones anteriores de colores. - setAlpha(int alfa): modifica el grado de transparencia del pincel. - setStrokeWidth(float grosor): define el grosor del trazado. - setStyle(Paint.Style estilo): marca el estilo de relleno con las constante FILL (relleno), FILL_AND_STROKE (relleno y borde), STROKE (sólo dibuja borde). - setShadowLayer(float radio, float x, float y, int color): realiza un segundo trazado a modo de sombra. - setTextAlign(Paint.Align justif): justifica el texto según las constantes CENTER, LEFT y RIGHT. - setTextSize(float size): establece el tamaño de la fuente del texto. - setTypeface (Typeface typeface): indica el tipo de fuente MONOSPACE, SERIF y SANS_ SERIF. Además, podemos definir negrita e itálica. - setTextScaleX(float escala): señala el factor de escalado horizontal. Un valor de 1.0 indica sin escalado. - setTextSkewX(float inclinacion): indica el factor de inclinación del texto. 0 denota sin inclinación. - setUnderlineText(boolean subrayado): determina si un texto aparece subrayado o no.
4.2.2 Clase Rectángulo La clase Rect permite dibujar un rectángulo que se representa mediante sus cuatro lados: izquierdo, derecho, alto y bajo. Su constructor tiene este aspecto: Rect(int left, int top, int right, int bottom) Podemos obtener su ancho y largo mediante los métodos height() y width() respectivamente.
4.2.3 Clase Path La clase Path (del inglés, camino) permite definir un trazado mediante segmentos de línea y curvas. Un Path también se puede utilizar para dibujar un texto sobre el trazado marcado y para ocultar (tramas) o difuminar. Entre sus métodos más importante podemos encontrar: - moveTo(float x, float y): mueve el pincel de dibujo a la posición x, y. - lineTo(float x, float y): pinta un línea desde la posición actual hasta la posición x, y.
U1 Multimedia y Gráficos en Android
- addRect(float left, float top, float right, float bottom, Path.Direction dir): añade un rectángulo del tamaño indicado siguiendo el sentido dir que puede tomar los valores CW (giro de la agujas del reloj) y CCW (sentido contrario a las agujas del reloj). - addCircle(float x, float y, float radio, Path.Direction dir): añade un círculo de radio siguiendo el sentido dir que puede tomar los valores CW (giro de la agujas del reloj) y CCW (sentido contrario a las agujas del reloj). - offset (float dx, float dy): desplaza el trazado en dx y dy. - reset(): limpia el trazado actual. - close(): cierra el trazado actual, es decir, ya no se pueden añadir nuevos elementos al
mismo.
Fíjate en el siguiente ejemplo que dibuja una línea y un círculo: Path trazado = new Path(); trazado.moveTo(10, 10); trazado.lineTo(10, 50); trazado.addCircle(50, 50, 60, Direction.CCW);
4.2.4 Clase Canvas La clase Canvas (del inglés, lienzo) representa la superficie básica donde podemos dibujar gráficos. Dispone de varios métodos que permiten representar líneas, círculos, texto, etcétera. Para dibujar en un lienzo debemos utiliza un pincel (clase Paint que hemos visto) donde indicamos el color, grosor de trazo, transparencia, etcétera. También es posible definir una matriz de 3x3 (Matrix) que permite transformar coordenadas aplicando una translación, escala o rotación del lienzo. Además, podemos seleccionar un área conocida como Clip para que los métodos de dibujo afecten solo a esta área. A continuación, veamos los métodos más importantes de esta clase Canvas según su función. Como verás, no hemos incluido todos sus métodos, por lo que recomendamos consultar la documentación oficial para obtener información más detallada.
4.2.4.1 Obtener tamaño del Canvas: - int getHeight(): devuelve el ancho del lienzo. - int getWidth(): devuelve el largo del lienzo. Puedes notar que el tamaño del lienzo es un número entero aunque, para dibujar en él, debes indicar posiciones con números decimales (float) contenidos en éste.
4.2.4.2 Dibujar figuras geométricas: - drawCircle(float x, float y, float r, Paint paint): dibuja un círculo en la posición (x, y), de radio r y utilizando el pincel paint. - drawOval(RectF rect, Paint paint): dibuja una eclipse contenida en el rectángulo rect y utilizando el pincel paint. - drawRect(RectF rect, Paint paint): dibuja un rectángulo utilizando el pincel paint. - drawPoint(float x, float y, Paint paint): pinta un punto en la posición (x, y) empleando el pincel paint.
61
Aula Mentor
- drawPoints(float[] puntos, Paint paint): pinta los puntos de la matriz bidimensional puntos empleando el pincel paint.
4.2.4.3 Dibujar líneas y arcos: - drawLine(float iniX, float iniY, float finX, float finY, Paint paint): pinta un línea empezando en la posición (iniX, iniY) y finalizando en (finX, finY) empleando el pincel paint. - drawLines(float[] puntos, Paint paint): pinta una línea continua siguiendo los puntos de la matriz bidimensional puntos empleando el pincel paint. - drawArc(RectF rect, float iniAngulo, float finAngulo, boolean usarCentro, Paint paint): dibuja un arco en el rectángulo rect, de ángulo inicial iniAngulo y final finAngulo, muestra el centro del óvalo si lo indicamos en el parámetro usarCentro y emplea el pincel paint. - drawPath(Path trazo, Paint paint): dibuja un camino utilizando el pincel paint.
4.2.4.4 Dibujar texto:
62
- drawText(String texto, float x, float y, Paint paint): añade un texto en la posición (x, y) utilizando el pincel paint. - drawTextOnPath(String texto, Path trazo, float desplazamHor, float desplazamVert, Paint paint): añade un texto a lo largo del trazo con un desplazamiento horizontal o vertical y utilizando el pincel paint. - drawPosText(String texto, float[] posicion, Paint paint): añade un texto en la posición utilizando el pincel paint.
4.2.4.5 Colorear todo el lienzo Canvas: - drawColor(int color): rellena el lienzo completo con el color. - drawARGB(int alfa, int rojo, int verde, int azul): rellena el lienzo completo
utilizando la terminología ARGB.
- drawPaint(Paint paint): rellena el lienzo completo con el pincel paint.
4.2.4.6 Dibujar imágenes: - drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint): dibuja la imagen bitmap en el rectángulo dst del lienzo recortando la imagen original con el rectángulo src y utilizando el pincel paint. - drawBitmap(Bitmap bitmap, Matrix matriz, Paint pincel): dibuja la imagen bitmap recortando la imagen original utilizando la matriz y el pincel paint.
4.2.4.7 Definir un Clip (área de selección): - boolean clipRect(RectF rect): selecciona el área mediante un rectángulo rect sobre el que se aplicarán los dibujos.
U1 Multimedia y Gráficos en Android
- boolean clipRegion(Region region): selecciona el área mediante la region (superficie que se define con la clase Path) sobre la que se aplicarán los dibujos. - boolean clipPath(Path trazo): selecciona el área mediante el path sobre el que se aplicarán los dibujos.
4.2.4.8 Definir matriz de transformación (Matrix): Una matriz de transformación Matrix permite transformar las coordenadas de referencia aplicando una translación, escala o rotación mediante una matriz 3x3. Veamos sus métodos básicos: - setMatrix(Matrix matriz): establece la matriz en el lienzo. - Matrix getMatrix(): obtiene la matriz en el lienzo. - concat(Matrix matriz): añade una nueva matriz a la transformación ya existente. - translate(float despazX, float despazY): mueve el eje de coordenadas. - scale(float escalaX, float escalaY): escala la matriz activa. - rotate(float grados, float centroX, float centroY): rota la matriz activa. - skew(float despazX, float despazY): inclina la matriz activa. A continuación, se muestra el Ejemplo 3 donde se crea una Vista que se dibuja mediante código Java empleando la clase Canvas, es decir, no se define un fichero layout xml en el proyecto. Si abres el archivo MainActivity.java verás que contiene: public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // No definimos Layout, el layout será definido por CanvasView setContentView(new CanvasView(this)); } } // end clase
El código anterior comienza con la creación de una Activity en la que asociamos un objeto CanvasView extendido de tipo View mediante el método setContentView() que no está definido mediante un layout XML. Sin embargo, si accedes al fichero CanvasView.java verás que define: zpublic class CanvasView extends View { // Trazo del dibujo Path trazo = new Path(); // Pincel del dibujo Paint pincel = new Paint(); // Bitmap con el icono Mentor Bitmap miBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mentor); // Variables para almacenar largo, ancho, centro_x y centro_y float l, a, cx, cy; // Constructor de la clase public CanvasView(Context context) { super(context); } @Override
63
Aula Mentor
protected void onDraw(Canvas canvas) { // Buscamos el largo, ancho y centro del canvas l = getWidth(); a = getHeight(); cx = l/2; cy = a/2;
64
// Dibujamos un trazo de tipo rectángulo trazo.addRect(l/5, a/4, l-l/5, a-a/4, Direction.CW); // Rellenamos de color blanco todo el fondo del canvas canvas.drawColor(Color.WHITE); // Definimos pincel de color azul de ancho 8 y con borde pincel.setColor(Color.BLUE); pincel.setStrokeWidth(8); pincel.setStyle(Style.STROKE); // Dibujamos el rectángulo canvas.drawPath(trazo, pincel); // Limpiamos el trazo trazo.reset(); // Movemos puntero a posición trazo.moveTo(0, a/7); // Trazamos una línea trazo.lineTo(l, a/7); // Indicamos estilo de relleno pincel.setStyle(Style.FILL); // Tamaño del texto, tipo de fuente y alineamiento pincel.setTextSize(25); pincel.setTypeface(Typeface.SANS_SERIF); pincel.setTextAlign(Paint.Align.CENTER); // Imprimimos el texto canvas.drawTextOnPath(“Curso avanzado de Android de Mentor.”, trazo, 0, 0, pincel); // Imprimimos la imagen de Mentor canvas.drawBitmap(miBitmap, cx-miBitmap.getWidth() / 2, cy miBitmap.getHeight() / 2, null); } // end onDraw // Evento que se lanza cada vez que cambia el tamaño de la Vista @Override protected void onSizeChanged(int ancho, int alto, int ancho_anterior, int alto_anterior){ // Mostramos un mensaje indicándolo Toast.makeText(getContext(), “Cambio de tamaño de pantalla. Alto nuevo: “ + alto + “ - Ancho nuevo: “ + ancho, Toast.LENGTH_LONG).show(); } } // end clase CanvasView
La clase CanvasView se extiende de View cambiando su método onDraw() que es el responsable de dibujar la vista. Puedes ver que hemos utilizado los métodos que hemos utilizado anteriormente y vemos su función en los comentarios del código fuente. Además, hemos implementado el método onSizeChanged() que Android invoca cada vez que cambia el tamaño de la Vista.
U1 Multimedia y Gráficos en Android
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 3 (Canvas) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado la clase Canvas. Si ejecutas en Eclipse ADT este Ejemplo 3, verás que se muestra la siguiente aplicación en el Emulador:
65
Si cambias la orientación del AVD con el atajo de teclado [CTRL+F12] verás que la imagen se amolda al nuevo tamaño de pantalla y muestra un mensaje indicándolo.
Aula Mentor
4.2.5 Definición de dibujables (Drawable) Un Dibujable (del inglés, Drawable) es un mecanismo para dibujar la interfaz de una aplicación Android. Existen muchos tipos de recursos dibujables, tales como, archivos de imágenes, colores, gradientes, formas geométricas, etcétera. A continuación, vamos a estudiar los más importantes. Podemos entender la clase Drawable como una abstracción que representa “algo que se puede dibujar”. Muchos de estos dibujables pueden ser definidos como recursos mediante ficheros XML.
Aquellos Drawables que se pueden especificar mediante ficheros XML debemos crearlos en la carpeta res/drawable del proyecto.
66
Entre ellos, podemos encontrar los siguientes: - BitmapDrawable: imagen basada en un fichero de imagen PNG o JPG. En un recurso XML se define con la etiqueta . - ShapeDrawable: permite dibujar un gráfico mediante primitivas vectoriales básicas como círculos, cuadrados, etcétera, y trazados (Path). No puede ser definido mediante un fichero XML. - LayerDrawable: contiene una matriz de elementos de tipo Drawable que podemos utilizar después en la aplicación. En un recurso XML se define con la etiqueta . - StateListDrawable: recurso similar al anterior pero que permite utilizar una máscara de bits para seleccionar los objetos visibles (por ejemplo, botón habilitado, deshabilitado, presionado, etcétera). En un recurso XML se define con la etiqueta . - GradientDrawable: permite definir un degradado de color que puede ser utilizado en botones o fondos. - TransitionDrawable: extensión de LayerDrawables que permite crear efectos de fundido entre capas. Para iniciar la transición hay que invocar el método startTransition(inttiempo). Para visualizar la primera capa hay que invocar resetTransition(). En un recurso XML se define con la etiqueta . - AnimationDrawable: permite crear animaciones fotograma a fotograma utilizando una serie de objetos Drawable. En un recurso XML se define con la etiqueta . Notamos que es posible utilizar como base la clase Drawable o uno de sus descendientes para crear tus propias clases gráficas. Al inicio de la Unidad 2 estudiaremos algunos tipos de Drawables más. Además, esta clase Drawable proporciona una serie de mecanismos para indicar cómo debe ser pintado el gráfico (cada tipo de Drawable implementa algunos de ellos). Veamos los más importantes: - setBounds(x1, y1, x2, y2): indica el rectángulo donde se debe dibujar el Drawable. Éste debe respetar el tamaño indicando por el programador, es decir, el dibujo se escala. Podemos consultar el tamaño de un Drawable mediante los métodos getIntrinsicHeight() y getIntrinsicWidth(). - getPadding(Rect): proporciona información sobre los márgenes recomendados para representar contenidos. Por ejemplo, un Drawable destinado a ser el marco de un botón, debe devolver los márgenes correctos para localizar las etiquetas u otros contenidos en el interior del botón.
U1 Multimedia y Gráficos en Android
- setState(int[]): indica al Drawable en qué estado ha de ser dibujado. Por ejemplo “con foco”, “seleccionado”, etcétera. Algunos Drawable cambian su aspecto en función de este
estado.
Veamos con más detalle algunas de las subclases de Drawable:
4.2.5.1 Dibujable de tipo bitmap (BitmapDrawable) Como has visto, la forma más sencilla de añadir imágenes a una aplicación Android es incluirlas en la carpeta res/drawable del proyecto. El SDK de Android soporta los formatos PNG, JPG y GIF. El formato recomendado es PNG, aunque también se puede utilizar JPG. Android desaconseja el uso del formato GIF. Cada imagen de esta carpeta se asocia automáticamente a un ID de recurso. Por ejemplo, para el archivo mentor.png creará el ID mentor que permite hacer referencia a la imagen desde el código o desde un archivo de recursos XML:
Si usamos sentencias Java, podemos escribir:
Drawable miImagen = context.getResources().getDrawable(R.drawable.mi_imagen);
4.2.5.2 GradientDrawable (Gradiente dibujable) Un Gradient Drawable permite al programador dibujar un gradiente de colores que consiste en mostrar un degradado de dos colores en el fondo de cualquier Vista. Por ejemplo, el siguiente archivo define un degradado desde el color blanco (FFFFFF) a rojo (FF0000):
Este tipo de objetos gráficos se utiliza con frecuencia como fondo de botones o de pantalla. El parámetro angle establece la dirección del degradado. Únicamente se pueden definir los ángulos 0, 90, 180 y 270. Si guardamos el archivo anterior en res/drawable/degradado.xml entonces podemos utilizarlo para establecer el fondo de una vista en su Layout en XML introduciendo el siguiente atributo en el Layout main.xml de la aplicación: android:background=”@drawable/degradado” Además, es posible introducir la siguiente sentencia en el constructor de una Actividad para que este drawable sea utilizado como degradado de fondo: setBackgroundResource(R.drawable.degradado);
67
Aula Mentor
4.2.5.3 ShapeDrawable (Dibujable con forma) Este drawable permite dibujar formas dinámicamente mediante primitivas vectoriales disponibles en Android: Veamos un ejemplo sencillo que pinta una forma ovalada y la rellena de color rojo: ShapeDrawable imagen = new ShapeDrawable(new OvalShape()); imagen.getPaint().setColor(0xffff0000); imagen.setBounds(10, 10, 250, 75); imagen.draw(canvas);
Con la orden setBounds() hemos establecido los límites de la forma, es decir, su tamaño.
4.2.5.4 AnimationDrawable (Dibujable animado) Android proporciona varias formas de crear animaciones, también mediante Drawables, que tienen la ventaja de que se pueden crear desde un fichero XML. Estas animaciones se crean a partir de un grupo de fotogramas. Para ello, emplearemos la clase AnimationDrawable. Veamos en el Ejemplo 4 de esta Unidad una aplicación sencilla que utiliza este tipo de animación. Si abres el archivo res/drawable/animacion.xml advertirás que hemos definido los fotogramas de la animación utilizando la etiqueta animation-list y las imágenes contenidas en esta carpeta: 68
Si accedes al fichero Java MainActivity.java que describe la lógica de la aplicación observarás que contiene:
public class MainActivity extends Activity { AnimationDrawable animacion; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Buscamos las imágenes de la animación animacion = (AnimationDrawable)getResources(). getDrawable(R.drawable.animacion); // Definimos la Vista donde vamos a mostrar la animación ImageView imagen = new ImageView(this); // La coloremos de blanco
U1 Multimedia y Gráficos en Android
imagen.setBackgroundColor(Color.WHITE); // Establecemos la animación en la imagen imagen.setImageDrawable(animacion); // Definimos el evento onClick de la imagen que inicia o para la // animación imagen.setOnClickListener(new OnClickListener() { public void onClick(View view) { if (animacion.isRunning()) animacion.stop(); else animacion.start(); } }); // El contenido de la actividad es la imagen setContentView(imagen); } // end onCreate } // end clase
El código fuente anterior comienza declarando el objeto animacion de la clase AnimationDrawable. Éste se inicializa a partir del fichero XML anterior incluido en la carpeta de recursos. Después, se crea una nueva vista del tipo ImageView que sirve de contenedor de esta vista animacion. Finalmente, se crea un listener de evento onClick para que la animación se inicie o se pare utilizando los métodos start() y stop() respectivamente. De igual forma que el ejemplo anterior, se usa la Vista animacion para dibujar mediante código Java la interfaz del usuario, es decir, no se define fichero layout xml en el proyecto.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 4 (Animación Drawable) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado la clase AnimationDrawable.
Si ejecutas en Eclipse ADT este Ejemplo 4, verás que se muestra la siguiente aplicación en el Emulador:
69
Aula Mentor
En el apartado siguiente de esta Unidad estudiaremos animaciones más complejas. En el manual oficial sobre DRAWABLES de Android puedes encontrar un listado completo de todos los drawables disponibles, así como sus propiedades.
5. Animaciones de Android Android dispone de tres mecanismos para crear animaciones en las aplicaciones: - AnimationDrawable: mediante esta clase, ya vista en el apartado anterior, podemos crear drawables que reproducen una animación fotograma a fotograma (en inglés se denomina Frame Animation). - Animaciones Tween: crean efectos de translación, rotación, zoom y alfa a cualquier vista de una aplicación Android, cambiando su representación en la pantalla. - API de animación de Android: anima cualquier propiedad de un objeto Java sea del tipo Vista o no, modificando el objeto en sí mismo. A continuación, mediante ejemplos aprenderemos a usar estos dos últimos mecanismos para crear animaciones en Android.
5.1 Animaciones Tween
70
Una animación “tween” (del inglés “between”, que significa en medio o entre) consiste en realizar una serie de transformaciones simples en las Vistas de una Actividad, como su posición, tamaño, rotación y transparencia. Por ejemplo, es posible mover, rotar, modificar el tamaño o cambiar la transparencia a un objeto del tipo TextView.
Para componer la secuencia de instrucciones que definen esta animación de tipo “tween” podemos utilizar un archivo xml o código Java. Es recomendable emplear el archivo xml al ser más legible y reutilizable.
La clase Animation de Android es la que permite crear animaciones en las Vistas de una Actividad. Las instrucciones que definen esta animación son transformaciones donde indicamos cuándo ocurrirán y cuánto tiempo tardarán en completarse. Estas transformaciones pueden ejecutarse de forma secuencial o simultánea. Cada tipo de transformación posee unos parámetros específicos, si bien existen unos parámetros comunes a todas ellas, como son el tiempo de duración y de inicio. Los ficheros XML que definen animaciones deben almacenarse en el directorio res/ anim/ del proyecto Android y deben contener un único elemento raíz que indique las transformaciones que deseamos ejecutar. Esta etiqueta raíz debe ser una de las siguientes: - : mueve la Vista. - : rota la Vista. - : escala la Vista. - : modifica la opacidad de la Vista. - : conjunto de varias transformaciones anteriores.
U1 Multimedia y Gráficos en Android
Por defecto, todas las instrucciones de una animación ocurren a partir del instante inicial. Si es necesario que una animación comience más tarde, hay que especificar su atributo startOffset.
5.1.1 Atributos de las transformaciones Tween Los atributos siguientes se aplican a todas las transformaciones: - startOffset: instante inicial de la transformación en milisegundos. - duration: duración de la transformación en milisegundos. - repeatCount: número de repeticiones adicionales de la animación. - Interpolator: permite acelerar o desacelerar la animación. Alguno de sus valores posibles son: • accelerate_decelerate_interpolator • accelerate_interpolator • anticipate_interpolator • anticipate_overshoot_interpolator • bounce_interpolator • cycle_interpolator • decelerate_interpolator • linear_interpolator • overshoot_interpolator Al final de este apartado veremos su descripción. En el manual oficial de Android sobre Tween puedes encontrar más información sobre estos aceleradores de la animación. Veamos ahora la lista de transformaciones que tiene atributos específicos: Nombre transformación
Atributo
Descripción
fromXDelta toXDelta
Valores inicial y final del desplazamiento en el eje X. Valores inicial y final del desplazamiento en el eje Y. Grado inicial y final de la rotación. Para realizar un giro completo en sentido antihorario debemos establecer 0 y 360 respectivamente. Para sentido horario, de 360 a 0 o de 0 a -360. Para dos giros consecutivos escribe 0 y 720. Punto sobre el que se realiza el giro que queda fijo en la pantalla. Valor inicial y final para la escala del eje X (0.5=50%, 1=100%)
fromYDelta toYDelta
fromDegrees toDegrees
pivotX pivotY
fromXScale toXScale fromYScale toYScale pivotX pivotY fromAlpha, toAlpha
Valor inicial y final para la escala del eje Y Punto sobre el que se realiza el giro que queda fijo en la pantalla. Valor inicial y final de la opacidad.
71
Aula Mentor
En el Ejemplo 5 de esta Unidad hemos desarrollado una aplicación sencilla que utiliza este tipo de animación Tween. Si abres el archivo res/anim/animacion.xml advertirás que hemos definido un conjunto de transformaciones de la animación utilizando la etiqueta set:
72
Es bastante sencillo entender las transformaciones que aplicamos secuencialmente en el archivo anterior. Si accedes al fichero Java MainActivity.java que describe la lógica de la aplicación, observarás que contiene las sentencias siguientes: public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Buscamos las imágenes de la animación setContentView(R.layout.main); final TextView texto = (TextView)findViewById(R.id.textoAnimado); // Definimos la animación final Animation animacion = AnimationUtils.loadAnimation(this, R.anim.animacion); // Evento que lanza Android cuando le ocurre algo a la animación animacion.setAnimationListener(new AnimationListener() { @Override public void onAnimationStart(Animation animation) {} @Override public void onAnimationRepeat(Animation animation) {} @Override public void onAnimationEnd(Animation animation) { // Cuando la animación acaba, volvemos a iniciarla
U1 Multimedia y Gráficos en Android
// La animación se repite de forma infinita texto.startAnimation(animacion); } }); // Iniciamos la animación al texto texto.startAnimation(animacion); } // end onCreate } // end clase
El código fuente anterior comienza declarando el objeto animacion de la clase Animation que se inicializa mediante el método loadAnimation() a partir del fichero XML anterior incluido en la carpeta anim. Después, se inicia la animación sobre una etiqueta TextView mediante el método startAnimation().
Podemos utilizar el método startAnimation() sobre cualquier subclase de View, es decir, podemos animar cualquier Vista con este mecanismo.
Finalmente, se crea un listener del tipo AnimationListener para detectar cuándo se inicia la animación, se para o se repite. En este caso usamos este listener para que la animación comience de nuevo.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 5 (Animaciones Tween) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado una Animación Tween de Android.
Si ejecutas en Eclipse ADT este Ejemplo 5 en el AVD, verás que se muestra la siguiente aplicación:
73
Aula Mentor
5.2 API de Animación de Android En este apartado vamos a estudiar la API de Animación de Android que permite cambiar las propiedades de objetos en un intervalo de tiempo, dando así al usuario una sensación de movimiento lineal en la interfaz. Esta API está disponible a partir de la versión 3.0 de Android (nivel de API 11). Sin embargo, existe una librería en Internet que permite utilizar animaciones en versiones anteriores. En este curso sólo utilizaremos la librería nativa de Android. Mediante esta API podemos indicar el movimiento de una Vista o cualquier objeto Java de una aplicación indicando su nueva posición y el tiempo en el que debe acabar la animación. A diferencia de las animaciones Tween, que únicamente son aplicables a Vistas, la API de animación puede aplicarse a cualquier tipo de objetos Java. Además, es más flexible poder animar cualquier propiedad del objeto, es decir, no está restringida a las cuatro transformaciones que hemos estudiado anteriormente. Por ejemplo, podemos crear una animación que cambie progresivamente el color de fondo de una Vista. Las animaciones Tween sólo modifican la forma en que Android representa una Vista, pero sus propiedades internas no cambian. Por ejemplo, si aplicamos una animación Tween a una etiqueta para desplazarla por la pantalla, la animación se visualizará correctamente pero, al finalizar, la etiqueta seguirá en la posición inicial. Si hubiéramos empleado la API de Animación, las propiedades de la etiqueta hubieran sido modificadas efectivamente.
74
Desventajas de las animaciones Tween: - Únicamente podemos animar objetos de la clase View. - Está limitada a cuatro transformaciones estáticas (no se puede cambiar el color de fondo). - Modifica la representación de la vista, pero no sus propiedades en sí. Desventajas de la API de Animación: - Solo disponible a partir de la versión 3.0 de Android. - Requiere más tiempo en cargarse en el dispositivo y el programador debe escribir más código Java.
La API de Animación de Android se denomina también Animación de Propiedades ya que, como hemos dicho, modifica las propiedades propiamente dichas del objeto que se anima.
5.2.1 Clases principales de la API de animación - Animator: superclase de la que se extienden el resto de clases. - ValueAnimator: permite modificar el valor de una variable o Vista. - ObjectAnimator: permite establecer los atributos del movimiento de un objeto. - AnimatorSet: permite ejecutar animaciones en secuencia o en paralelo. - AnimatorBuilder: permite añadir animaciones a la clase AnimatorSet y relacionarlas entre sí de forma compleja. - AnimatorListener: permite definir listeners en función del estado de la animación, por ejemplo, cuando comienza o cuando finaliza. - PropertyValuesHolder: permite animar múltiples valores durante el ciclo de animación. - Keyframe: permite crear animaciones con fotogramas.
U1 Multimedia y Gráficos en Android
- TypeEvaluator: si la propiedad que queremos modificar en una animación es un objeto en sí mismo, esta interface permite interactuar con él. - ViewPropertyAnimator: permite animar de forma automática y optimizada varias propiedades a la vez de una Vista exclusivamente. - LayoutTransition: permite animar los Layout mediante transiciones. Es posible definir animaciones en archivos XML dentro del directorio /res/anim del proyecto empleando esta API. Además, cuando una aplicación consta de varias Actividades, podemos también animar los cambios que ocurren cuando el usuario cambia la Actividad activa. A continuación, vamos a estudiar en detalle esta API.
5.2.1.1 Animator Superclase de la API de la que se extienden el resto de clases. Veamos los métodos más importantes de esta clase: - start(): comienza la animación. - end(): termina la animación. El objeto se queda en el último estado marcado por esta animación. - cancel(): cancela la animación y vuelve al estado inicial. - setDuration(long duration): indica la duración en milisegundos de la animación. - setTarget(): establece el objeto que va a animar. - addListener(): añade el listener correspondiente a los cambios de estado de la animación. - removeAllListeners(): quita todos los listeners de la animación. - removeListener(listener): quita el listener indicado como parámetro de la animación. - isRunning(): señala si la animación se está ejecutando. - setInterpolator(): establece el interpolador que usaremos en la animación. - setStartDelay(long retraso): retrasa el inicio de la animación después de invocar el método start(). Como es habitual, el tiempo retraso se indica en milisegundos. Hay que tener en cuenta que todas las clases siguientes, al extenderse de ésta, heredan los métodos anteriores.
5.2.1.2 ValueAnimator Una forma de entender mejor por qué se llama también a esta API “Animación de Propiedades“ es mediante el siguiente ejemplo que define una animación mediante la clase ValueAnimator. En ésta se modifica el valor de una variable de tipo entero durante un intervalo de tiempo de 5000 milisegundos. Veamos código fuente del ejemplo: // Definimos el animator que cambia el valor entero de 10 a 200 ValueAnimator animacion = ValueAnimator.ofInt(10f, 200f); // Indicamos la duración de la animación animacion.setDuration(5000); // Establecemos el listener para detectar el cambio del valor entero animacion.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() {
75
Aula Mentor
public void onAnimationUpdate(ValueAnimator animation) { Int valor = (Int)animation.getAnimatedValue(); // Durante 5 segundos la variable valor va cambiando // desde 10 hasta 200. Por defecto, esta clase // modifica el valor cada 10 milisegundos } } ); // Iniciamos la animación animacion.start();
La clase ValueAnimator es un mecanismo para hacer algo cada 10 milisegundos (valor por defecto).
5.2.1.3 ObjectAnimator
76
La clase ObjectAnimator (animador de objetos) es un mecanismo de animación que consiste en modificar un objeto y animarlo desde un estado “inicial” a un estado “final” en un período de tiempo concreto. Este período de tiempo se define en milisegundos. Además, podemos indicar la rutina que marca cómo se comporta la animación durante ese período de tiempo; estas rutinas se denominan interpoladores (del inglés, interpolators). El interpolador predeterminado es accelerate_decelerate que comienza y termina la animación con una aceleración y desaceleración suaves respectivamente. Un poco más adelante, trataremos los interpoladores con un ejemplo. Veamos ahora, los métodos más importantes de esta clase: - ofFloat(Object objeto, String nombrePropiedad, float... valores): establece los nuevos valores de tipo float que debe tomar el objeto para la propiedad nombrePropiedad al final de la animación. - ofInt(Object objeto, String nombrePropiedad, int ... valores): establece los nuevos valores de tipo entero que debe tomar el objeto para la propiedad nombrePropiedad al final de la animación. - ofObject(Object objeto, String nombrePropiedad, TypeEvaluator evaluador, Object... valores): establece los nuevos valores del tipo object que debe tomar el objeto para la propiedad nombrePropiedad y deben interpretarse con el evaluador al
final de la animación.
- ofPropertyValuesHolder(Object objeto, PropertyValuesHolder... valores): establece los nuevos conjuntos de valores del tipo PropertyValueHolder que debe tomar el objeto. Como su nombre indica, PropertyValueHolder se usa para denotar un conjunto
de propiedades del objeto con los valores asociados. En el Ejemplo 6 de esta Unidad veremos cómo se utiliza.
5.2.1.4 AnimatorSet Esta clase AnimatorSet facilita la creación de animaciones en secuencia o en paralelo. Veamos los métodos más destacables de esta clase: - playSequentially(Animator... animaciones): las animaciones indicadas se ejecutaran secuencialmente.
U1 Multimedia y Gráficos en Android
- playTogether(Animator... animaciones): las animaciones señaladas se ejecutaran al
mismo tiempo.
- play(Animator animacion): este método devuelve un objeto del tipo AnimatorBuilder
que permite interrelacionar las animaciones. A continuación, veremos un ejemplo.
5.2.1.5 AnimatorBuilder La clase AnimatorBuilder permite añadir animaciones a la clase AnimatorSet y relacionarlas de forma más compleja indicando la interdependencia entre ellas. Veamos un ejemplo para aclarar el concepto: // Creamos un objeto de la clase AnimatorSet AnimatorSet s = new AnimatorSet(); // Ejecutamos la animación 1 con la animación 2 (a la vez) s.play(anim1).with(anim2); // Ejecutamos la animación 3 antes de la animación 2, es decir, antes de la animación 1 también. s.play(anim2).before(anim3); // Ejecutamos la animación 3 después de la animación 4 s.play(anim4).after(anim3);
Es importante saber que la clase AnimatorBuilder no dispone de constructor, es decir, se construye internamente al invocar el método play(). Veamos los métodos de esta clase: - after(Animator animación): ejecuta la animación después de la animación indicada en el método play(). - after(long retraso): marca el tiempo de retraso en ejecutar la animación establecida
en el método anterior.
- before(Animator animación): ejecuta la animación antes de la animación indicada en el método play(). - with(Animator animación): ejecuta la animación a la vez que la animación indicada en el método play().
5.2.1.6 AnimationListener Como es habitual, los eventos de la animación se definen en la clase de tipo interface:
public static interface Animator.AnimatorListener { // Se invoca cuando se inicia la animación abstract void onAnimationStart(Animator animation); // Se invoca cuando se repite la animación abstract void onAnimationRepeat(Animator animation); // Se invoca cuando se cancela la animación abstract void onAnimationCancel(Animator animation); // Se invoca cuando la animación termina abstract void onAnimationEnd(Animator animation); }
Como puedes ver, los listeners son bastante intuitivos y sencillos de implementar.
77
Aula Mentor
5.2.1.7 PropertyValuesHolder Esta clase permite animar múltiples valores durante el ciclo de animación. Puede entenderse esta clase como un objeto que contiene la dupla propiedad/valor. Podemos utilizar esta clase para crear animaciones con ValueAnimator y ObjectAnimator y, así, ejecutar varios cambios de propiedades en paralelo. Veamos los métodos más importantes de esta clase: - ofFloat(String nombrePropiedad, float... valores): establece los nuevos valores de tipo float que debe tomar la propiedad nombrePropiedad al final de la animación. - ofInt(String nombrePropiedad, int ... valores): establece los nuevos valores de tipo entero que debe tomar la propiedad nombrePropiedad al final de la animación. - ofObject(String nombrePropiedad, TypeEvaluator evaluador, Object... valores): establece los nuevos valores del tipo object que debe tomar la propiedad nombrePropiedad y deben interpretarse con el evaluador al final de la animación. - ofKeyframe(Property propiedad, Keyframe... valores): permite crear una animación de tipo de fotograma para la propiedad.
5.2.1.8 Keyframe
78
Esta clase base permite crear animaciones con fotogramas. Esta clase alberga la dupla tiempo/ valor que se debe aplicar a una animación. Se puede utilizar con las clases ValueAnimator y ObjectAnimator. Para entender cómo funciona, podemos hacer una analogía con el cine, es decir, cada fotograma Keyframe es un estado del objeto y el movimiento de la animación se produce al cambiar el fotograma en el tiempo. Incluso es posible definir para cada Keyframe un objeto del tipo TimeInterpolator que define cómo se hace la interpolación en el cambio de fotograma. Veamos los métodos más importantes de esta clase: - ofFloat(float tiempo, float... valores): establece los nuevos valores de tipo float que debe tener la animación al final del fotograma en la fracción de tiempo medida como
porcentaje de 0 a 1 del tiempo total.
- ofInt(float tiempo, int
... valores): establece los nuevos valores de tipo entero que debe tener la animación al final del fotograma en la fracción de tiempo medida como porcentaje de 0 a 1 del tiempo total. - ofObject(float tiempo, Object... valores): establece los nuevos valores de tipo Object que debe tener la animación al final del fotograma en la fracción de tiempo medida como porcentaje de 0 a 1 del tiempo total.
5.2.1.9 TypeEvaluator Esta interface permite interactuar con un objeto cuando éste es la propiedad que queremos animar. Hay que tener en cuenta que los objetos tienen mucha complejidad y debemos indicar cómo se animan. Sólo dispone del método evaluate(float tiempo, T valorInicial, T valorFinal) que devuelve el resultado de evaluar el cambio desde el valorInicial al valorFinal del objeto durante el tiempo.
U1 Multimedia y Gráficos en Android
Android dispone de los evaluadores por defecto: - ArgbEvaluator: para cambios de color. - FloatEvaluator: para cambios en números decimales. - IntEvaluator: para cambios en números enteros.
Vemos ahora un sencillo ejemplo que evalúa una animación que utiliza puntos (clase PointF) de Android: // Definimos el evaluador de la clase PointF (punto de Android) public class MiPuntoEvaluator implements TypeEvaluator { // Definimos su método evaluate public PointF evaluate(float fraction, PointF valorInicio, PointF valorFinal) { // Obtenemos la posición actual PointF valorInicio = (PointF) valorInicio; PointF valorFinal = (PointF) valorFinal; // Devolvemos la nueva posición moviendo el punto la fracción // indicada return new PointF( valorInicio.x + fraction * (valorFinal.x - valorInicio.x), valorInicio.y + fraction * (valorFinal.y - valorInicio.y)); } }
79
5.2.1.10 ViewPropertyAnimator Esta clase permite animar de forma automática y optimizada varias propiedades de una Vista exclusivamente al mismo tiempo. La sintaxis de esta clase es muy intuitiva y fácil de aplicar ya que únicamente debemos decirle a la Vista que deseamos animarla indicando el nuevo valor de la propiedad. Es decir, no tenemos que utilizar todo el mecanismo de Animator. Esta clase no dispone de constructor, por lo que para crear un objeto de este tipo debemos invocar la orden animate() en la Vista que deseamos animar y que devolverá una instancia a la clase ViewPropertyAnimator. Veamos los métodos más importantes de esta clase que permiten modificar propiedades de una Vista: - alpha(float valor): cambia la opacidad de la Vista según el valor. - rotationX(float valor): rota la Vista en el eje X según el valor. - rotationY(float valor): rota la Vista en el eje Y según el valor. - scaleX(float valor): escala la Vista en el eje X según el valor. - scaleY(float valor): escala la Vista en el eje Y según el valor. - translationX(float valor): traslada la Vista en el eje X según el valor. - translationY(float valor): traslada la Vista en el eje Y según el valor. - x(float valor): cambia la posición X de la Vista según el valor. - y(float valor): cambia la posición Y de la Vista según el valor.
Aula Mentor
5.2.1.11 LayoutTransition Esta clase permite animar los Layout mediante transiciones cada vez que se añade o se quita una Vista de este contenedor. Para ello, debemos invocar el método setLayoutTransition (LayoutTransition) en el Layout que deseemos animar.
5.3 Animación de Actividad También es posible aplicar animaciones a los cambios entre Actividades de una aplicación. Para hacerlo, debemos definir el método overridePendingTransition() en la Actividad activa. Este método tiene dos parámetros: la animación de salida de la Actividad actual y la animación de entrada de la Actividad nueva. En el Ejemplo 6 de esta Unidad hemos desarrollado una aplicación sencilla que utiliza la API de animación. Si abres el archivo res/layout/main_layout.xml advertirás que hemos definido una etiqueta que animaremos y un conjunto de botones que permitirán al usuario elegir cómo desea animarla:
80
81
Aula Mentor
Si abres el archivo res/anim/fundidos.xml advertirás que hemos definido un conjunto de transformaciones en la opacidad (alpha) utilizando la etiqueta set:
82
Si accedes al fichero Java MainActivity.java que describe la lógica de la aplicación, observarás que contiene las siguientes sentencias: public class MainActivity extends Activity implements AnimatorListener { // Etiqueta que vamos a animar private TextView etiqueta = null; private float fuenteEtiqueta; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); etiqueta = (TextView)this.findViewById(R.id.etiqueta);
U1 Multimedia y Gráficos en Android
fuenteEtiqueta=etiqueta.getTextSize(); } // Evento que se lanza cuando el usuario pulsa un botón public void animar(View boton) { // Variables que se usan más abajo float dest = 0; float h, w , x , y; PropertyValuesHolder pvhX; // Animación de tipo objeto ObjectAnimator animacion=null; // Animación secuencial AnimatorSet as = null;
// Ponemos la etiqueta con su color y tamaño fuente originales etiqueta.setBackgroundColor(getResources().getColor( R.color.azul)); etiqueta.setTextSize(fuenteEtiqueta); // Si el usuario pulsa el botón... switch (boton.getId()) { // Boton que hace fundido a negro o blanco case R.id.boton1: // Ponemos la etiqueta en posición 0 en eje X etiqueta.setX(0); // Obtenemos el botón que ha sido pulsado Button tButton = (Button)boton; // Si la etiqueta no está en negro if (etiqueta.getAlpha() != 0) { // Definimos una animación que cambia la propiedad // alpha (opacidad) de la etiqueta al negro (0f) animacion = ObjectAnimator.ofFloat(etiqueta, “alpha”, 0f); // Cambiamos la etiqueta botón tButton.setText(“Fundido a negro”); } else { // Definimos una animación que cambia la propiedad // alpha de la etiqueta al blanco (1f) animacion = ObjectAnimator.ofFloat(etiqueta, “alpha”, 1f); // Cambiamos la etiqueta botón tButton.setText(“Fundido a blanco”); } // La animación dura 5 segundos animacion.setDuration(5000); break; // Botón Mover case R.id.boton2: // Dejamos la etiqueta sin fundido // Usamos la clase Paint para medir el tamaño que // ocupa el texto de la etiqueta Paint paint = new Paint(); float longitudEtiqueta = paint.measureText( etiqueta.getText().toString()); // Vamos a mover la etiqueta hacia la izq hasta el
83
Aula Mentor
84
// final de la longitud del texto dest = 0 - longitudEtiqueta; // Si la etiqueta ya está fuera de la pantalla // volvemos a ponerla en su sitio if (etiqueta.getX() < 0) { dest = 0; } // Definimos una animación que cambia la propiedad // X de la etiqueta hacia dest animacion = ObjectAnimator.ofFloat(etiqueta,”x”, dest); // Definimos la animación durante 2 segundos animacion.setDuration(2000); break; // Animación secuencial por lotes case R.id.boton3: // Ponemos la etiqueta en su sitio y sin fundido etiqueta.setX(0); etiqueta.setAlpha(1f); // Definimos una animación que cambia el color de la // etiqueta del azul actual al negro ObjectAnimator color1 = ObjectAnimator.ofInt(etiqueta, “backgroundColor”, getResources().getColor(R.color.negro)); // Definimos una animación que cambia de nuevo el // color de la etiqueta del negro actual al azul ObjectAnimator color2 = ObjectAnimator.ofInt(etiqueta,”backgroundColor”, getResources().getColor(R.color.azul)); // Cambiamos el tamaño de la fuente a 40f ObjectAnimator fuente1 = ObjectAnimator.ofFloat(etiqueta, “textSize”, 40f); // Dejamos el tamaño de la fuente inicial ObjectAnimator fuente2 = ObjectAnimator.ofFloat(etiqueta, “textSize”, fuenteEtiqueta); // Definimos un animador por lotes as = new AnimatorSet(); // Ejecutamos la animación en secuencia as.playSequentially(color1,color2, fuente1, fuente2); // La animación dura 4 segundos as.setDuration(4000); break; // Animación desde fichero XML case R.id.boton4: // Ponemos la etiqueta en su sitio y sin fundido etiqueta.setX(0); etiqueta.setAlpha(1f); // Cargamos la animación de un archivo XML as = (AnimatorSet)AnimatorInflater.loadAnimator(this, R.anim.fundidos); // Indicamos la Vista sobre la que se debe aplicar as.setTarget(etiqueta); break; // Botón PropertiesHolder (Contenedor de propiedades)
U1 Multimedia y Gráficos en Android
case R.id.boton5: // Ponemos la etiqueta en su sitio y sin fundido etiqueta.setX(0); etiqueta.setAlpha(1f); // Obtenemos el tamaño de la etiqueta h = etiqueta.getHeight(); w = etiqueta.getWidth(); x = etiqueta.getX(); y = etiqueta.getY(); // Movemos la etiqueta a un lado etiqueta.setX(w); etiqueta.setY(h); // Creamos un PropertiesHolder en el eje X e Y para // que vuelva a la posición inicial pvhX = PropertyValuesHolder.ofFloat(“x”, x); PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(“y”, y); // Definimos la animación con estos PropertiesHolder animacion =ObjectAnimator.ofPropertyValuesHolder( etiqueta, pvhX, pvhY); // La animación dura 5 segundos animacion.setDuration(5000); // Definimos un acelerador de la animación animacion.setInterpolator(new AccelerateDecelerateInterpolator()); break; // Botón ViewAnimator case R.id.boton6: // Ponemos la etiqueta en su sitio y sin fundido etiqueta.setX(0); etiqueta.setAlpha(1f); // Obtenemos el tamaño de la etiqueta h = etiqueta.getHeight(); w = etiqueta.getWidth(); x = etiqueta.getX(); y = etiqueta.getY(); // Movemos la etiqueta a un lado etiqueta.setX(w); etiqueta.setY(h); // Definimos una animación del tipo ViewAnimator ViewPropertyAnimator vpa = etiqueta.animate(); // Definimos la posición final de la etiqueta en la // animación vpa.x(x); vpa.y(y); // La animación dura 5 segundos vpa.setDuration(5000); // Definimos el listener de la animación vpa.setListener(this); // Definimos un acelerador de la animación vpa.setInterpolator(new AccelerateDecelerateInterpolator()); break; // Botón TypeEvaluator case R.id.boton7: // Ponemos la etiqueta en su sitio y sin fundido
85
Aula Mentor
86
etiqueta.setX(0); etiqueta.setAlpha(1f); // Definimos el color inicial y final de la animación Integer colorIni = getResources().getColor(R.color.rojo); Integer colorFin = getResources().getColor(R.color.azul); // Definimos la animación utilizando el TypeEvaluator // ArgbEvaluator e indicando los colores inicial y // final ValueAnimator colorAnimation = ValueAnimator.ofObject( new ArgbEvaluator(), colorIni, colorFin); // Definimos el listener que se lanza cada vez que es // necesario actualizar la animación colorAnimation.addUpdateListener( new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { // Cambiamos el color de la etiqueta etiqueta.setBackgroundColor( (Integer)animator.getAnimatedValue()); } }); // La animación dura 5 segundos colorAnimation.setDuration(5000); // Quitamos todos los listeners de la animación colorAnimation.removeAllListeners(); // Añadimos el listener local colorAnimation.addListener(this); // Iniciamos la animación colorAnimation.start(); break; // Botón KeyFrames (Fotogramas) case R.id.boton8: // Ponemos la etiqueta en su sitio y sin fundido etiqueta.setX(0); etiqueta.setAlpha(1f); // Obtenemos el tamaño de la etiqueta h = etiqueta.getHeight(); w = etiqueta.getWidth(); x = etiqueta.getX(); y = etiqueta.getY(); // Fotograma de inicio : 0.2 // Valor alpha: 0.8 Keyframe kf0 = Keyframe.ofFloat(0.2f, 0.8f); // Fotograma intermedio: 0.5 // Valor alpha: 0.2 Keyframe kf1 = Keyframe.ofFloat(.5f, 0.2f); // Fotograma final: 0.8 // Valor alpha: 0.8 Keyframe kf2 = Keyframe.ofFloat(0.8f, 0.8f); // Definimos un PropertyHolder para almacenar los // cambios PropertyValuesHolder pvhAlpha =
U1 Multimedia y Gráficos en Android
PropertyValuesHolder.ofKeyframe(“alpha”, kf0, kf1, kf2); // Definimos también el movimiento horizontal pvhX = PropertyValuesHolder.ofFloat(“x”, w, x); // Definimos la animación a partir de fotogramas animacion = ObjectAnimator.ofPropertyValuesHolder( etiqueta, pvhAlpha,pvhX); // La animación dura 5 segundos animacion.setDuration(5000); break; default: break; } if (animacion!=null) { // Quitamos todos los listeners de la animación animacion.removeAllListeners(); // Añadimos el listener local animacion.addListener(this); // Iniciamos la animación animacion.start(); } else if (as!=null) { // Quitamos todos los listeners de la animación as.removeAllListeners(); // Añadimos el listener local as.addListener(this); as.start(); } } // end animar // Métodos que lanza Android cuando ocurre un evento en la animación @Override public void onAnimationCancel(Animator animation) { Toast.makeText(this, “Se cancela la animación”, Toast.LENGTH_SHORT).show(); } @Override public void onAnimationEnd(Animator animation) { Toast.makeText(this, “Finaliza la animación”, Toast.LENGTH_SHORT).show(); } @Override public void onAnimationRepeat(Animator animation) {} @Override public void onAnimationStart(Animator animation) { Toast.makeText(this, “Empieza la animación”, Toast.LENGTH_SHORT).show(); } } // end clase
El código fuente anterior utiliza la clase ObjectAnimator aprovechando sus métodos ofFloat y ofInt para cambiar la opacidad (propiedad alpha) y mover la etiqueta (propiedad x)
87
Aula Mentor
respectivamente. Mediante la clase AnimatorSet y su método playSequentially() hemos ejecutado dos animaciones de forma secuencial que llevan a cabo un fundido a negro y a blanco. También hemos desarrollado una animación desde un fichero XML mediante la orden AnimatorInflater.loadAnimator(this, R.anim.fundidos). El fichero fundidos.xml se ha descrito anteriormente. La clase PropertyValuesHolder mueve la etiqueta en el eje X e Y con el método ofFloat(). De forma similar, la clase ViewPropertyAnimator desplaza la etiqueta con los métodos x() e y(). En el botón séptimo hemos utilizado la subclase ArgbEvaluator de TypeEvaluator para cambiar los colores de fondo de la etiqueta. La clase KeyFrames desarrolla una animación de tipo fotograma mediante sus métodos ofFloat() de la propiedad alpha de la etiqueta. Finalmente, se crea un listener del tipo AnimationListener para detectar cuándo la animación se inicia, se para o se repite, y mostrar al usuario un mensaje de tipo Toast. El resto de métodos son intuitivos y se ha explicado en la parte teórica de este apartado, te sugerimos que le eches un vistazo detallado al código fuente completo.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 6 (API Animaciones) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la API de Animación de Android. 88 Si ejecutas en Eclipse DT este Ejemplo 6 en el AVD, verás que se muestra la siguiente aplicación:
U1 Multimedia y Gráficos en Android
Recomendamos al alumno que pruebe en Eclipse ADT el efecto de animación que se produce sobre la etiqueta superior cuando pulsa sobre los diferentes botones.
5.4 Interpolators (Interpoladores) La clase Interpolator permite controlar cómo se acelera o desacelera una animación. El listado completo de interpoladores es el siguiente: - AccelerateDecelerateInterpolator: la animación comienza y termina desacelerando el movimiento. La aceleración se produce cuando la animación se encuentra a la mitad. - AccelerateInterpolator: la animación acelera de forma continua hasta que termina. - AnticipateInterpolator: la animación comienza volviendo hacia atrás y termina con una aceleración de forma continua. - AnticipateOvershootInterpolator: la animación comienza volviendo hacia atrás y avanza más allá del punto final, terminando en este punto final. - BounceInterpolator: la animación acelera de forma continua hasta que llega al final y termina con unos botes. - CycleInterpolator: la animación se acelera en forma de ciclo a lo largo de los valores de cambio definidos. - DecelerateInterpolator: la animación desacelera de forma continua hasta que termina. - LinearInterpolator: la animación cambia de forma continua sin acelerar. - OvershootInterpolator: la animación comienza acelerando y avanza más allá del punto final, terminando en este punto final.
En esta Unidad puedes encontrar el vídeo “API de Animación - Interpoladores”, que muestra de manera visual cómo cambia una animación en función del interpolador aplicado. En el Ejemplo 7 de esta Unidad hemos desarrollado una sencilla aplicación que utiliza interpoladores. Si abres el archivo res/layout/main_layout.xml advertirás que hemos definido una imagen a la izquierda a la que animaremos mediante un conjunto de botones que llaman a un interpolador diferente. Si abres los archivos de la carpeta res/anim/ advertirás que hemos definido un conjunto de animaciones que trasladan la imagen de arriba abajo con los mimos parámetros mediante el atributo translate. Sin embargo, mediante el atributo android:interpolator hemos indicando que se aplique un interpolador distinto en cada una de ellas. Si accedes al fichero Java MainActivity.java que describe la lógica de la aplicación observarás que contiene las siguientes sentencias: public class MainActivity extends Activity
{
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout);
// Definimos las animaciones a partir de archivos XML. Estas // animaciones mueven siempre la imagen de arriba a abajo pero
89
Aula Mentor
//cambian el interpolador. final Animation animAccelerateDecelerate = AnimationUtils.loadAnimation(this, R.anim.accelerate_decelerate); final Animation animAccelerate = AnimationUtils.loadAnimation(this, R.anim.accelerate); final Animation animAnticipate = AnimationUtils.loadAnimation(this, R.anim.anticipate); final Animation animAnticipateOvershoot = AnimationUtils.loadAnimation(this, R.anim.anticipate_overshoot); final Animation animBounce = AnimationUtils.loadAnimation(this, R.anim.bounce); final Animation animCycle = AnimationUtils.loadAnimation(this, R.anim.cycle); final Animation animDecelerate = AnimationUtils.loadAnimation(this, R.anim.decelerate); final Animation animLinear = AnimationUtils.loadAnimation(this, R.anim.linear); final Animation animOvershoot = AnimationUtils.loadAnimation(this, R.anim.overshoot); // Buscamos la imagen que vamos a animar final ImageView imagen = (ImageView)findViewById(R.id.imagen); // Botones de la interfaz de usuario Button botonAccelerateDecelerate = (Button)findViewById(R.id.acceleratedecelerate); Button botonAccelerate = (Button)findViewById(R.id.accelerate); Button botonAnticipate = (Button)findViewById(R.id.anticipate); Button botonAnticipateOvershoot = (Button)findViewById(R.id.anticipateovershoot); Button botonBounce = (Button)findViewById(R.id.bounce); Button botonCycle = (Button)findViewById(R.id.cycle); Button botonDecelerate = (Button)findViewById(R.id.decelerate); Button botonLinear = (Button)findViewById(R.id.linear); Button botonOvershoot = (Button)findViewById(R.id.overshoot);
90
// Cada botón inicia la animación que corresponde teniendo en // cuenta el interpolador botonAccelerateDecelerate.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animAccelerateDecelerate); } }); botonAccelerate.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animAccelerate); } });
botonAnticipate.setOnClickListener(new Button.OnClickListener(){ @Override
U1 Multimedia y Gráficos en Android
public void onClick(View arg0) { imagen.startAnimation(animAnticipate); } }); botonAnticipateOvershoot.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animAnticipateOvershoot); } }); botonBounce.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animBounce); } }); botonCycle.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animCycle); } }); botonDecelerate.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animDecelerate); } }); botonLinear.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animLinear); } }); botonOvershoot.setOnClickListener(new Button.OnClickListener(){ @Override public void onClick(View arg0) { imagen.startAnimation(animOvershoot); } }); } // end onCreate } // end clase
Como puedes observar, el código anterior carga las animaciones a partir de archivos XML y asocia a cada botón la animación de la imagen que corresponde teniendo en cuenta el interpolador.
91
Aula Mentor
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 7 (API Interpoladores) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado distintos Interpolators de Android.
Si ejecutas en Eclipse ADT este Ejemplo 7 en el AVD, verás que se muestra la siguiente aplicación:
92
6. Vista de tipo Superficie (ViewSurface) El tipo de Vista Superficie (clase SurfaceView) es muy importante para dibujar gráficos en 2D y 3D mediante la librería OPENGL y, también, para desarrollar juegos, ya que permite al programador controlar completamente lo que muestra la interfaz de usuario e interaccionar con ella. No obstante, hay que tener en cuenta que requiere más esfuerzo programar interfaces desde cero utilizando este tipo de Vista.
U1 Multimedia y Gráficos en Android
6.1 Arquitectura de Gráficos en Android A continuación, mostramos la arquitectura de gráficos de Android exponiendo los pasos que el sistema operativo ejecuta cuando un usuario abre una aplicación:
93
- El usuario ejecuta una nueva aplicación que, a su vez, crea la Actividad principal. - El sistema operativo crea una nueva ventana para esa Actividad y la registra en el gestor de ventanas WindowManagerService. - Android crea una nueva superficie para la ventana y la devuelve a la Actividad. Para ello, usa el Compositor de pantalla (Screen Compositor) denominado SurfaceFlinger. - La aplicación dibuja jerárquicamente las Vistas de la Actividad (TextView, Button, ImageView, etcétera) en la superficie. - Finalmente, se componen todas las superficies visibles en la pantalla del dispositivo, es decir, la de la Actividad que aparece en la pantalla y la de la barra de notificación.
6.2 ¿Qué es la clase ViewSurface? ViewSurface (Vista de tipo Superficie) proporciona una superficie de dibujo dedicado que se incrusta dentro de una jerarquía de vistas. Es posible dibujar el contenido de esta superficie, su posición y su tamaño en la pantalla del dispositivo. Esencialmente, la clase ViewSurface es una Vista que contiene una Superficie de Android (Surface) que, a su vez, es un buffer en crudo de datos y que utiliza el Compositor de pantalla de Android, que es el encargado de dibujar toda la interfaz de usuario en Android.
Aula Mentor
Veamos algunas diferencias entre las clases View y SurfaceView: - Todas las Vistas (Views) de una aplicación se dibujan en el mismo hilo principal de la interfaz de usuario, que también se usa para gestionar la interacción con él. - Sin embargo, es posible dibujar la clase SurfaceView desde varios hilos diferentes al principal. - Un objeto Surfaceview no puede ser transparente; sin embargo, puede estar detrás de otros elementos en la jerarquía de vistas. - Si queremos utilizar animaciones, un objeto SurfaceView se ejecuta más rápido que una Vista (View); por lo tanto, es recomendable utilizar el primero. - Sin embargo, nada es gratis en esta vida: un objeto del tipo Surfaceview consume más recursos (CPU y batería) que View. En el Ejemplo 8 de este Unidad vamos a mostrar cómo desarrollar una sencilla aplicación en Android que utiliza la clase SurfaceView. En ella se presenta un juego sencillo de una pelota que rebota en toda la pantalla del dispositivo y, si el usuario pulsa sobre ella, cambia el sentido de su movimiento. Es recomendable abrir el Ejemplo 8 en Eclipse ADT para entender la explicación siguiente.
Fíjate que en el código fuente de esta aplicación no hemos incluido ningún archivo Layout que defina la interfaz de usuario. 94
La interfaz de usuario de la aplicación se compone de una Actividad que crea una superficie y la asigna a la interfaz de usuario. Veamos su contenido en el fichero MainActivity.java: public class MainActivity
extends Activity {
// Evento onCreate de la Actividad public void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); // La ventana de esta Actividad no tiene título requestWindowFeature(Window.FEATURE_NO_TITLE); //Ocultamos la barra de notificaciones getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // El contenido de esta Actividad es el objeto de la clase // SuperficieView setContentView(new SuperficieView(this)); } // end onCreate }
En el código anterior hemos utilizado la orden requestWindowFeature() para ocultar el título de la aplicación y ampliar el espacio que ocupa la superficie. También definimos el atributo FLAG_FULLSCREEN que muestra la pantalla completa (sin barra de notificación) con el método setFlags() de la ventana (Window). Así, el usuario disfruta de toda la pantalla del dispositivo y puede jugar sin distracciones. Aunque es posible definir la interfaz de usuario mezclando layout y superficie, en este ejemplo establecemos directamente el contenido de esta Actividad creando un objeto de la clase SuperficieView. Es decir, no definimos un layout.
U1 Multimedia y Gráficos en Android
A continuación, se muestra cómo se implementa la interfaz visual del usuario mediante esta superficie definida en el fichero SuperficieView.java: // Clase que se extiende de SurfaceView e implementa el método // Callback de SurfaceHolder public class SuperficieView extends SurfaceView implements SurfaceHolder.Callback { // Hilo que se encarga de actualizar la superficie private BucleThread bucleThread; // Constructor de la Superficie public SuperficieView(Context context) { super(context); // Creamos el hilo bucleThread = new BucleThread(this); // Obtenemos el Holder de la superficie y le asignamos los // eventos definidos en esta clase getHolder().addCallback(this); // Delegamos el evento onTouch al Hilo setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { if(bucleThread!=null) { return bucleThread.onTouch(event); } else return false; } }); // end onTouch } // end onCreate // Si la superficie cambia, entonces guardamos el tamaño de la // pantalla @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Indicamos el nuevo tamaño de la superficie al hilo bucleThread.setSurfaceSize(width, height); } // Cuando la superficie se ha creado, marcamos que se ejecuta el // hilo y lo iniciamos @Override public void surfaceCreated(SurfaceHolder holder) { bucleThread.setRunning(true); bucleThread.start(); } // Si la superficie se destruye, entonces paramos el hilo @Override public void surfaceDestroyed(SurfaceHolder holder) { boolean reintentar = true; bucleThread.setRunning(false); // Mientras el hilo esté activo lo intentamos parar while (reintentar) { try { // Unimos el Hilo de actualización con el Hilo // principal de la aplicación invocando la orden
95
Aula Mentor
}
}
// join hasta que lo conseguimos. Es necesario unir // todos los hilos para destruirlos correctamente. bucleThread.join(); reintentar = false; } catch (InterruptedException e) { }
} // end clase superficie
Veamos ahora cómo acceder a la superficie subyacente de toda la Actividad. Para utilizar una superficie debemos crear una clase que se extienda de SurfaceView e implemente la interfaz SurfaceHolder.Callback de Android para poder gestionar los métodos relacionados con esta superficie: - surfaceCreated(): ocurre cuando se crea la superficie. - surfaceDestroyed(): se lanza cuando la superficie ya no es necesaria y hay que liberarla de la memoria. - surfaceChanged(): ocurre cuando la superficie cambia, por ejemplo, su tamaño si el usuario cambia la posición de la pantalla. - onTouchEvent(): se lanza cuando el usuario toca la pantalla. - onDraw(): se lanza cuando es necesario dibujar la superficie.
96
En el constructor de la clase del código anterior puedes observar que se invoca al método getHolder().addCallback(this) para gestionar los eventos del SurfaceView y poder acceder a la superficie. En este caso hemos implementado algunos de los eventos anteriores. Veamos las sentencias más novedosas de éstos. En el método surfaceCreated() del SurfaceView ejecutamos un hilo que hemos creado en el constructor de esta clase y al que hemos pasado como parámetro la referencia del surfaceholder. Después, establecemos el estado de ejecución del hilo y lo arrancamos con la orden start(). La función de este hilo es actualizar la interfaz de usuario dibujándola cuando lo consideremos necesario. El método SurfaceDestroyed() se lanza cada vez que la aplicación pasa a segundo plano y el SurfaceView se va a destruir junto con todo su contenido. Por lo tanto, es necesario parar el hilo. Para ello, unimos el Hilo de actualización con el Hilo principal de la aplicación invocando la orden join hasta que lo conseguimos. Es necesario unir todos los hilos para destruirlos correctamente. Es imprescindible que la interacción del usuario con la aplicación sea suave y los gráficos se muestren sin parpadeos. Para conseguirlo, tenemos que ejecutar dos hilos en la aplicación: el hilo principal, que se encarga de gestionar la Actividad de la aplicación, y el segundo hilo, que dibuja la superficie y gestiona la interacción del usuario con ésta. La razón principal de esta división de tareas está en que, como estudiamos en el curso de Iniciación a Android, no debemos bloquear nunca el hilo principal de una aplicación. Por ejemplo, si el usuario presiona la pantalla táctil puede crear paradas en la ejecución del código.
Es muy importante separar siempre el código en dos o más hilos: el hilo principal de la aplicación y un segundo o tercer hilo que se encargan de dibujar las superficies, para que el usuario obtenga una sensación más fluida del movimiento e interacción con la superficie.
U1 Multimedia y Gráficos en Android
Hemos implementado el Hilo BucleThread para actualizar el dibujo del objeto SurfaceView y gestionar los toques sobre éste. En el constructor de este hilo le pasamos como parámetro la referencia (this) al objeto SuperficieView para poder modificar el contenido de ésta. Por esto, en el método surfaceChanged() del SuperficieView obtenemos el tamaño de la pantalla del dispositivo Android y se lo pasamos al hilo BucleThread. Además, en el constructor de esta clase, hemos delegado el método onTouchEvent() en este Hilo mediante la sentencia setOnTouchListener(). Finalmente, no hemos definimos el método onDraw() de la superficie ya que será el hilo quien se encargue de dibujarla. A continuación, se muestra cómo se implementa este hilo en el archivo BucleThread. java: // Clase que se extiende de la clase Thread (Hilo) public class BucleThread extends Thread { // Número de actualizaciones por segundo que hace el hilo static final long FPS = 10; // Variable donde guardamos la superficie private SuperficieView superfView; // Variables que almacenan el ancho y largo de la pantalla private int width, height; // Sirve para saber si se está ejecutando el hilo private boolean running = false; // Posición actual de la pelota private int pos_x = -1; private int pos_y = -1; // Velocidad del movimiento private int xVelocidad = 10; private int yVelocidad = 5; // Variables que indican las coordenadas donde el usuario tocó la // pantalla public int touched_x, touched_y; // Indica si se está tocando la pantalla o no public boolean touched; // Bitmap donde cargamos la imagen de la pelota private BitmapDrawable pelota; // Constructor que guarda la superficie public BucleThread(SuperficieView view) { this.superfView = view; // Buscamos la imagen pelota pelota = (BitmapDrawable) view.getContext(). getResources().getDrawable(R.drawable.pelota); } // Método para establecer la variable running public void setRunning(boolean run) { running = run; } // Método típico de un hilo @Override public void run() { // Nº de actualizaciones que debemos hacer cada segundo long ticksPS = 1000 / FPS;
97
Aula Mentor
98
// Variables temporales para controlar el tiempo long startTime; long sleepTime; // Mientras estemos ejecutando el hilo while (running) { Canvas canvas = null; // Obtenemos el tiempo actual startTime = System.currentTimeMillis(); try { // Bloqueamos el canvas de la superficie para dibujarlo canvas = superfView.getHolder().lockCanvas(); // Sincronizamos el método draw() de la superficie para // que se ejecute como un bloque synchronized (superfView.getHolder()) { if (canvas!=null) doDraw(canvas); } } finally { // Liberamos el canvas de la superficie desbloqueándolo if (canvas != null) { superfView.getHolder().unlockCanvasAndPost(canvas); } } // Tiempo que debemos parar la ejecución del hilo sleepTime = ticksPS - System.currentTimeMillis()-startTime; // Paramos la ejecución del hilo try { if (sleepTime > 0) sleep(sleepTime); else sleep(10); } catch (Exception e) { } } // end while } // end run() // Evento que se lanza cada vez que es necesario dibujar la // superficie protected void doDraw(Canvas canvas) { // Primera posición de la pelota en el centro if (pos_x this.height - pelota.getBitmap().getHeight()) || (pos_y < 0)) { yVelocidad = yVelocidad*-1; } } // Color gris para el fondo de la aplicación canvas.drawColor(Color.LTGRAY); // Dibujamos la pelota en la nueva posición canvas.drawBitmap(pelota.getBitmap(), pos_x, pos_y, null); } // Evento que se lanza cuando el usuario hace clic sobre la // superficie public boolean onTouch(MotionEvent event) { // Obtenemos la posición del toque touched_x = (int) event.getX(); touched_y = (int) event.getY(); // Obtenemos el tipo de Accion int action = event.getAction(); Log.e(“Toque (X,Y)”, “(“ + touched_x + “,” + touched_y + “)”);
switch (action) { // Cuando se toca la pantalla case MotionEvent.ACTION_DOWN: Log.e(“TouchEven ACTION_DOWN”, “Usuario toca la pantalla “); touched = true; break; // Cuando se desplaza el dedo por la pantalla case MotionEvent.ACTION_MOVE: touched = true; Log.e(“TouchEven ACTION_MOVE”, “Usuario desplaza dedo por la pantalla “); break; // Cuando levantamos el dedo de la pantalla que estábamos // tocando case MotionEvent.ACTION_UP: touched = false; Log.e(“TouchEven ACTION_UP”, “Ya no tocamos la pantalla”); break; // Cuando se cancela el toque. Es similar a ACTION_UP
99
Aula Mentor
case MotionEvent.ACTION_CANCEL: touched = false; Log.e(“TouchEven ACTION_CANCEL”, “ “); break; // El usuario ha tocado fuera del área de la interfaz del // usuario case MotionEvent.ACTION_OUTSIDE: Log.e(“TouchEven ACTION_OUTSIDE”, “ “); touched = false; break; default: } return true; } // end onTouch // Se usa para establecer el nuevo tamaño de la superficie public void setSurfaceSize(int width, int height) { // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (superfView) { // Guardamos el nuevo tamaño this.width = width; this.height = height; } }
100
} // end clase
El código anterior es un hilo típico de Java con su correspondiente método run(). En éste puedes ver que bloqueamos el canvas de la superficie para dibujarlo con la orden superfView.getHolder().lockCanvas() y, cuando es necesario, sincronizamos el método local doDraw(), que dibuja la pelota y actualiza la interfaz del usuario. Una vez hemos ejecutado este bloque de código, liberamos el canvas de la superficie desbloqueándolo con la orden unlockCanvasAndPost(canvas). Para simular el movimiento de la imagen hemos creado un temporizador mediante la sentencia sleep(), que para la ejecución de este hilo durante el tiempo establecido en la contante FPS (Frames Per Second = Fotogramas por segundos). El método doDraw() especifica lo que se debe dibujar en la pantalla del dispositivo utilizando la clase Canvas (lienzo o zona de dibujo que ya hemos estudiado) y la orden drawBitmap() dibujamos una pelota a partir de una imagen almacenada como recurso en la aplicación, teniendo en cuenta las dimensiones de la pantalla y hacia dónde se debe mover. Además, también se tiene en cuenta si el usuario ha hecho clic cerca de la pelota y, en ese caso, se cambia el sentido del movimiento de esta pelota. El método onTouch() sirve para interactuar con la pulsación del usuario en la pantalla. Este método tiene como parámetro la clase MotionEvent que permite conocer la acción del usuario mediante la orden getAction(). Así, toma los siguientes valores en función del comportamiento del usuario: - MotionEvent.ACTION_DOWN: cuando toca la pantalla. - MotionEvent.ACTION_MOVE: cuando desplaza el dedo por la pantalla. - MotionEvent.ACTION_UP: cuando levanta el dedo de la pantalla que estaba tocando. - MotionEvent.ACTION_CANCEL: cuando se cancela el toque. Es similar a ACTION_UP. - MotionEvent.ACTION_OUTSIDE: cuando ha tocado fuera del área de la interfaz del usuario.
U1 Multimedia y Gráficos en Android
En la teoría de la Unidad 5 puedes encontrar una descripción detallada de la gestión de pulsaciones del usuario sobre la pantalla.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 8 (Vista de Superficie) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado un elemento SurfaceView.
Si ejecutas en Eclipse ADT este Ejemplo 8 en el AVD, verás que se muestra la siguiente aplicación:
101
Puedes probar a hacer clic sobre la bola para comprobar que su movimiento cambia de sentido. Si instalas este sencillo juego en un dispositivo real verás que es más sencillo probar la aplicación.
7. Gráficos en 3D en Android Android dispone de dos bibliotecas para pintar gráficos en 3D: - OpenGL: biblioteca estándar de diseño 3D. - RenderScript: nueva biblioteca de bajo nivel de dibujo gráfico en 3D disponible desde la versión 3.0 de Android que aprovecha el procesador GPU de la tarjeta gráfica para pintar dejando así al procesador principal del dispositivo capacidad disponible para otras tareas. Estas gráficas se desarrollan con un lenguaje específico llamado C99.
Aula Mentor
7.1 OpenGL En este apartado vamos a aprender a aplicar los conceptos básicos de la biblioteca gráfica más utilizada y difundida: OpenGL. La biblioteca OpenGL (Open Graphics Library) es una especificación estándar de una API multilenguaje y multiplataforma que permite escribir aplicaciones que pinten gráficos en 2D y en 3D. Contiene más de 250 funciones diferentes que pueden usarse para dibujar gráficos tridimensionales complejos a partir de primitivas geométricas simples, tales como puntos, líneas y triángulos. Android soporta las siguientes versiones: - OpenGL 2.0 ES: a partir de la versión 2.2 de Android. - OpenGL 1.1 ES: en todas las versiones de Android. Empezaremos diseñando gráficos sencillos en Android para acabar aumentando la complejidad y llegar a dibujar gráficos en 3D con texturas.
102
Las clases básicas de esta biblioteca son las siguientes: - GLSurfaceView: clase base que permite escribir las aplicaciones que usen OpenGL. Esta clase se extiende de SurfaceView que hemos estudiado en el apartado anterior. Así, según lo estudiado en el apartado anterior, podríamos utilizar un objeto GLSurfaceView como parámetro del método setContentView() para dibujar la interfaz de la Actividad. - GLSurfaceView.Renderer: interfaz (interface) de dibujo (renderizado) genérica donde se detalla el código que indica lo que se debe dibujar. Como ya sabes, al implementar una interfaz, es necesario implementar todos sus métodos, que en este caso son los siguientes: • onSurfaceCreated(): método invocado cuando se crea o se recrea la superficie (surface). Como este método se invoca cuando comienza el renderizado, es un buen sitio para definir lo que no cambiará durante el ciclo del renderizado, por ejemplo, el color del fondo, etcétera. • onDrawFrame(): método que se invoca cada vez que es necesario dibujar sobre la superficie. • onSurfaceChanged(): método llamado cuando la superficie cambia de alguna manera, por ejemplo, al girar el móvil.
Para entender de forma gráfica estas dos clases anteriores, podemos hacer una analogía con un pintor y su lienzo. En este caso, el pintor es la interfaz GLSurfaceView.Renderer y el lienzo es GLSurfaceView.
Por compatibilidad entre las distintas versiones de Android vamos a utilizar la versión 1.0 de la biblioteca OpenGL para desarrollar los ejemplos del curso.
7.1.1 Conceptos básicos de geometría Antes de empezar a dibujar gráficos es importante recordar los siguientes conceptos básicos
U1 Multimedia y Gráficos en Android
de geometría: - Línea o Recta: conjunto de puntos que se extienden con determinada longitud. - Vértice: punto donde dos o más rectas se encuentran. Veamos unos ejemplos sobre cómo utilizar vértices para definir un punto o polígonos en Android: • Punto // Punto ubicado en la coordenada (1,1,0) float vertice[] = { 1f ,1f ,0f };
• Cuadrado
float vertices[] = { -1f, 1f, 0f, // -1f, -1f, 0f, // 1f, -1f, 0f, // 1f, 1f, 0f // };
vértice vértice vértice vértice
ubicado ubicado ubicado ubicado
en en en en
(-1,1,0) (-1,-1,0) (1,-1,0) (1,1,0)
vértice vértice vértice vértice
ubicado ubicado ubicado ubicado
en en en en
(-1,-1,0) (1,-1,0) (0,0.8,0) (0, 0, 2)
• Pirámide de base triangular
float vertices[] = { -1f, -1f, 0f, // 1f, -1f, 0f, // 0f, 0.8f, 0f, // 0f, 0f, 2f // };
Como puede observar, para definir vértices, simplemente debemos indicar las coordenadas (x,y,z) en una matriz de tipo float. - Cara: cada lado que forma un polígono. La modificación de una cara afecta a sus vértices y a sus aristas. - Arista o Borde: unión de dos vértices. En un modelo 3D, la arista puede estar compartida por dos caras o polígonos. - Polígono: figura cerrada que está formada por tres o más caras que se unen en sus aristas. - Eje de coordenadas: tipo de coordenadas ortogonales usadas en espacios euclidianos caracterizadas por que usa como referencia ejes ortogonales entre sí que se cortan en un punto origen. En el espacio 3D estos ejes tienen el aspecto que se refleja en el gráfico inferior. En el dispositivo Android, X e Y se encuentran en los ejes planos horizontal y vertical. El eje Z es un eje abstracto perpendicular a su pantalla, que en gráficos 3D se usa para crear la sensación de lejanía o cercanía del gráfico respecto al observador. Cuanto más lejos esté un objeto respecto al observador, menor será su valor en el eje Z.
103
Aula Mentor
7.1.2 Conceptos básicos de OpenGL - Framebuffer: parte de la memoria donde se dibuja una escena. El tamaño en memoria
viene dado por la siguiente fórmula: Tamaño total en memoria = Ancho pantalla x Alto pantalla x Número de canales x bytes por muestra. - Primitivas de OpenGL: funciones que permiten dibujar puntos, líneas, triángulos, etcétera. En general, es recomendable utilizar triángulos para dibujar figuras complejas, ya que esta biblioteca es más eficiente así y el gráfico aparece en menos tiempo. - Estados de OpenGL: OpenGL guarda información sobre diferentes flags que determinan qué factores y qué valores afectarán a la hora de dibujar. Cada vez que cambiamos el estado mediante la llamada a una función de la API debemos tener en cuenta que el estado se quedará así hasta que lo volvamos a cambiar de nuevo. Por ejemplo, la función glColor cambia el color del pincel de todas las funciones primitivas que utilicemos desde ese momento en adelante. Podemos usar glEnable y glDisable para habilitar y deshabilitar propiedades de OpenGL según las necesidades. - Buffer de profundidad o Z-Buffer: normalmente, en gráficos en 3D interesa que OpenGL tenga en cuenta qué objetos están delante o detrás de otros (eje Z de perspectiva) a la hora de pintarlos. El buffer de profundidad (del inglés, DepthBuffer) o Z-Buffer almacena el valor de profundidad de cada píxel de la imagen en un buffer separado que se usa para descartar píxeles que quedan detrás de objetos ya existentes en la imagen. - Sistema de coordenadas: en la biblioteca OpenGL utilizaremos el sistema de coordenadas ortogonal que hemos visto anteriormente al que añadiremos otra coordenada nueva que indica la perspectiva (desde donde miramos el objeto). Así, el sistema de coordenadas inicial de OpenGL puede representarse con esta matriz: 104
Transformaciones: funciones que cambian la posición del objeto relativa al centro de la figura y pueden trasladarla, escalarla y rotarla. Matemáticamente, estas transformaciones multiplican la matriz anterior de coordenadas por otra matriz que es la que genera la transformación. Veamos unos ejemplos gráficos: Esquema
Descripción La orden glTranslatef(0.1, 0.1, 0) mueve el objeto en el eje X,Y de coordenadas.
U1 Multimedia y Gráficos en Android
Esquema
Descripción La orden glRotatef(-45, 0, 0, 1) rota el objeto. Los ángulos positivos rotan al contrario que el movimiento de las agujas del reloj. La orden glScalef(2, 2, aumenta el tamaño del objeto.
1)
Por ejemplo, al trasladar un objeto dos unidades en el eje X, se genera la siguiente matriz de transformación:
Si aplicamos esta transformación a la matriz anterior, nos quedará que la nueva matriz de transformación es:
Si ahora dibujamos el punto (1, 0, 0) teniendo en cuenta que la matriz de transformación indica un desplazamiento de dos unidades en eje X, el punto deberá dibujarse en la posición (3,0,0). Para esto, se multiplica el punto por la matriz de transformación:
- Matriz de visualización/modelado: matriz que guarda la transformación de los objetos una vez aplicada la transformación, es decir, es la última matriz anterior. - Matriz de proyección: matriz que guarda la información relativa a la “perspectiva” a través de la cual el usuario visualiza la escena. Al realizar operaciones que modifiquen alguna de estas dos últimas matrices, tendremos que
105
Aula Mentor
cambiar al “modo de matriz” correspondiente, para que las operaciones afecten a esa matriz específicamente. - Modo de dibujo: OpenGL dispone de distintas formas de pintar: • GL_POINTS: dibuja únicamente los vértices. • GL_LINES: dibuja las líneas entre dos vértices independientemente de otros vértices. • GL_LINE_STRIP: dibuja las líneas conectadas entre sí por un vértice. • GL_LINE_LOOP: pinta de la misma forma que el anterior método, pero une el punto final con el punto inicial. • GL_POLYGON: dibuja una superficie de tipo polígono. • GL_TRIANGLES: dibuja superficies mediante triángulos usando tres puntos de referencia. • GL_TRIANGLE_STRIP: pinta superficies mediante triángulos continuos usando tres puntos de referencia • GL_TRIANGULE_FAN: pinta superficies mediante triángulos usando un punto en común, a modo de abanico. • GL_SQUADS: dibuja cuadriláteros de cuatro vértices. • GL_QUAD_STRIP: dibuja cuadriláteros continuos de cuatro vértices. Como una imagen vale más que mil palabras, veamos esquemáticamente estos mecanismos:
106
U1 Multimedia y Gráficos en Android
OpenGL es una biblioteca muy compleja y requiere conocimientos previos por parte del alumno o alumna para utilizarla con soltura. Existen multitud de cursos específicos donde puede adquirirlos. De todas formas, se explicarán las funciones básicas de esta biblioteca, si bien no es objeto de este apartado estudiar la biblioteca OpenGL en sí misma.
Una vez explicados los conceptos básicos de OpenGL, vamos a desarrollarlos mediante ejemplos prácticos, pues es más sencillo y pedagógico entender en la práctica cómo se utiliza esta biblioteca.
7.2 Gráficos en 2D En el Ejemplo 9 de esta Unidad hemos dibujado los polígonos triángulo y cuadrado en 2D. Es recomendable abrir el Ejemplo 9 en Eclipse ADT para entender la explicación siguiente. Si abres el archivo res/layout/main_layout.xml, advertirás que hemos definido una barra de botones que permitirán al usuario elegir el polígono que desea visualizar:
107
Aula Mentor
En el diseño de la interfaz de usuario anterior hemos incluido la etiqueta FrameLayout, que sirve de contenedor de otras vistas, como los gráficos que incluiremos dentro de éste. Si accedes al fichero Java MainActivity.java que describe la lógica de la aplicación, observarás que contiene las siguientes sentencias: public class MainActivity extends Activity { // Superficie de OpenGL private GLSurfaceView glSurface; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout);
108
// Creamos la instancia de la superficie de OpenGL glSurface = new GLSurfaceView(this); // Definimos el Renderer de la superficie indicando que se // dibuje un triángulo sin color glSurface.setRenderer(new MiRenderer(MiRenderer.MOSTRAR_TRIANGULO, false)); // Buscamos el FrameLayout para añadirle la superficie anterior final FrameLayout frame = (FrameLayout)findViewById(R.id.frame); frame.addView(glSurface, 0); // Buscamos el resto de Vistas de la Actividad final CheckBox conColor = (CheckBox) findViewById(R.id.cbColorear); final Button boton_triang = (Button) findViewById(R.id.triangulo); // Creamos el evento onClick de los botones boton_triang.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Recreamos la superficie con el polígono triángulo e // indicamos si es necesario añadir el color glSurface = new GLSurfaceView(MainActivity.this); glSurface.setRenderer(new MiRenderer(MiRenderer.MOSTRAR_TRIANGULO, conColor.isChecked())); // Quitamos las Vistas del Frame y añadimos la nueva // superficie frame.removeAllViews(); frame.addView(glSurface, 0); } });
final Button boton_cuadrado = (Button)
U1 Multimedia y Gráficos en Android
findViewById(R.id.cuadrado); boton_cuadrado.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Recreamos la superficie con el polígono cuadrado e // indicamos si es necesario añadir el color glSurface = new GLSurfaceView(MainActivity.this); glSurface.setRenderer(new MiRenderer(MiRenderer.MOSTRAR_CUADRADO, conColor.isChecked())); // Quitamos las Vistas del Frame y añadimos la nueva // superficie frame.removeAllViews(); frame.addView(glSurface, 0); } });
} // end onCreate // Cuando la Actividad vuelve a primer plano, indicamos a la // superficie que se actualice de nuevo @Override protected void onResume() { super.onResume(); glSurface.onResume(); } // Cuando la Actividad pasa a segundo plano, indicamos a la // superficie que NO se actualice @Override protected void onPause() { super.onPause(); glSurface.onPause(); } } // end clase
Puedes observar en el código anterior que, para crear los gráficos, hemos utilizado la clase superficie GLSurfaceView de OpenGL a la que asociamos con el método setRenderer() el Renderer que se encargará de dibujar la imagen en función de los parámetros pasados en el constructor de este método. Además, hemos utilizado la Layout del tipo FrameLayout para mostrar en la interfaz del usuario la superficie creada anteriormente. Hemos detallado el Renderer anterior en la clase MiRenderer del proyecto, que contiene el siguiente código fuente: public class MiRenderer implements Renderer { // Constantes que indican el tipo de polígono que debemos dibujar public static int MOSTRAR_TRIANGULO=1; public static int MOSTRAR_CUADRADO=2; // Guarda el polígono que estamos mostrando private int mostrar_poligono=-1; // Objeto del tipo Triángulo que describe su geometría private Triangulo triangulo; // Objeto del tipo Cuadrado que describe su geometría
109
Aula Mentor
private Cuadrado cuadrado; // Constructor de la clase public MiRenderer(int mostrar_poligono, boolean ver_colores) { // Guardamos el tipo de polígono que queremos mostrar y // creamos la geometría correspondiente this.mostrar_poligono=mostrar_poligono; if (mostrar_poligono == MOSTRAR_TRIANGULO) triangulo = new Triangulo(ver_colores); else cuadrado = new Cuadrado(ver_colores); }
110
// Evento que se lanza cuando se crea la superficie. // Aquí debemos indicar todo aquello que no cambia en la superficie public void onSurfaceCreated(GL10 gl, EGLConfig config) { // Indicamos el modo de sombreado suave gl.glShadeModel(GL10.GL_SMOOTH); // Establecemos el color GRIS como fondo de la superficie gl.glClearColor(0.6f, 0.6f, 0.6f, 0.5f); // Configura el buffer de profundidad gl.glClearDepthf(1.0f); // Modo de renderizado de la profundidad gl.glEnable(GL10.GL_DEPTH_TEST); // Función de comparación de la profundidad gl.glDepthFunc(GL10.GL_LEQUAL); // Cómo se calcula la perspectiva gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); } // Evento que se lanza cada vez que es necesario dibujar la // superficie public void onDrawFrame(GL10 gl) { // Cada vez que se dibuja una escena hay que limpiar // tanto el buffer de color como el de profundidad gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // Reinicia la matriz de proyección del gráfico gl.glLoadIdentity(); // Movemos la posición de los ejes en (x,y,z). // Es decir, alejamos el dibujo en el eje Z dando // sensación de profundidad. gl.glTranslatef(0.0f, 0.0f, -6.0f); // Dibujamos el polígono solicitado if (mostrar_poligono == MOSTRAR_TRIANGULO) triangulo.draw(gl); else cuadrado.draw(gl); } // end onDrawFrame // Evento que se lanza cuando cambia la superficie public void onSurfaceChanged(GL10 gl, int width, int height) { // Nos aseguramos de que no se puede dividir por 0 if(height == 0) { height = 1; } // Reiniciamos la zona de visión
U1 Multimedia y Gráficos en Android
gl.glViewport(0, 0, width, height); // Establecemos la matriz de proyección como // activa para modificarla y poder girar el polígono gl.glMatrixMode(GL10.GL_PROJECTION); // Reinicia la matriz de proyección del gráfico gl.glLoadIdentity(); // Calculamos el tamaño de la perspectiva en función del nuevo // tamaño de la pantalla GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f); // Establecemos de nuevo la matriz de vista/modelo // como activa gl.glMatrixMode(GL10.GL_MODELVIEW); // Reiniciamos la matriz de proyección del gráfico gl.glLoadIdentity(); } } // end clase
En el código anterior podemos ver que en el constructor guardamos el tipo de polígono que queremos mostrar y creamos la geometría correspondiente. Después, en el método onSurfaceCreated()establecemos los valores cuya variación será poca o nula durante la ejecución del programa. En este caso, fijamos: - Sombreado suave en el dibujo (renderizado) mediante el método glShadeModel(). - Color del fondo gris mediante glClearColor(). - Configurar el tamaño del buffer de profundidad mediante glClearDepth() cuando se vacía. El buffer de profundidad o z-buffer se utiliza para gestionar la visibilidad de varios gráficos 3D superpuestos según las coordenadas de sus pixeles. - Indicamos el modo de renderizado mediante glEnable(GL10.GL_DEPTH_TEST), que también se conoce como algoritmo de z-Buffer porque gestiona qué elementos de una escena son visibles y cuáles permanecerán ocultos según sus posiciones en el eje Z (distancia a cámara). En definitiva, lo que se hace es comparar las profundidades de todos los gráficos para representar el objeto más cercano o lejano al observador según el parámetro que indiquemos. Es decir, con el modo GL_DEPTH_TEST, “los objetos cercanos ocultan a los más lejanos”. - Especificamos la función que se usará para comparar la profundidad de los objetos mediante el método glDepthFunc(). El parámetro GL_LEQUAL indica que el valor de profundidad se almacena si la cercanía al observador es menor o igual al valor existente. - Con el método glHint() establecemos, con la propiedad GL_PERSPECTIVE_CORRECTION_ HINT, la calidad del color y la interpolación de las coordenadas de la textura al valor GL_NICEST, que es la máxima calidad posible. En el método onDrawFrame() insertamos las sentencias que deben ejecutarse durante el dibujo: - Cada vez que se dibuja una escena hay que limpiar tanto el buffer de color como el de profundidad mediante el método glClear(). - Se reinicia la matriz de proyección del gráfico con el método glLoadIdentity(). Como hemos visto, la matriz de proyección se utiliza para poder realizar cambios de perspectiva (giros, translaciones, etcétera). Realmente, lo que estamos haciendo es borrar la matriz de proyección dejándola a “0”, es decir, no hay transformación ninguna. - Movemos la posición de los ejes en (x,y,z) con el método glTranslatef(). Es decir, alejamos el dibujo en el eje Z dando sensación de profundidad al observador. - Invocamos el método draw del objeto que debemos dibujar y que hemos instanciado anteriormente.
111
Aula Mentor
Finalmente, en el método onSurfaceChanged() colocamos las sentencias que se ejecutarán cuando la superficie sobre la que dibujamos sufra alguna modificación como la orientación de posición de la pantalla. En este caso: - Utilizamos glViewPort() para redefinir el ancho y largo de la vista de dibujo (viewport) teniendo en cuenta el nuevo tamaño de la pantalla. - Usamos el método glMatrixMode() para especificar a qué matriz se le van a aplicar los cambios posteriores. En este caso, seleccionamos la matriz de proyección (GL_PROJECTION). - Se reinicia la matriz de proyección del gráfico con el método glLoadIdentity(). - El método gluPerspective() establece el tamaño de la perspectiva en función del nuevo tamaño de la pantalla. Así, conseguimos que el polígono entre dentro de la nueva pantalla. - Usamos de nuevo el método glMatrixMode() para especificar a qué matriz se le aplican los cambios posteriores. En este caso, seleccionamos la matriz de vista/modelo (GL_MODELVIEW). Esta matriz de vista/modelo (modelview) se usa para mapear las coordenadas del polígono utilizando el sistema de coordenadas de OpenGL. - Se reinicia la matriz de proyección del gráfico con el método glLoadIdentity(). Veamos ahora cómo hemos definido las clases que describen la geometría de los polígonos. Empezamos por el triángulo desarrollado en el archivo Triangulo.java:
112
public class Triangulo { // Indica si deseamos colorear el triángulo private boolean conColor; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los vértices del triángulo private FloatBuffer bufferVertices; // Vértices del triángulo private float vertices[] = { 0.0f, 1.0f, 0.0f, // Arriba -1.0f, -1.0f, 0.0f, // Abajo izquierda 1.0f, -1.0f, 0.0f // Abajo derecha }; // Constructor del triángulo public Triangulo(boolean conColor) { // Definimos el buffer con los vértices del polígono. // Un número float tiene 4 bytes de longitud, así que // multiplicaremos x 4 el número de vértices. ByteBuffer byteBuf=ByteBuffer.allocateDirect(vertices.length*4); // Establecemos el orden de los bytes en el buffer con el valor // nativo (es algo así como indicar cómo se leen los bytes de // izq a dcha o al revés). byteBuf.order(ByteOrder.nativeOrder()); // Asignamos el nuevo buffer al buffer de esta clase bufferVertices = byteBuf.asFloatBuffer(); // Introducimos los vértices en el buffer bufferVertices.put(vertices); // Movemos la posición del buffer al inicio bufferVertices.position(0); // Guardamos si es necesario colorear el polígono this.conColor=conColor; } // Método que invoca el Renderer cuando debe dibujar el triángulo
U1 Multimedia y Gráficos en Android
public void draw(GL10 gl) { // Dibujamos al revés que las agujas del reloj gl.glFrontFace(GL10.GL_CCW); // Indicamos el nº de coordenadas (3), el tipo de datos de la // matriz (float), la separación en la matriz de los vértices // (0) y el buffer con los vértices gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices);
// Indicamos al motor OpenGL que le hemos pasado una matriz de // vértices gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// Dibujamos la superficie mediante la matriz en el modo // triángulo gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
// Si hemos indicado que queremos color... if (conColor) // Establecemos el color del triángulo en modo RGBA gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f); // Desactivamos el buffer de los vértices gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); } } // end clase
Podemos ver en el código anterior que hemos definido los vértices del triángulo en el constructor utilizando un buffer de tipo float, ya que la biblioteca OpenGL necesita disponer, en este tipo de variable, de los vértices del polígono. Después, en el método draw(), que invoca el Renderer cuando debe dibujar el triángulo, realizamos los siguientes trabajos: - Indicamos el sentido del dibujo al contrario que las agujas del reloj mediante el método glFrontFace(GL10.GL_CCW). Este sentido de dibujo es importante ya que determina la dirección del vector normal de iluminación; si éste apunta a la dirección equivocada, no veremos nada si utilizamos iluminación. El orden contrario a las agujas del reloj indica que la superficie del polígono está dirigida hacia el observador (regla de la mano derecha). Fíjate en este esquema:
- Con el método glVertexPointer() indicamos el número de coordenadas (3), el tipo de datos de la matriz (float), la separación (0) en la matriz de los vértices (podría haber más información en esta matriz además de los vértices) y el buffer con los vértices. - Indicamos al motor OpenGL que le hemos pasado una matriz de vértices mediante el método glEnableClientState(). - Usamos el método glDrawArrays()para dibujar la superficie del polígono utilizando la matriz de vértices y en el modo de dibujo de triángulos continuos (GL_TRIANGLE_STRIP).
113
Aula Mentor
- Si hemos indicado que debemos colorear el polígono, establecemos el color del triángulo en modo RGBA mediante glColor4f(). - Finalmente, se desactiva el buffer de los vértices con glDisableClientState(). Existen dos métodos para dibujar un gráfico mediante sus vértices: - public abstract void glDrawArrays(int mode, int first, int count): dibuja el
gráfico según el orden definido en la construcción de los vértices. Ya hemos utilizado este método en el código anterior.
- public abstract void glDrawElements(int mode, int count, int type, Buffer indices): el segundo método es similar al anterior, si bien debemos indicar el orden en el
que se pintarán los vértices.
En ambos métodos debemos indicar el modo de dibujo empleado por OpenGL para pintar el gráfico. Veamos ahora el cuadrado especificado en el archivo Cuadrado.java: public class Cuadrado {
114
// Indica si debemos dibujar el cuadrado con color private boolean conColor; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los vértices del cuadrado private FloatBuffer bufferVertices; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los colores del cuadrado private FloatBuffer bufferColores; // Vértices del cuadrado private float vertices[] = { -1.0f, -1.0f, 0.0f, // Abajo izq 1.0f, -1.0f, 0.0f, // Abajo dcha -1.0f, 1.0f, 0.0f, // Arriba izq 1.0f, 1.0f, 0.0f // Arriba dcha }; // Matriz con los colores rojo, verde y azul (RGBA) private float colores[] = { 1.0f, 0.0f, 0.0f, 1.0f, // Color Rojo con 100% de luminosidad 0.0f, 1.0f, 0.0f, 1.0f, // Color Verde con 100% de luminosidad 0.0f, 0.0f, 1.0f, 1.0f // Color Azul con 100% de luminosidad }; // Constructor del cuadrado public Cuadrado(boolean conColor) { // Definimos el buffer con los vértices del polígono. // Un número float tiene 4 bytes de longitud, así que // multiplicaremos x 4 el número de vértices. ByteBuffer byteBuf=ByteBuffer.allocateDirect(vertices.length*4); // Establecemos el orden de los bytes en el buffer con el valor // nativo (es algo así como indicar cómo se leen los bytes de // izq a dcha o al revés). byteBuf.order(ByteOrder.nativeOrder()); // Asignamos el nuevo buffer al buffer de esta clase bufferVertices = byteBuf.asFloatBuffer(); // Introducimos los vértices en el buffer
U1 Multimedia y Gráficos en Android
bufferVertices.put(vertices); // Movemos la posición del buffer al inicio bufferVertices.position(0); // Guardamos si es necesario colorear el polígono this.conColor=conColor; // Si es necesario colorear el cuadrado... if (conColor) { // Definimos el buffer de la matriz de colores de igual // forma que hemos hecho con la matriz de vértices byteBuf = ByteBuffer.allocateDirect(colores.length * 4); byteBuf.order(ByteOrder.nativeOrder()); bufferColores = byteBuf.asFloatBuffer(); bufferColores.put(colores); bufferColores.position(0); } } // end constructor // Método que invoca el Renderer cuando debe dibujar el cuadrado public void draw(GL10 gl) { // Dibujamos al revés que las agujas del reloj gl.glFrontFace(GL10.GL_CCW);
// Indicamos el nº de coordenadas (3), el tipo de datos de la // matriz (float), la separación en la matriz de los vértices // (0) y el buffer con los vértices gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices);
// Indicamos al motor OpenGL que le hemos pasado una matriz de // vértices gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // Buffer de colores if (conColor) { // Indicamos el nº de campos que definen el color (4), el // tipo de datos de la matriz (float), la separación en la // matriz de los colores (0) y el buffer con los colores. gl.glColorPointer(4, GL10.GL_FLOAT, 0, bufferColores); // Indicamos al motor OpenGL que le hemos pasado una matriz // de colores gl.glEnableClientState(GL10.GL_COLOR_ARRAY); }
// Dibujamos la superficie mediante la matriz en el modo // triángulo gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
// Desactivamos el buffer de los vértices gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); // Desactivamos el buffer de los colores si es necesario if (conColor) gl.glDisableClientState(GL10.GL_COLOR_ARRAY); } } // end clase
Podemos ver en el código anterior que hemos definido los vértices y colores del cuadrado en el constructor utilizando dos buffer de tipo float ya que la biblioteca OpenGL necesita disponer,
115
Aula Mentor
en este tipo de variable, de los vértices y colores del polígono. Después, en el método draw(), que invoca el Renderer cuando debe dibujar el cuadrado, realizamos los siguientes trabajos: - Indicamos el sentido del dibujo al contrario que las agujas del reloj mediante el método glFrontFace(GL10.GL_CCW). - Con el método glVertexPointer() indicamos el número de coordenadas (3), el tipo de datos de la matriz (float), la separación (0) en la matriz de los vértices (podría haber más información en esta matriz además de los vértices) y el buffer con los vértices. - De forma similar, usamos el método glColorPointer() para señalar el número de campos que definen un color (4), el tipo de datos de la matriz (float), la separación en la matriz de colores (0) y el buffer con los colores. Hemos definido en esta matriz los colores Rojo>Verde->Azul. La biblioteca OpenGL aplica los colores siguiendo el orden de dibujo de los vértices. Así el vértice 1º (abajo izq.) usa el color rojo, el 2º vértice (abajo dcha.) aplica el color verde, el vertice 3º (arriba izq.) utiliza el color Azul y el 4º vértice (arriba dcha.) vuelve a utilizar un color intermedio entre el azul y verde. - Indicamos al motor OpenGL que le hemos pasado una matriz de vértices y colores mediante el método glEnableClientState(). Usamos el método glDrawArrays() para dibujar la superficie del polígono mediante la matriz en el modo triángulos continuos GL_TRIANGLE_STRIP. Es decir, el cuadrado está formado por dos triángulo pegados:
116
- Finalmente, se desactivan los buffers de los vértices y colores con glDisableClientState(). Finalmente, para que la aplicación aparezca sin título y en el modo de pantalla completa hemos definido en el fichero AndroidManifest.xml el tipo de estilo siguiente:
android:theme=”@android:style/Theme.NoTitleBar.Fullscreen”
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 9 (OpenGL 2D) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la biblioteca OpenGL de Android.
Si ejecutas en Eclipse ADT este Ejemplo 9 en el AVD, verás que se muestra la siguiente aplicación que dibuja dos polígonos:
U1 Multimedia y Gráficos en Android
117
7.3 Gráficos en 3D con movimiento En el Ejemplo 10 de esta Unidad hemos dibujado los polígonos triángulo y cuadrado en 3D, es decir, una pirámide y un cubo a los que hemos animado con movimientos. Es recomendable abrir el Ejemplo 10 en Eclipse ADT para seguir la explicación posterior. Sólo se describirán en detalle aquellos ficheros de código fuente que son distintos del Ejemplo 9 anterior. Los archivos res/layout/main_layout.xml, MainActivity.java, Cuadrado.java y Triangulo.java de este proyecto son muy parecidos a los estudiados en el ejemplo anterior. Te invitamos a que los abras en Eclipse ADT. Hemos desarrollado el Renderer en la clase MiRenderer del proyecto, que contiene el siguiente código fuente: public class MiRenderer implements Renderer { // Constantes que indican el tipo de polígono que debemos dibujar public static int MOSTRAR_TRIANGULO=1; public static int MOSTRAR_CUADRADO=2; public static int MOSTRAR_CUBO=3; public static int MOSTRAR_PIRAMIDE=4; // Guarda el polígono que estamos mostrando private int mostrar_poligono=-1; // Objeto del tipo Triángulo que describe su geometría private Triangulo triangulo; // Objeto del tipo Cuadrado que describe su geometría
Aula Mentor
private Cuadrado cuadrado; // Objeto del tipo Pirámide que describe su geometría private Piramide piramide; // Objeto del tipo Cubo que describe su geometría private Cubo cubo; // Ángulo de giro del movimiento del polígono private float angulo; // Constructor de la clase public MiRenderer(int mostrar_poligono) { // Guardamos el tipo de polígono que queremos mostrar y // creamos la geometría correspondiente this.mostrar_poligono=mostrar_poligono; if (mostrar_poligono == MOSTRAR_TRIANGULO) triangulo = new Triangulo(); else if (mostrar_poligono == MOSTRAR_CUADRADO) cuadrado = new Cuadrado(); else if (mostrar_poligono == MOSTRAR_CUBO) cubo = new Cubo(); else if (mostrar_poligono == MOSTRAR_PIRAMIDE) piramide = new Piramide(); }
118
// Evento que se lanza cuando se crea la superficie. // Aquí debemos indicar todo aquello que no cambia en la superficie public void onSurfaceCreated(GL10 gl, EGLConfig config) { // Indicamos el modo de sombreado suave gl.glShadeModel(GL10.GL_SMOOTH); // Establecemos el color GRIS como fondo de la superficie gl.glClearColor(0.6f, 0.6f, 0.6f, 0.5f); // Configuramos el buffer de profundidad gl.glClearDepthf(1.0f); // Modo de renderizado de la profundidad gl.glEnable(GL10.GL_DEPTH_TEST); // Función de comparación de la profundidad gl.glDepthFunc(GL10.GL_LEQUAL); // Cómo se calcula la perspectiva gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); } // Evento que se lanza cada vez que es necesario dibujar la // superficie public void onDrawFrame(GL10 gl) { // Cada vez que se dibuja una escena hay que limpiar // tanto el buffer de color como el de profundidad gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // Reinicia la matriz de proyección del gráfico gl.glLoadIdentity(); // Movemos la posición de los ejes en (x,y,z). // Es decir, alejamos el dibujo en el eje Z dando // sensación de profundidad. gl.glTranslatef(0.0f, 0.0f, -6.0f);
U1 Multimedia y Gráficos en Android
// Dibujamos el polígono solicitado if (mostrar_poligono == MOSTRAR_TRIANGULO) { // Rotamos el triángulo un ángulo sobre el eje Y gl.glRotatef(angulo, 0.0f, 1.0f, 0.0f); triangulo.draw(gl); // Definimos el ángulo del siguiente giro angulo -= 0.45f; } else if (mostrar_poligono == MOSTRAR_CUADRADO) { // Rotamos el cuadrado un ángulo sobre el eje Y gl.glRotatef(angulo, 0.0f, 1.0f, 0.0f); cuadrado.draw(gl); // Definimos el ángulo del siguiente giro angulo += 0.4f; } else if (mostrar_poligono == MOSTRAR_CUBO) { // Escalamos el cubo al 80% para que quepa en // la pantalla del AVD gl.glScalef(0.8f, 0.8f, 0.8f); // Rotamos el cubo un ángulo sobre el eje X, Y, Z gl.glRotatef(angulo, 1.0f, 1.0f, 1.0f); cubo.draw(gl); // Definimos el ángulo del siguiente giro angulo -= 0.45f; } else { // Movemos la pirámide 1.0 al fondo para que quepa // en la pantalla del AVD gl.glTranslatef(0.0f, 0.0f, -1.0f); // Rotamos la pirámide un ángulo sobre el eje X,Y gl.glRotatef(angulo, 0.5f, 1.0f, 0.0f); piramide.draw(gl); // Definimos el ángulo del siguiente giro angulo -= 0.45f; } } // end onDrawFrame // Evento que se lanza cuando cambia la superficie public void onSurfaceChanged(GL10 gl, int width, int height) { // Nos aseguramos de que no se puede dividir por 0 if(height == 0) { height = 1; } // Reiniciamos la zona de visión gl.glViewport(0, 0, width, height); // Establecemos la matriz de proyección como // activa para modificarla y poder girar el polígono gl.glMatrixMode(GL10.GL_PROJECTION); // Reiniciamos la matriz de proyección del gráfico gl.glLoadIdentity(); // Calculamos el tamaño de la perspectiva en función del nuevo // tamaño de la pantalla GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f);
119
Aula Mentor
// Establecemos de nuevo la matriz de vista/modelo // como activa gl.glMatrixMode(GL10.GL_MODELVIEW); // Reiniciamos la matriz de proyección del gráfico gl.glLoadIdentity(); } } // end clase
En el código anterior podemos ver que en el constructor guardamos el tipo de polígono que queremos mostrar y creamos la geometría correspondiente. Después, en el método onSurfaceCreated()establecemos los valores cuya variación será poca o nula durante la ejecución del programa. En este caso, fijamos los mismos parámetros que en el Ejemplo 9. En el método onDrawFrame() insertamos las sentencias que deben ejecutarse durante el dibujo: - Cada vez que se dibuja una escena hay que limpiar tanto el buffer de color como el de profundidad mediante el método glClear(). - Se reinicia la matriz de proyección del gráfico con el método glLoadIdentity(). La matriz de proyección se utiliza para poder realizar cambios de perspectiva (giros, translaciones, etcétera). Realmente, lo que estamos haciendo es mover de nuevo el puntero de dibujo al centro del eje de coordenadas (0,0,0). - Movemos la posición de los ejes en (x,y,z) con el método glTranslatef(). Es decir, alejamos el dibujo en el eje Z dando sensación de profundidad al observador. - Rotamos el polígono mediante el método glRotatef(). Así, se logra la sensación de movimiento del polígono. - Invocamos el método draw del objeto que debemos dibujar y que hemos instanciado anteriormente.
120
Finalmente, en el método onSurfaceChanged() colocamos las sentencias que se ejecutarán cuando la superficie sobre la que dibujamos sufra alguna modificación, como la orientación de posición de la pantalla. En este caso, ejecutamos las mismas sentencias que en el Ejemplo 9. Veamos ahora cómo hemos definido las clases que describen la geometría de los nuevos polígonos. Empezamos por la pirámide desarrollada en el archivo Piramide.java: public class Piramide { // Buffer de tipo float que usamos para pasar // a la librería OpenGL los vértices de la pirámide private FloatBuffer bufferVertices; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los colores de la pirámide private FloatBuffer bufferColores; // Vértices de la pirámide private float vertices[] = { 0.0f, 1.0f, 0.0f, // Punto superior del triángulo frontal -1.0f, -1.0f, 1.0f, // Punto izq. del triángulo frontal 1.0f, -1.0f, 1.0f, // Punto dcho. del triángulo frontal
0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f,
// Punto superior del triángulo dcho. // Punto izq. del triángulo dcho. // Punto dcho. del triángulo dcho.
U1 Multimedia y Gráficos en Android
0.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
// Punto superior del triángulo de la base // Punto izq. del triángulo de la base // Punto dcho. del triángulo de la base
0.0f, 1.0f, 0.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f
// Punto superior del triángulo izq. // Punto izq. del triángulo izq. // Punto dcho. del triángulo izq.
}; // Matriz con colores en formato RGBA private float colores[] = { 1.0f, 0.0f, 0.0f, 1.0f, // Rojo 0.0f, 1.0f, 0.0f, 1.0f, // Verde 0.0f, 0.0f, 1.0f, 1.0f, // Azul 1.0f, 0.0f, 0.0f, 1.0f, // Rojo 0.0f, 0.0f, 1.0f, 1.0f, // Azul 0.0f, 1.0f, 0.0f, 1.0f, // Verde 1.0f, 0.0f, 0.0f, 1.0f, // Rojo 0.0f, 1.0f, 0.0f, 1.0f, // Verde 0.0f, 0.0f, 1.0f, 1.0f, // Azul 1.0f, 0.0f, 0.0f, 1.0f, // Rojo 0.0f, 0.0f, 1.0f, 1.0f, // Azul 0.0f, 1.0f, 0.0f, 1.0f // Verde };
// Constructor de la pirámide public Piramide() { // Definimos el buffer con los vértices del polígono. // Un número float tiene 4 bytes de longitud, así que // multiplicaremos x 4 el número de vértices. ByteBuffer byteBuf=ByteBuffer.allocateDirect(vertices.length*4); byteBuf.order(ByteOrder.nativeOrder()); bufferVertices = byteBuf.asFloatBuffer(); bufferVertices.put(vertices); bufferVertices.position(0);
// Definimos el buffer de la matriz de colores de igual forma // que hemos hecho con la matriz de vértices byteBuf = ByteBuffer.allocateDirect(colores.length * 4); byteBuf.order(ByteOrder.nativeOrder()); bufferColores = byteBuf.asFloatBuffer(); bufferColores.put(colores); bufferColores.position(0); }
// Método que invoca el Renderer cuando debe dibujar la pirámide public void draw(GL10 gl) { // Dibujamos al revés que las agujas del reloj gl.glFrontFace(GL10.GL_CCW);
// Indicamos el nº de coordenadas (3), el tipo de datos de la // matriz (float), la separación en la matriz de los vértices // (0) y el buffer con los vértices gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices); // Indicamos el nº de campos que definen el color (4), el tipo // de datos de la matriz (float), la separación en la matriz de // los colores (0) y el buffer con los colores.
121
Aula Mentor
gl.glColorPointer(4, GL10.GL_FLOAT, 0, bufferColores);
// Indicamos al motor OpenGL que le hemos pasado una matriz de // vértices y de colores gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
// Dibujamos la superficie mediante la matriz en el modo // triángulo gl.glDrawArrays(GL10.GL_TRIANGLES, 0, vertices.length / 3);
// Desactivamos el buffer de los vértices y colores gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_COLOR_ARRAY); } } // end clase
Podemos ver en el código anterior que hemos definido los vértices de los cuatro triángulos que forman las superficies de la pirámide en el constructor utilizando un buffer de tipo float. Además, definimos también un buffer adicional para los colores de ésta.
122
Después, en el método draw(), que invoca el Renderer cuando debe dibujar el pirámide, realizamos los siguientes trabajos: - Indicamos el sentido del dibujo al contrario que las agujas del reloj mediante el método glFrontFace(GL10.GL_CCW). - Con los métodos glVertexPointer() y glColorPointer() asociamos los buffer de los vértices y colores a la pirámide. - Indicamos al motor OpenGL que le hemos pasado las matrices de vértices y colores mediante el método glEnableClientState(). - Usamos el método glDrawArrays()para dibujar la superficie del polígono mediante la matriz en el modo triángulo GL_TRIANGLES. - Finalmente, se desactiva los buffer de vértices y colores con glDisableClientState(). Veamos ahora el Cubo especificado en el fichero Cubo.java: public class Cubo { // Buffer de tipo float que usamos para pasar // a la librería OpenGL los vértices del cubo private FloatBuffer bufferVertices; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los colores del cubo private FloatBuffer bufferColores; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los índices del cubo private ByteBuffer bufferIndices; // Vértices del cubo private float vertices[] = { -1.0f, -1.0f, 1.0f, // abajo delante izq. (V0) 1.0f, -1.0f, 1.0f, // abajo delante dcha. (V1) -1.0f, 1.0f, 1.0f, // arriba delante izq. (V2) 1.0f, 1.0f, 1.0f, // arriba delante dcha. (V3) 1.0f, -1.0f, -1.0f, // abajo detrás dcha. (V4) -1.0f, -1.0f, -1.0f, // abajo detrás izq. (V5) 1.0f, 1.0f, -1.0f, // arriba detrás dcha. (V6)
U1 Multimedia y Gráficos en Android
};
-1.0f, 1.0f, -1.0f
// arriba detrás izq. (V7)
// Matriz con colores en formato RGBA private float colores[] = { 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f };
// Matriz con los índices que definen los triángulos que // se usan para crear las caras private byte indices[] = { /* * Por ejemplo: * Cara definida mediante los vértices abajo detrás izq. (0), * abajo delante izq. (4), abajo delante dcha. (5), 0, 5, 1 */ 5, 0, 1, 5, 1, 4, // Y así las 6 caras en total 4, 1, 3, 4, 3, 6, 6, 3, 2, 6, 2, 7, 7, 2, 0, 7, 0, 5, 0, 2, 3, 0, 3, 1, 7, 5, 4, 7, 4, 6 }; // Constructor del cubo public Cubo() { // Definimos el buffer con los vértices del polígono. // Un número float tiene 4 bytes de longitud, así que // multiplicaremos x 4 el número de vértices. ByteBuffer byteBuf= ByteBuffer.allocateDirect(vertices.length*4); byteBuf.order(ByteOrder.nativeOrder()); bufferVertices = byteBuf.asFloatBuffer(); bufferVertices.put(vertices); bufferVertices.position(0); // Definimos el buffer de la matriz de colores de igual // forma que hemos hecho con la matriz de vértices byteBuf = ByteBuffer.allocateDirect(colores.length * 4); byteBuf.order(ByteOrder.nativeOrder()); bufferColores = byteBuf.asFloatBuffer(); bufferColores.put(colores); bufferColores.position(0); // Definimos el buffer de la matriz de índices de igual // forma que hemos hecho con la matriz de vértices bufferIndices = ByteBuffer.allocateDirect(indices.length); bufferIndices.put(indices); bufferIndices.position(0); } // end constructor
123
Aula Mentor
// Método que invoca el Renderer cuando debe dibujar el cubo public void draw(GL10 gl) { // Dibujamos al revés que las agujas del reloj gl.glFrontFace(GL10.GL_CCW); // Indicamos el nº de coordenadas (3), el tipo de datos de // la matriz (float), la separación en la matriz de los // vértices (0) y el buffer con los vértices gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices); // Indicamos el nº de campos que definen el color (4), el // tipo de datos de la matriz (float), la separación en la // matriz de los colores (0) y el buffer con los colores. gl.glColorPointer(4, GL10.GL_FLOAT, 0, bufferColores); // Indicamos al motor OpenGL que le hemos pasado una matriz // de vértices y de colores gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
124
// Dibujamos la superficie mediante la matriz en el modo // triángulo utilizando los índices para unirlos y formar // las caras gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, bufferIndices); // Desactivamos el buffer de los vértices y colores gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_COLOR_ARRAY); } } // end clase
Podemos ver en el código anterior que hemos definido los vértices y colores del cubo en el constructor utilizando dos buffer de tipo float ya que la biblioteca OpenGL necesita disponer, en este tipo de variable, de los vértices y colores del polígono. Además, hemos definido el buffer adicional bufferIndices que usamos para pasar a la biblioteca OpenGL los índices del cubo. Las caras del cubo se componen de dos triángulos pegados, por lo tanto, debemos definir las caras del cubo mediante esta matriz. Veamos el aspecto geométrico del cubo:
Vértices del cubo
Cara delantera
Después, en el método draw(), que invoca el Renderer cuando debe dibujar el cubo, realizamos los siguientes trabajos:
U1 Multimedia y Gráficos en Android
- Indicamos el sentido del dibujo al contrario que las agujas del reloj mediante el método glFrontFace(GL10.GL_CCW). - Con el método glVertexPointer() y glColorPointer() asociamos los vértices y colores al gráfico. - Indicamos al motor OpenGL que le hemos pasado una matriz de vértices y colores mediante el método glEnableClientState(). - Usamos el método glDrawArrays() para dibujar la superficie del polígono mediante la matriz en el modo triángulo GL_TRIANGLE utilizando el buffer de índices bufferIndices para unirlos y formar las caras. - Finalmente, se desactivan los buffers de vértices y colores con glDisableClientState().
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 10 (3D Movimiento) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la API de OpenGL para crear gráficos en movimiento en 3D de Android.
Si ejecutas en Eclipse ADT este Ejemplo 10 en el AVD, verás que se muestra la siguiente aplicación que dibuja cuatro polígonos (en 2D y 3D):
125
7.4 Gráficos en 3D con textura y movimiento En el Ejemplo 11 de esta Unidad hemos dibujado una pirámide en 3D con textura y que responde con movimientos a los toques del usuario girando sobre sus ejes. Es recomendable
Aula Mentor
abrir el Ejemplo 11 en Eclipse ADT para seguir la explicación posterior. El archivo MainActivity.java de este proyecto es muy parecido a los estudiados en los ejemplos anteriores. Te invitamos a que los abras en Eclipse ADT. Hemos desarrollado conjuntamente el Renderer y la superficie GLSurfaceView en la clase Superficie del proyecto, que contiene el siguiente código fuente: public class Superficie extends GLSurfaceView implements Renderer { // Objeto que define la pirámide con textura private Piramide piramide; // Ángulos de rotación en X e Y. Inicialmente la pirámide // aparece un poco girada private float anguloX=-15.0f; private float anguloY=-45.0f; // Variables donde se almacenan los ángulos anteriores // para así poder calcular el cambio de ángulo y girar // la pirámide únicamente la diferencia de ángulo. private float anguloAntiguoX; private float anguloAntiguoY; // Constante que usamos de escala del toque del usuario private final float ESCALA_TOQUE = 0.2f;
126
// Constructor de la clase public Superficie(Context context) { super(context); // Indicamos que el Renderer se define en esta misma superficie this.setRenderer(this); // Solicitamos el foco de la aplicación para que el usuario // pueda hacer clic en la imagen. Nota: esto sólo es necesario // si la aplicación tuviera otras Vistas, como botones this.requestFocus(); // Indicamos que la superficie responde a toques del usuario, // es decir, implementa el evento onTouchEvent this.setFocusableInTouchMode(true); } // Evento que se lanza cuando se crea la superficie. // Aquí debemos indicar todo aquello que no cambia en la superficie public void onSurfaceCreated(GL10 gl, EGLConfig config) { // Deshabilitamos la mezcla de colores gl.glDisable(GL10.GL_DITHER); // Habilitamos las texturas en 2D gl.glEnable(GL10.GL_TEXTURE_2D); // Establecemos el color NEGRO como fondo de la superficie gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Configuramos el buffer de profundidad gl.glClearDepthf(1.0f); // Modo de renderizado de la profundidad gl.glEnable(GL10.GL_DEPTH_TEST); // Función de comparación de la profundidad gl.glDepthFunc(GL10.GL_LEQUAL); // Cómo se calcula la perspectiva gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
U1 Multimedia y Gráficos en Android
// Cargamos la imagen en un bitmap que luego servirá de textura // a la pirámide Bitmap textura = BitmapFactory.decodeResource(getResources(), R.drawable.textura_piramide); // Como usamos una única textura, la matriz de texturas sólo // debe tener un elemento int texturaIds[] = new int[1]; // Asociamos la matriz de texturas al gráfico OpenGL gl.glGenTextures(1, texturaIds, 0); // Seleccionamos la primera textura de la matriz gl.glBindTexture(GL10.GL_TEXTURE_2D, texturaIds[0]); // Cargamos la imagen en formato bitmap en la textura GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, textura, 0); // Indicamos cómo tratar la textura cuando es necesario // modifiarla porque debe hacerse más pequeña gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); // Indicamos cómo tratar la textura cuando es necesario // modificarla porque debe hacerse más grande gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST); // Libera el bitmap ya que ya no es necesario textura.recycle(); // Creamos la pirámide usando la textura correspondiente piramide = new Piramide(texturaIds[0]); }
// Evento que se lanza cada vez que es necesario dibujar la // superficie public void onDrawFrame(GL10 gl) { // Establecemos el color NEGRO como fondo de la superficie gl.glClearColor(0.0f, 0.0f, 0.0f, 1f); // Cada vez que se dibuja una escena hay que limpiar // tanto el buffer de color como el de profundidad gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // Reiniciamos la matriz de proyección del gráfico gl.glLoadIdentity();
// Movemos la posición de los ejes en (x,y,z). // Es decir, alejamos el dibujo en el eje Z dando // sensación de profundidad. gl.glTranslatef(0.0f, 0.0f, -3.0f); // Escalamos la pirámide un 50% para que quepa en la pantalla gl.glScalef(0.5f, 0.5f, 0.5f);
// Rotamos en los ejes X e Y el toque del usuario gl.glRotatef(anguloX, 1.0f, 0.0f, 0.0f); gl.glRotatef(anguloY, 0.0f, 1.0f, 0.0f); // Dibujamos la pirámide piramide.draw(gl); } // end onDrawFrame // Evento que se lanza cuando cambia la superficie public void onSurfaceChanged(GL10 gl, int width, int height) {
127
Aula Mentor
// Nos aseguramos de que no se puede dividir por 0 if(height == 0) { height = 1; } // Reiniciamos la zona de visión gl.glViewport(0, 0, width, height); // Establecemos la matriz de proyección como // activa para modificarla y poder girar la pirámide gl.glMatrixMode(GL10.GL_PROJECTION); // Reiniciamos la matriz de proyección del gráfico gl.glLoadIdentity(); // Calculamos el tamaño de la perspectiva en función del nuevo // tamaño de la pantalla GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f); // Establecemos de nuevo la matriz de vista/modelo // como activa gl.glMatrixMode(GL10.GL_MODELVIEW); // Reiniciamos la matriz de proyección del gráfico gl.glLoadIdentity(); }
128
// Evento que se lanza cuando el usuario toca la pantalla @Override public boolean onTouchEvent(MotionEvent event) { // Obtenemos el toque del usuario float x = event.getX(); float y = event.getY(); // Si el toque es un movimiento sobre la pantalla if(event.getAction() == MotionEvent.ACTION_MOVE) { // Calculamos el cambio de ángulo restando con los // anteriores float dx = x - anguloAntiguoX; float dy = y - anguloAntiguoY; // Los nuevos ángulos son el actual + cambio de ángulo // teniendo en cuenta un factor de corrección anguloX += dy * ESCALA_TOQUE; anguloY += dx * ESCALA_TOQUE; } // Almacenamos los nuevos ángulos anguloAntiguoX = x; anguloAntiguoY = y; // Indicamos que se controla el evento onTouch return true; } // end onTouchEvent } // end clase
Puedes observar en el código anterior que hemos extendido esta clase de GLSurfaceView e implementado Renderer; así, en un mismo fichero Java desarrollamos la superficie de dibujo (lienzo) y el dibujante. En el constructor indicamos que el Renderer se define en esta misma clase y que la superficie responde a toques del usuario, es decir, implementa el evento onTouchEvent().
U1 Multimedia y Gráficos en Android
Después, en el método onSurfaceCreated()establecemos los valores cuya variación será poca o nula durante la ejecución del programa; en este caso, fijamos los mismos parámetros que los ejemplos anteriores e incluimos las sentencias necesarias que cargan la textura que usaremos luego para cubrir la pirámide. En el método onDrawFrame() insertamos las sentencias que deben ejecutarse durante el dibujo: - Cada vez que se dibuja una escena hay que limpiar tanto el buffer de color como el de profundidad mediante el método glClear(). - Se reinicia la matriz de proyección del gráfico con el método glLoadIdentity(). - Movemos la posición de los ejes en (x,y,z) con el método glTranslatef(). Es decir, alejamos el dibujo en el eje Z dando al observador la sensación de profundidad. - Rotamos la pirámide mediante el método glRotatef(). Así, se logra la sensación de movimiento cuando el usuario toca la pantalla. - Invocamos el método draw del objeto que debemos dibujar, que hemos instanciado anteriormente. En el método onSurfaceChanged()colocamos las sentencias que se ejecutarán cuando la superficie sobre la que dibujamos sufra alguna modificación, como la orientación de posición de la pantalla. En este caso, ejecutamos las mismas sentencias que en los ejemplos anteriores. Finalmente, en el método onTouchEvent() definimos las sentencias que se ejecutarán cuando el usuario toca la pantalla para mover la pirámide. En este caso, obtenemos la información del evento ocurrido, calculamos la diferencia de ángulos para así girar la pirámide únicamente esta diferencia, establecemos los nuevos ángulos teniendo en cuenta un factor de corrección. Veamos ahora cómo hemos definido la clase que describe la geometría de la pirámide en el archivo Piramide.java: public class Piramide { // Id de OpenGL de la textura de la pirámide private int texturaId; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los vértices de la pirámide private FloatBuffer bufferVertices; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los vértices de la base de la pirámide private FloatBuffer bufferVerticesBase; // Buffer de tipo float que usamos para pasar // a la librería OpenGL las texturas de la pirámide private FloatBuffer bufferTexturas; // Buffer de tipo float que usamos para pasar // a la librería OpenGL los colores del cubo private FloatBuffer bufferColores; // Vértices de la pirámide private float vertices[] = { 0.0f, 1.0f, 0.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f,
// Punto superior del triángulo frontal // Punto izq. del triángulo frontal // Punto dcho. del triángulo frontal // Punto superior del triángulo dcho. // Punto izq. del triángulo dcho. // Punto dcho. del triángulo dcho.
129
Aula Mentor
0.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
// Punto sup. del triángulo de la base // Punto izq. del triángulo de la base // Punto dcho. del triángulo de la base
0.0f, 1.0f, 0.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f
// Punto superior del triángulo izq. // Punto izq. del triángulo izq. // Punto dcho. del triángulo izq.
130
};
// Vértices de la base de la pirámide private float vertices_base[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f }; // Matriz con texturas private float texturas[] = { 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, };
// Matriz con colores de private float colores[] = 0.6f, 0.4f, 0.0f, 0.6f, 0.4f, 0.0f, 0.6f, 0.4f, 0.0f, 0.6f, 0.4f, 0.0f, };
public // // //
la base en formato RGBA { 1.0f, 1.0f, 1.0f, 1.0f
Piramide(int texturaId) { Usando el método local makeFloatBuffer definimos los buffer de la pirámide: vértices, texturas, vértices de su base y colores de su base.
bufferVertices = makeFloatBuffer(vertices); bufferTexturas = makeFloatBuffer(texturas); bufferVerticesBase = makeFloatBuffer(vertices_base); bufferColores = makeFloatBuffer(colores);
// Guardamos el id de la imagen que sirve de textura this.texturaId = texturaId; } // end constructor public void draw(GL10 gl) {
U1 Multimedia y Gráficos en Android
// Dibujamos al revés que las agujas del reloj gl.glFrontFace(GL10.GL_CCW); // Activamos la texturas en 2D (superficie) gl.glEnable(GL10.GL_TEXTURE_2D); // Indicamos el ID de la textura gl.glBindTexture(GL10.GL_TEXTURE_2D, texturaId); // Indicamos el nº de coordenadas (3), el tipo de datos de la // matriz (float), la separación en la matriz de los vértices // (0) y el buffer con los vértices gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices); // Seleccionamos la textura indicando el tamaño y tipo de la // matriz de texturas gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, bufferTexturas); // Indicamos al motor OpenGL que le hemos pasado una matriz de // vértices y de texturas gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// Dibujamos la superficie mediante la matriz en el modo // triángulo gl.glDrawArrays(GL10.GL_TRIANGLES, 0, vertices.length / 3); // Deshabilitamos la textura para seguir pintando sin ésta gl.glDisable(GL10.GL_TEXTURE_2D); // Indicamos el nº de campos que definen el color (4), el tipo // de datos de la matriz (float), la separación en la matriz de // los colores (0) y el buffer con los colores. gl.glColorPointer(4, GL10.GL_FLOAT, 0, bufferColores); // Indicamos al motor OpenGL que le hemos pasado una matriz de // colores para la base gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
// Establecemos los nuevos vértices de la base de la pirámide gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVerticesBase); // Dibujamos la base de la pirámide mediante la matriz en el // modo triángulo gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices_base.length/3);
// Desactivamos el buffer de los vértices, texturas y colores. gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisableClientState(GL10.GL_COLOR_ARRAY); } // end draw // Método que crea un FloatBuffer. Así optimizamos el código fuente protected static FloatBuffer makeFloatBuffer(float[] matriz) { // Definimos el buffer multiplicando x 4 ya que un número float // tiene 4 bytes de longitud. ByteBuffer bb = ByteBuffer.allocateDirect(matriz.length*4); bb.order(ByteOrder.nativeOrder()); FloatBuffer fb = bb.asFloatBuffer(); fb.put(matriz); fb.position(0);
131
Aula Mentor
return fb; } } // end clase
En el constructor de la clase anterior hemos usado el método local makeFloatBuffer() para simplificar el código fuente y definir los siguientes cuatro buffer de tipo float: - los vértices de los cuatro triángulos que forman las superficies de la pirámide. - las texturas que se aplican a estas 4 caras superiores que definen la pirámide, en total, 4 caras * 3 vértices = 12 puntos. - el cuadrado que forma la base de la pirámide. - el color de la base de ésta.
132
Después, en el método draw(), que invoca el Renderer cuando debe dibujar la pirámide, realizamos los siguientes trabajos: - Indicamos el sentido del dibujo al contrario que las agujas del reloj mediante el método glFrontFace(GL10.GL_CCW). - Activamos la texturas en 2D (tipo superficie) con glEnable() y seleccionamos la textura activa invocando glBindTexture(). - Con los métodos glVertexPointer() y glTexCoordPointer() asociamos los buffer de vértices y texturas de la pirámide. - Indicamos al motor OpenGL que le hemos pasado las matrices de vértices y texturas mediante el método glEnableClientState(). - Usamos el método glDrawArrays() para dibujar la superficie de la pirámide mediante la matriz en el modo triángulo GL_TRIANGLES. - Deshabilitamos la textura para seguir pintando sin ésta con el método glDisable(). - Con los métodos glVertexPointer() y glColorPointer() asociamos los buffer de vértices y colores de la base de la pirámide. - Indicamos al motor OpenGL que le hemos pasado las matrices de vértices y colores mediante el método glEnableClientState(). - Usamos el método glDrawArrays() para dibujar la superficie de la base de la pirámide mediante la matriz en el modo triángulo GL_TRIANGLES_STRIP. - Finalmente, se desactiva los buffer de vértices, colores y texturas con glDisableClientState().
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 11 (3D con texturas e interacción) de la Unidad 1. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la API de OpenGL para crear una pirámide en 3D con textura y que interacciona con el usuario.
U1 Multimedia y Gráficos en Android
Si ejecutas en Eclipse ADT este Ejemplo 11 en el AVD, verás que se muestra la siguiente aplicación que dibuja cuatro una pirámide con textura en 3D:
133
Recomendamos al alumno que desplace el ratón sobre la pirámide para ver cómo gira sobre sus ejes y cambia la perpectiva de ésta.
Las posibilidades de diseño gráfico con OpenGL son prácticamente infinitas. El diseñador gráfico puede incluir texturas complejas, transparencias, luces, sombras, cambios de perspectiva, etcétera. En este apartado se ha mostrado el uso básico de esta biblioteca.
Aula Mentor
8. Resumen Hay que saber al final de esta unidad: - Las clases de Android que permiten acceder a los servicios Multimedia son las siguientes: • MediaPlayer: reproduce audio y vídeo. • MediaController: representa los controles estándar para MediaPlayer. • VideoView: Vista que permite la reproducción de vídeo. • MediaRecorder: permite grabar audio y vídeo. • AsyncPlayer: reproduce una lista de archivos de tipo audio. • AudioManager: gestor del sonido del sistema operativo. • AudioTrack: reproduce un archivo de audio PCM. • SoundPool: gestiona y reproduce una colección de recursos de audio de corta duración. • JetPlayer: reproduce audio y vídeo interactivo. • Camera: cámara para tomar fotos y vídeo. • FaceDetector: clase para identificar la cara de personas en una imagen. - No todos los dispositivos Android incluyen el formato DivX de forma nativa.
134
- Android representa un color utilizando un número entero de 32 bits. Estos bits se dividen en 4 campos de 8 bits: alfa, rojo, verde y azul (ARGB, si usamos las iniciales en inglés). - Las clases más importantes para dibujar en Android son: • Paint se emplea para definir un pincel que utilizaremos para pintar. • Rect permite dibujar un rectángulo. • Path (del inglés, camino) permite definir un trazado mediante segmentos de líneas y curvas. • Canvas (del inglés, lienzo) representa la superficie básica donde podemos dibujar gráficos. - Un Dibujable (del inglés, Drawable) es un mecanismo para dibujar la interfaz de una aplicación Android. Podemos entender la clase Drawable como una abstracción que representa “algo que se puede dibujar”. - Android dispone de tres mecanismos para crear animaciones: • AnimationDrawable: drawables que reproducen una animación fotograma a fotograma (en inglés se denomina Frame Animation). • Animaciones Tween: permiten crear efectos de translación, rotación, zoom y alfa a cualquier vista de una aplicación Android, cambiando su representación en la pantalla. • API de animación de Android: anima cualquier propiedad de un objeto Java sea del tipo Vista o no, modificando el objeto en sí mismo. Esta API se denomina también Animación de Propiedades.
U1 Multimedia y Gráficos en Android
- ViewSurface (Vista de tipo Superficie) proporciona una superficie de dibujo dedicado que se incrusta dentro de una jerarquía de vistas. - Al utilizar Superficies es imprescindible que la interacción del usuario con la aplicación sea suave y los gráficos se muestren sin parpadeos. Para conseguirlo, debemos ejecutar dos hilos en la aplicación: el hilo principal, que se encarga de gestionar la Actividad, y un segundo hilo, que dibuja la superficie y gestiona la interacción del usuario con ésta. - Android dispone de dos bibliotecas para pintar gráficos en 3D: • OpenGL: librería estándar de diseño 3D. • RenderScript: nueva librería de bajo nivel de dibujo gráfico en 3D disponible desde la versión 3.0 de Android, que aprovecha el procesador GPU de la tarjeta gráfica para pintar. - Las clases básicas de la biblioteca OpenGL son las siguientes: • GLSurfaceView: clase base que permite escribir aplicaciones con OpenGL. • GLSurfaceView.Renderer: interfaz de dibujo genérica donde se detalla el código que indica lo que se debe dibujar. Podemos hacer una analogía con un pintor y su lienzo: el pintor es la interfaz GLSurfaceView.Renderer y el lienzo es GLSurfaceView. - Es importante recordar los conceptos básicos de geometría: Línea o Recta, Vértice, Cara, Arista, Polígono y Eje de coordenadas. - Las posibilidades de diseño gráfico con OpenGL son prácticamente infinitas. El diseñador gráfico puede incluir texturas complejas, transparencias, luces, sombras, cambios de perspectiva, etcétera.
135
Aula Mentor
Unidad 2. Interfaz de usuario avanzada
1. Introducción En esta segunda Unidad vamos a explicar cómo se aplican los temas y estilos de Android para cambiar el aspecto visual de una aplicación. Además, implementaremos Widgets en el Home y Lock Screen y crearemos un Live WallPaper (fondo de escritorio animado). Asimismo, utilizaremos los Fragmentos en Android y las Barras de Acción (Action Bars) en un proyecto Android. Finalmente, usaremos las clases GridView, Interruptor(Switch) y Navigation Drawer para mejorar la interfaz de usuario con varios ejemplos de aplicación en Android.
2. Estilos y Temas en las aplicaciones de Android
136
A todos los programadores nos gusta desarrollar funciones, procedimientos o métodos sencillos y eficientes que realicen un cometido específico. Cuando juntamos todos estos trocitos de código fuente obtenemos una aplicación tan compleja como queramos. Es decir, para resolver un problema complejo, hay que separarlo en pequeños problemas. Aunque la aplicación desarrollada cumpla con su cometido excelentemente, hoy en día es necesario que también tenga un aspecto visual atractivo con el diseño apropiado. A todos nos gusta que un plato esté bueno y, además, tenga un buen aspecto. Para un programador nativo es difícil afrontar el diseño visual de una aplicación; para eso existen diseñadores que conciben la interfaz visual sin estas ataduras del ingeniero. Desde el punto de vista de un programador, cuantas menos partes movibles, cambios de colores o fuentes tenga una aplicación más fácil será desarrollarla. En este apartado de teoría vamos a mostrar ejemplos sobre cómo implementar de forma sencilla y rápida estas mejoras visuales que hacen que las aplicaciones sean más agradables. Para ello utilizaremos temas (themes, en inglés) y estilos (styles, en inglés). Como sabes, en Android se utilizan Vistas (Views) para diseñar la interfaz visual de las Actividades (Activities). Aunque es posible definir las Vistas mediante sentencias de Java dentro de las Actividades, lo usual es declararlas en un archivo de diseño layout de tipo XML. El entorno de desarrollo en Eclipse ADT incluye un editor visual para diseñar estos archivos xml. Sin embargo, este editor no permite cambiar el aspecto (color, fuente, márgenes y otros atributos) que van a tener estas Vistas. En inglés, se denomina “look and feel” al aspecto visual de una aplicación. Para facilitar el diseño visual, el programador/diseñador de Android dispone de temas y estilos. Si has diseñado alguna vez una página web, sabrás que para optimizar recursos, el estilo de cada página se incluye en un archivo CSS con los atributos de las diferentes etiquetas de HTML. A continuación, vamos a ver cómo crear un tema en una aplicación Android.
U2 Multimedia y Gráficos en Android
2.1 Cómo crear un Tema Los temas en Android se incluyen dentro de la carpeta res/values/ mediantes un archivo xml (por defecto, se llama styles.xml) donde se definen todos los atributos que aplican a este tema.
Nota: Dependiendo de la versión de Android para la que vayamos a compilar el proyecto es necesario incluir en directorios distintos la definición de los temas, ya que la versión 14 (Android 4.0) dispone de más atributos que la versión 11 (Android 3.0). En el ejemplo de este apartado, por simplificación, sólo definimos un tema para la versión de Android del curso. 137 Es recomendable que abras desde Eclipse el proyecto Ejemplo 1 (Temas y estilos) de la Unidad 2 que vamos a describir a continuación.
Si abres el archivo res/values/temas.xml, verás que hemos creado el tema “Tema” a partir del tema padre por defecto de Android android:Theme y añadido un par de atributos:
2sp 20dip
En el código anterior podemos ver que hemos personalizado los atributos “margenActividad” (margen de la página) y “android:windowTitleSize” (tamaño de la barra del título de la aplicación). Es recomendable establecer el tema padre (parent=”android:Theme”) dentro del tema por defecto del sistema operativo Android para heredar todos los atributos del sistema.
Aula Mentor
Los temas visuales de Android son un medio rico y complejo que permiten definir todos los atributos del aspecto de Vistas de Android. Dada la cantidad de atributos que define Android, es recomendable definir nuestro nuevo tema a partir de uno ya existente en el sistema operativo, es decir, debemos extenderlo de uno predefinido. Veamos los cuatro temas básicos por defecto que define el sistema operativo Android: - Theme: es el Tema más básico que se incluyó en la primera versión de Android y que hemos utilizado en el ejemplo del curso. Se trata de un tema con fondo oscuro y los textos en color claro que está disponible en todas las versiones de Android, si bien puede variar ligeramente dependiendo del fabricante del dispositivo. - Theme.Light: variación de tema Theme, muestra texto oscuro con fondo claro. - Theme.Holo: este tema se introdujo en la versión 3.0 de Android y presenta un aspecto más moderno que los dos anteriores. Tiene una particularidad: los fabricantes de dispositivos no deben modificarlo. - Theme.Holo.Light: variación en color claro del tema anterior. Para indicar el tema visual por defecto de la aplicación podemos establecer el atributo “android_ theme” en la aplicación o en cada una de las actividades en su archivo Manifest.xml. Por ejemplo, así:
138
También se puede establecer el tema de una actividad mediante sentencias de Java. Para ello, debemos utilizar el método setTheme() en el evento onCreate() de la actividad justo antes de la ejecución de la sentencia setContentView(). Hay que tener en cuenta que, por lo general, se debe evitar esta forma de indicar el tema visual, salvo que la aplicación que desarrollemos deba cambiar de forma dinámica sus temas.
2.2 Atributos personalizados Como hemos visto anteriormente, para modificar los márgenes de todas las actividades, podemos definir el nuevo tamaño en un único lugar: archivo temas.xml de la aplicación estableciendo el atributo margenActividad: 2sp
Sin embargo, si copiaras el texto anterior en el archive tema.xml verías que Eclipse mostraría el error: No resource found that matches the given name: attr ‘margenActividad’. Esto ocurre porque el atributo margenActividad no existe por defecto en el sistema Android. No obstante, el atributo android:windowTitleSize es correcto ya que sí está definido en el SDK de Android. Para definir este atributo nuevo, debemos crear el archivo atributos.xml dentro de la carpeta res/values/. Podemos ver cómo hemos definido el atributo margenActividad en este fichero:
Ahora, Eclipse no mostrará un error al estar definido el atributo margenActividad. El campo
U2 Multimedia y Gráficos en Android
format indica el tipo de valores que se pueden definir para este atributo; en este caso, hemos indicado que debe indicarse el nombre de otro atributo (reference) o (|) una dimensión (dimension) como, por ejemplo 2sp o 4px. Además, podemos incluir tantos formatos como
queramos del siguiente listado: - reference: el nombre de otro atributo - string: de tipo cadena - color - dimension: dimensión - boolean: lógico - integer: número entero - float: número con decimales - fraction: fracción - enum: campo enumerado - flag: de tipo marcas
Una vez definidos los márgenes de las Vistas, podemos establecerlo haciendo referencia al mismo en la definición del layout correspondiente: por ejemplo, así:
Es decir, si defino el atributo margenActividad en un único archivo y se hace referencia a éste en la definición de las Vistas en el Layout de la interfaz de la aplicación, puedo modificarlo de forma sencilla y rápida sin tener que alterar el resto del código fuente. Si te fijas en el código anterior, hemos definido también el atributo tamanioTexto, es decir, para todas las Vistas es necesario incluir tantas propiedades como atributos hayamos definido en el tema. Android soluciona este inconveniente mediante los Estilos (Styles) que agrupan varios atributos de un Vista en un único bloque que se pueden aplicar al aspecto de esta Vista. Veamos cómo se hace. Por ejemplo, si todos los TextViews de una aplicación deben tener exactamente el mismo aspecto (color, fuente, margen, alineamiento, etcétera), podemos definir un estilo en el archivo res/values/atributos.xml con sus características de igual forma que hemos hecho en el caso de los atributos:
En este caso, hemos indicado que el estilo, que se define como un atributo más, debe hacer referencia a otro atributo. En el ejemplo del curso, en el archivo res/layout/main.xml podemos ver cómo se asigna el estilo textoTitulo a un TextView:
Finalmente, para definir el estilo textoTitulo debemos declararlo en el tema correspondiente en el fichero temas.xml:
139
Aula Mentor
@style/pagina_background_blanco ... @style/texto_titulo_blanco
Si te fijas en el código anterior, verás que se hace referencia al estilo texto_titulo_blanco que se define en el archivo values/estilos.xml:
@drawable/barra_titulo_blanco center_vertical|center_horizontal #FFF 24sp 1.0 1.0 3 #888
Aquí ya sí se establecen las propiedades visuales de las distintas Vistas teniendo en cuenta las propiedades que posean.
140
A modo de resumen, para utilizar los temas conjuntamente con estilos debemos dar los siguientes pasos: - Definir el nombre del tema y sus atributos (características de aspecto) disponibles en el archivo res/values/temas.xml del proyecto. - En el fichero res/values/atributos.xml especificar los mismos atributos anteriores mediante referencia a otro atributo (format=”reference”). - Diseñar el aspecto de cada atributo con las propiedades visuales de una Vista Android en el fichero res/values/estilos.xml. - Aplicar el estilo correspondiente a las vistas que define la interfaz del usuario (en el archivo layout xml) mediante su propiedad style.
2.3 Definición de recursos dibujables (Drawable) Como hemos estudiado en la Unidad 1, un recurso dibujable (del inglés, Drawable) es una forma de definir cómo se dibuja la interfaz de una aplicación Android. Hemos visto que existen muchos tipos de recursos dibujables, tales como archivos de imágenes, colores, gradientes, formas geométricas, etcétera. A continuación, vamos a estudiar algunos recursos, los que están directamente relacionados con los estilos:
2.3.1 Recurso de color Como su nombre indica, con este recurso podemos definir un color común a toda la aplicación creando el fichero res/values/colores.xml y escribiendo lo siguiente: #FFF
U2 Multimedia y Gráficos en Android
Como verás, su utilización es muy sencilla: en el código anterior hemos declarado la etiqueta
color con su nombre e indicado el color en formato hexadecimal. Después, para poder aplicar este color a un estilo o Vista, basta con abrir el archivo estilos.xml y usar este color con la nomenclatura @color: @color/azul
2.3.2 Recurso de dimensión Como su nombre indica, un recurso de tipo Dimensión permite definir una longitud común a toda la aplicación. Por ejemplo, en el fichero res/values/estilos.xml hemos incluido lo siguiente: 2sp
Como verás, su utilización es también muy sencilla; en el código anterior hemos declarado la etiqueta dimen con su nombre e indicado el tamaño en formato “sp” como unidad de medida. Después, para poder aplicar esta dimensión a un estilo o Vista, basta con abrir el archivo estilos.xml y usarla con la nomenclatura @dimen: #000 @dimen/pagina_margen_azul
En este caso hemos establecido un margen interno (padding) del fondo del tema azul. Las magnitudes de medida en interfaces gráficas de Android son las siguientes: - px: Píxeles - unidad en píxeles independientemente de la resolución de la pantalla. - in: Inches (Pulgadas) - unidad en pulgadas independientemente del tamaño de la pantalla. - mm: Milímetros - unidad en milímetros que depende del tamaño de la pantalla. - pt: Points (Puntos) - unidad en puntos por pulgada que depende del tamaño de la pantalla del dispositivo. - dp: Density-independent Pixels (Densidad independiente en píxeles) - unidad abstracta basada en la densidad física de la pantalla. Es recomendable intentar utilizar siempre esta unidad ya que los elementos gráficos y Vistas se representarán con el mismo tamaño independientemente del tamaño y resolución del dispositivo Android. - sp: Scale-independent Pixels (Independiente de la escala en píxeles) – unidad parecida a la anterior (dp) pero utilizada para definir los tamaños de la fuente de las textos que aparecen en la interfaz del usuario.
2.3.3 Gradiente Drawable (Gradiente dibujable) Un Gradient Drawable permite al programador dibujar un gradiente de colores que consiste en mostrar un degradado de dos colores en el fondo de cualquier Vista. En el ejemplo del curso, puedes abrir al archivo res/drawable/ gradiente_gris_background.xml para ver cómo se define este tipo de elemento visual:
141
Aula Mentor
En este caso hemos definido un degradado que comienza (startColor) en el color gris y termina (endColor) en el color negro. Como puedes ver, estos colores también están definidos como un recurso de color. Además, hemos indicado que aplique un giro (angle) al degradado de 270 grados. Una vez hemos definido el gradiente, es sencillo aplicarlo al tema en el archivo estilos.xml así: @drawable/gradiente_gris_background @dimen/pagina_margen_blanco
Aplicándolo al tema en el archivo temas.xml:
@style/pagina_background_blanco
Y asociando el estilo en el fichero layout main.xml que define la interfaz de la aplicación:
2.3.4 Selector Drawable (Selector dibujable) 142
Un Selector Drawable es un tipo de elemento que permite realizar cambios automáticos basados en el aspecto de una Vista teniendo en cuenta el estado actual de ésta. En el ejemplo del curso, puedes abrir al archivo res/drawable/boton_blanco.xml
para ver cómo se define este tipo de elemento visual:
Verás que hemos definido un selector que cambia el aspecto de una Vista cuando ocurre alguno de estos eventos: - state_pressed: si el usuario presiona (hace clic) sobre la Vista que tiene asociado este estilo, entonces se debe cambiar su aspecto utilizando el elemento dibujable boton_blanco_presionado. Definido también dentro de la carpeta drawable con el nombre boton_ blanco_presionado.xml. Puedes abrir este archivo para ver su contenido. - state_focused: si el usuario selecciona (centra el foco con el tabulador) la Vista que tiene asociado este estilo, entonces se debe cambiar su aspecto utilizando el elemento dibujable boton_blanco_seleccionado. Definido también dentro de la carpeta drawable con el nombre boton_blanco_seleccionado.xml. Puedes abrir este archivo para ver su contenido. - Sin evento: en este caso, no se incluye ningún evento y se define el aspecto normal de la Vista utilizando el elemento dibujable boton_blanco_normal. Definido también dentro de la carpeta drawable con el nombre boton_blanco_normal.xml. Puedes abrir este archivo para ver su contenido.
U2 Multimedia y Gráficos en Android
- state_selected: establece el aspecto de la Vista al estar seleccionada, por ejemplo, en el caso de una opción de un listado. En este caso, al tratarse de un botón, no hemos incluido este evento al no tener sentido. En el archivo res/values/estilos.xml puedes encontrar cómo se define el botón blanco: @drawable/boton_blanco 3sp
Después, en el archivo res/values/temas.xml es muy sencillo asociar este estilo en el tema que corresponda: ... @style/blanco_background_blanco @style/boton_blanco
Si haces clic en los botones del ejemplo del curso, verás que cambia el aspecto de botón:
2.3.5 Nine-patch drawable con botones Un Nine-patch Drawable es un tipo especial de imagen que escala o crece tanto a lo largo como a lo ancho y que mantiene su relación de aspecto visual. Es decir, Android va a aumentar el tamaño de este elemento teniendo en cuenta las dimensiones de las vistas. Aunque se pueden utilizar en cualquier tipo de Vista, comúnmente se utilizan en Botones. En el Ejemplo 1 hemos implementado este tipo de elemento en el Tema Azul. Veamos cómo se hace siguiendo estos sencillos pasos: - Disponemos de un archivo de tipo imagen que representa el botón en tamaño pequeño res/drawable/boton.png:
- Definimos un estilo en el archivo estilos.xml indicando el padre de la Vista y el fondo que debe aplicarse en este caso: @drawable/boton
• En el fichero temas.xml, aplicamos al tema visual correspondiente el estilo de botón que hemos definido en el punto anterior: ... @style/blanco_background_azul @style/MiBoton<
143
Aula Mentor
• Finalmente, indicamos el estilo que debe aplicarse a los botones en el layout de la aplicación:
Si ejecutamos la aplicación del ejemplo y hacemos clic en el botón “Tema Azul”, veremos que el aspecto de los botones cambia cuando utilizamos este archivo de tipo imagen como fondo de estos botones:
En el manual oficial sobre DRAWABLES de Android puedes encontrar un listado completo de todos los drawables disponibles, así como sus propiedades. En este apartado hemos incluido los más interesantes o utilizados por el programador en el diseño de estilos.
2.4 Atributos de los temas 144
Como hemos visto, mediante temas visuales podemos definir en las Vistas una amplia variedad de atributos que cambian el aspecto de éstas. A continuación, vamos a indicar los más utilizados por el programador: - colorBackgroundCacheHint Mediante este atributo establecemos el color del fondo de pantalla de una Vista (en los ejemplos hemos utilizado también el atributo background) y debería ser vacío (“null”) cuando el fondo es una textura o traslúcido, es decir, sólo sirve para establecer colores sólidos. - textAppearance Atributo para establecer el aspecto por defecto del texto: color, tipo de fuente, tamaño y estilo. - textColorPrimary Color principal del texto. - textAppearanceLarge Aspecto por defecto del texto del tamaño grande: color, tipo de fuente, tamaño y estilo. - textAppearanceMedium Aspecto por defecto del texto del tamaño medio: color, tipo de fuente, tamaño y estilo. - textAppearanceSmall Aspecto por defecto del texto del tamaño pequeño: color, tipo de fuente, tamaño y estilo. - buttonStyle Estilo del botón normal. - listDivider Atributo que define el drawable del divisor de una lista.
U2 Multimedia y Gráficos en Android
- windowBackground Drawable utilizado como el fondo de la ventana. A diferencia del atributo anterior colorBackgroundCacheHint, podemos establecer tanto texturas, colores sólidos y traslúcidos como fondos de pantalla. - windowFrame Drawable utilizado para definir el marco de una ventana. - windowActionBar Marca que indica que la ventana debe mostrar una Barra de Acción (Action Bar) en lugar del usual título de la ventana. Más adelante, en esta misma Unidad 2, veremos en qué consiste un Action Bar. - alertDialogTheme Tema que se debe aplicar a una ventana de diálogo de alerta. - progressBarStyle Estilo por defecto de la vista ProgressBar. Normalmente, es un círculo que gira.actionBarStyle
Atributo que permite establecer el estilo de un Action Bar.
Recuerda que para indicar un atributo por defecto del sistema operativo de Android es necesario escribir el texto android: antes del nombre del mismo, por ejemplo, así: android:windowBackground.
En el manual oficial sobre ATRIBUTOS de Android puedes encontrar un listado completo con la descripción de todos los atributos disponibles. En este apartado hemos incluido los más interesantes o utilizados por el programador.
2.5 Carga dinámica de Temas Es posible definir en tiempo de ejecución, el tema visual que debe cargar una aplicación Android. Si abres el archivo temas.xml, verás que hemos definido varios temas visuales: @style/pagina_background_blanco @style/pagina_padding_layout_blanco @style/texto_titulo_blanco ... @style/pagina_background_azul @style/pagina_padding_layout_azul @style/texto_titulo_azul
Para intercambiar los diferentes temas en tiempo de ejecución hemos reiniciado la Actividad principal y utilizado el método setTheme() en el evento onActivityCreateSetTheme() antes de que la aplicación invoque el método setContentView(): // Reinicia y establece el tema de una Actividad. public static void changeToTheme(Activity activity, int tema) {
145
Aula Mentor
nTema = tema; // Finalizamos la actividad activity.finish(); // Volvemos a ejecutar de nuevo la Actividad activity.startActivity(new Intent(activity, activity.getClass()));
} // Establece el tema de la Actividad de acuerdo con la variable nTema public static void onActivityCreateSetTheme(Activity activity) { switch (nTema) { default: case TEMA_DEFECTO: break; case TEMA_BLANCO: activity.setTheme(R.style.Tema_Blanco); break; case TEMA_AZUL: activity.setTheme(R.style.Tema_Azul); break; } }
146
Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Temas y estilos) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado Temas visuales.
Si ejecutas en Eclipse este Ejemplo 1, verás que se muestra la siguiente aplicación en el Emulador:
U2 Multimedia y Gráficos en Android
Al pulsar en los distintos botones, verás que la carga de un tema hace que el aspecto de la aplicación cambie radicalmente. Así de potente es esta técnica si se emplea correctamente. Aunque a priori pueda parecer complicado gestionar temas visuales, basta con recorrer el código fuente del Ejemplo 1 del curso para ir entendiendo las relaciones entre Estilos, Temas y Vistas. Te animamos a que estudies con atención su código fuente.
3. Implementación de Widgets en la pantalla principal En informática, un Widget es una aplicación reducida o programa de tamaño pequeño que permite al usuario visualizar información de forma rápida en la pantalla y/o acceder a funciones utilizadas frecuentemente. Dado que son pequeñas aplicaciones, los Widgets en Android pueden hacer casi todo lo que nuestra imaginación desee e interactuar con servicios e información diversa. Por ejemplo, pueden consistir en relojes vistosos, notas, calculadoras, calendarios, agendas, juegos, ventanas con información del tiempo en tu ciudad, etcétera. En Android, se pueden diseñar Widgets tanto en la pantalla principal del dispositivo (en inglés, Home Screen) como en su pantalla de bloqueo (en inglés, Lock Screen); esto último, únicamente es posible a partir de la versión 4.2 de Android). Sobre los Widgets de Android debemos saber que: - Se pueden arrastrar y cambiar su posición en el escritorio. - Es posible eliminarlo del escritorio. - El usuario puede modificar su tamaño a partir de la versión 3.1 de Android. - Ofrecen una interfaz en la que el usuario puede interactuar como si fuera una Actividad más. - Actualizan su información de forma periódica al recibir información de cualquier componente del sistema. - Se puede crear un Widget tantas veces como queramos. - Un Widget al ser creado puede invocar a una Actividad de configuración. En esta actividad se suelen recoger los parámetros iniciales necesarios para generar la instancia del Widget. - Un Widget se ejecuta como parte del proceso de la aplicación que lo define y, por lo tanto, mantiene los permisos de seguridad de ésta. Para programar un Widget de Android hay que tener en cuenta lo siguiente: - Como hemos visto, en el escritorio de un dispositivo pueden existir varias instancias de un mismo Widget, pues el sistema operativo los distingue asignándoles un ID para cada instancia del mismo. - El comportamiento de todas sus instancias se programa en una clase que hereda de AppWidgetProvider. Esta clase dispone del método onUpdate() que el sistema invoca cada vez que es necesario actualizar el Widget. Esta clase es la que da forma al concepto de Widget en el sistema operativo. - Para crear la interfaz visual de usuario de los Widgets debemos utilizar la clase RemoteViews. Un objeto de tipo RemoteView puede ser modificado por otro proceso como un servicio que tenga los mismos permisos que la aplicación original. - La interfaz de usuario del Widget se define mediante un BroadcastReceiver que infla el layout en un objeto del tipo RemoteViews que es el que pasa a formar parte del escritorio del dispositivo. En el archivo AndroidManifest.xml del proyecto debe quedar registrado este receptor (receiver) para que el Widget reciba mensajes del tipo android.appwidget.action.APPWIDGET_UPDATE cuando sea necesario crearlo o actualizarlo. - Los parámetros que definen el Widget se incluyen en un archivo xml que determina aspectos visuales tales como sus dimensiones, la frecuencia de actualización, los controles de la interfaz y la Actividad que permite al usuario configurarlo.
147
Aula Mentor
- Si hemos definido una Actividad de configuración en el fichero anterior, cada vez que se cree el Widget se ejecutará esta Actividad para indicar sus parámetros iniciales. Si deseamos que el usuario pueda reconfigurar el Widget debemos crear un Actividad extra que aparezca en el escritorio y permita realizar esta tarea.
3.1 Tipos de Widgets y sus limitaciones
148
Los Widgets de Android parecen muy simples cuando los utilizamos, sin embargo existen ciertas limitaciones a la hora de programarlos que debes conocer. Si desarrollas un Widget sencillo que no requiere la gestión de su estado y se actualiza un par de veces al día, verás que es muy fácil de implementar. Si implementas un Widget con gestión del estado que se actualiza con poca frecuencia con cambios elementales, podemos definir su comportamiento dentro del propio Widget. Sin embargo, para un Widget complejo que se actualiza cada poco tiempo (segundos o milisegundos), debemos utilizar el gestor de alarmas (AlarmManager) o, probablemente, sea necesario desarrollar un servicio que gestione el estado del Widget. En el ejemplo del curso hemos utilizado los servicios, aunque no es estrictamente necesario, para mostrar cómo se implementa este tipo de Widget complejo. La única interacción posible del usuario con las Vistas incluidas en un Widget es mediante el evento OnClickListener que se lanza cuando el usuario hace clic sobre éste. En relación con los gestos, únicamente se permite un toque (Touch) y arrastrar verticalmente (Vertical Swipe) En el Ejemplo 3 veremos cómo funciona este último gesto al cambiar las imágenes. Otro factor a considerar es que los objetos RemoteViews que especifican la interfaz del usuario tienen restricciones sobre los tipos Vistas y layouts que podemos incluir en ellos. Es decir, no podemos incluir en un Widget todas las Vistas disponibles en el SDK de Android. Además, el programador no puede gestionar directamente las Vistas internas del RemoteView, sino debe hacerlo mediante los métodos proporcionados por esta clase. Seguidamente, mostramos las Vistas y layouts que podemos incluir en el diseño de un Widget: FrameLayout LinearLayout RelativeLayout AnalogClock Button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFlipper
Aunque esta lista parezca reducida, aumenta con cada nueva versión de Android. La razón principal para restringir lo que se puede incluir en una RemoteView es que estas Vistas deben estar desconectadas de los procesos que las controlan. En realidad, estas Vistas están alojadas en otra aplicación como es “Home Application” (aplicación de “lanzadera” de aplicaciones que funciona como escritorio en Android que, además, el usuario puede cambiar a su antojo). Los gestores de estas Vistas son procesos en segundo plano que se invocan por temporizadores, por
U2 Multimedia y Gráficos en Android
esta razón, a esas vistas se las denomina vistas remotas (remote views) y pueden ser modificadas por otros procesos con los mismos permisos.
Las Vistas ListView, GridView, StackView y AdapterViewFlipper se pueden utilizar en el interior de un Widget a partir de la versión Android 3.0, aunque su uso tiene algunas particularidades ya que debemos emplear Colecciones (Collections). Al final de esta apartado comentaremos algo más sobre ellas.
3.2 Ciclo de vida de un Widget Antes de empezar a implementar un Widget, vamos a analizar el ciclo de vida de éste dentro del sistema operativo Android estudiando la secuencia de creación y actualización.
149
La descripción de estos pasos es la siguiente: - Cuando el usuario decide añadir un Widget al escritorio de su dispositivo, el sistema operativo captura la orden y envía el mensaje android.appwidget.action.APPWIDGET_UPDATE a la aplicación. - Android infla el Widget y crea su instancia obteniendo su ID único. - A continuación, si existe, invoca la Actividad de configuración en el método onCreate() con un mensaje Intent que incluye entre sus parámetros el ID del widget creado anteriormente. - La Actividad de configuración realiza las operaciones oportunas como el inicio de un servicio, uso de un ContentProvider, etcétera; y actualiza las preferencias sobre la instancia del Widget. - Finalmente, se devuelve el control al sistema operativo. - Cada vez que es necesario actualizar el Widget, Android ejecuta el método onUpdate() de su Proveedor de Widgets (WidgetProvider, que veremos más adelante). - El WidgetProvider actualiza todas las instancias de Widgets creadas en el escritorio. Además, el sistema operativo puede desactivar o activar un Widget como una Actividad normal y corriente mediante los métodos ya conocidos por el alumno onDisable() onEnabled() respectivamente y onDestroy() en caso de eliminación.
Aula Mentor
3.3 Ejemplo de Creación de un Widget Para iniciar un proyecto que implemente un Widget debemos crear en Eclipse ADT un proyecto normal de tipo “Android”. El Ejemplo 2 de esta Unidad consta de los siguientes archivos: - Interfaz de usuario del Widget: /res/layout/widget_layout_rojo.xml y /res/layout/ widget_layout_verde.xml
- Fichero de configuración del widget: /res/xml/widget_info.xml - Clase que define el Widget (extendida de AppWidgetProvider): HorarioTrabajoWidgetProvider.java
- Servicio que actualiza el Widget: ServicioActualizacionWidget.java - Interfaz de la Actividad de configuración del Widget: /res/layout/preferencias_widget.xml
- Actividad de configuración de las preferencias: PreferenciasWidget.java - Definición de la aplicación: AndroidManifest.xml
3.4 Ejemplo de implementación de un Widget En esta parte, vamos a ver como se implementa un Widget estudiando los ficheros que hemos nombrado anteriormente. Interfaz de usuario del Widget: /res/layout/widget_layout_rojo.xml y /res/layout/ widget_layout_verde.xml
150
Estos dos primeros archivos definen los layout que dibujan la interfaz del Widget que está compuesta únicamente por un LinearLayout, una imagen (ImageView) y una etiqueta de texto (TextView) donde aparece el mensaje al usuario. Veamos, por ejemplo, cómo es el layout widget_layout_verde.xml:
U2 Multimedia y Gráficos en Android
Importante: como verás, hemos utilizado únicamente Vistas permitidas dentro de un Widget accesibles mediante la clase RemoteViews.
Además, como fondo del layout hemos utilizado el dibujable @drawable/forma_verde en el que se define un rectángulo redondeado con un gradiente de color verde así:
151
3.4.1 Fichero de configuración del widget: /res/xml/widget_info.xml
En este fichero XML se definen las propiedades del Widget como, por ejemplo, el tamaño en pantalla o la frecuencia de actualización. Este XML se debe crear en la carpeta \res\xml del proyecto. En el ejemplo del curso tiene la siguiente estructura:
Hemos declarado las siguientes propiedades: - initialLayout: layout XML creado en el paso anterior que debe inflar el Widget al iniciarse. - minHeight: alto mínimo del widget en pantalla en dp (density-independent pixels). - minWidth: ancho mínimo del widget en pantalla en dp (density-independent pixels). - resizeMode: modo de redimensionamiento (sólo para Android 3.1 en adelante). - updatePeriodMillis: frecuencia de actualización del widget en milisegundos. En el ejemplo definimos 30 minutos = 180.000 milisegundos. - configure: actividad de configuración del widget.
Aula Mentor
Existen otras propiedades que se pueden definir como, por ejemplo, el icono de la vista previa del widget (android:previewImage), aunque sólo para Android >3.1. Puedes consultarlas todas en AppWidgetProviderInfo. La pantalla inicial de Android se divide en un mínimo de 4×4 celdas (un dispositivo con mayor pantalla pueden contener más) donde se pueden colocar aplicaciones, accesos directos y Widgets. Teniendo en cuenta las diferentes dimensiones de estas celdas según el dispositivo y la orientación de la pantalla, existe una fórmula sencilla para ajustar las dimensiones de nuestro widget para que ocupe un número determinado de celdas sea cual sea la orientación: - ancho_mínimo = (num_celdas * 70) – 30 - alto_mínimo = (num_celdas * 70) – 30 Empleando esta fórmula, el widget del Ejemplo del curso ocupa un tamaño mínimo de 3 celdas de ancho por 1 celda de alto; por lo tanto, debemos indicar unas dimensiones de 146dp x 40dp.
En la Guía de diseño de Widgets de Android puedes encontrar recomendaciones de estilo a la hora de diseñarlos.
3.4.2 Clase que define el Widget: HorarioTrabajoWidgetProvider.java
152
Esta clase implementa la funcionalidad del widget y deberá extenderse de la clase AppWidgetProvider que, a su vez, es una clase derivada de BroadcastReceiver, ya que los widgets de Android son un caso particular de este tipo de componentes. En esta clase se implementan los mensajes a los que vamos a responder desde el Widget, entre los que destacan: - onEnabled(): lanzado cuando se crea la primera instancia de un Widget. - onUpdate(): ejecutado periódicamente cuando hay que actualizar un Widget cada vez que se cumple el periodo de tiempo definido por el parámetro updatePeriodMillis descrito anteriormente o al añadir el Widget al escritorio. - onDeleted(): lanzado cuando se elimina del escritorio una instancia de un widget. - onDisabled(): ejecutado cuando se elimina del escritorio la última instancia de un Widget. - onReceive(): se implementa para enviar llamadas a los distintos métodos de otros AppWidgetProvider o para actualizar un Widget individualmente. En la mayoría de los casos, tendremos que implementar, como mínimo, el evento onUpdate(). El resto de métodos dependerán de la funcionalidad de nuestro Widget. En el Ejemplo del curso implementamos este método onUpdate() y el método onReceive(): // Define la Clase que llama Android cada vez que se crea y actualiza // el Widget. Debe extenderse de la clase AppWidgetProvider public class HorarioTrabajoWidgetProvider extends AppWidgetProvider { private static final String LOG = “es.mentor.unidad2.eje2.widgethomescreen”; // Evento que inicia Android cada vez que tiene que actualizar el // widget @Override public void onUpdate(Context context, AppWidgetManager
U2 Multimedia y Gráficos en Android
appWidgetManager, int[] appWidgetIds) { // Actualizamos el Widget llamando a un servicio actualizaWidget(context, appWidgetManager); // Nota: si el Widget fuera sencillo no es necesario utilizar un // Servicio y podríamos incluir aquí las sentencias que actualizan // el Widget
} // Si quisiéramos actualizar un único Widget al hacer clic sobre él // deberíamos implementar aquí la actualización @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); Log.i(LOG,”Mensaje recibido: “ + action + “ del Widget: “ +appWidgetId); super.onReceive(context,intent); } // Método que lanza el servicio que actualiza el contenido del // Widget public static void actualizaWidget(Context context, AppWidgetManager appWidgetManager) { // Generamos un log para ver si se actualiza el widget Log.w(LOG, “Método onUpdate ejecutado”); // Obtenemos el nombre del componente del Widget ComponentName esteWidget = new ComponentName(context, HorarioTrabajoWidgetProvider.class); // Obtenemos todos los IDs de los widget mediante el método // getAppWidgetIds del AppWidgetManager y los copiamos a una // matriz int[] todosWidgetIds = appWidgetManager.getAppWidgetIds(esteWidget); // Construimos un Intent para llamar al servicio correspondiente Intent intent = new Intent(context.getApplicationContext(), ServicioActualizacionWidget.class); // Incluimos los IDs de todos los Widgets intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, todosWidgetIds); // Actualizamos el widget invocando el servicio de actualización context.startService(intent); } }
En el código anterior hemos utilizado un servicio o el gestor de Alarmas (AlarmManager) para actualizar el Widget. Si la ejecución del método onUpdate()conlleva complejas operaciones o se hace una petición a un servicio web en Internet que puede tardar en responder, Android puede cerrar el Widget y mostrar el típico mensaje de error “Aplicación no responde” (Application Not Responding = ANR) al pasar más de 10 segundos. Por el contrario, si el Widget es sencillo, no es necesario utilizar un servicio y podríamos incluir en este método onUpdate() las sentencias que lo actualizan utilizando la clase RemoteViews de la misma manera que se hacemos en el servicio.
153
Aula Mentor
3.4.3 Servicio que actualiza el Widget: ServicioActualizacionWidget.java
Como hemos visto, un Widget puede actualizar su aspecto/información cada cierto tiempo, definido en su archivo xml de información con la propiedad updatePeriodMillis. Cuando la actualización implica operaciones de larga duración (más de 10 segundos), podemos aplicar dos métodos en el método onUpdate() de la clase WidgetProvider para implementarla: - Mediante un servicio que veremos en el ejemplo de este curso avanzado - Utilizando el Gestor de alarmas (AlarmManager) estudiado en el curso de iniciación de Mentor (Unidad 7 – Ejemplo 3). La diferencia entre un método y otro radica en que el mínimo intervalo que podemos definir en un servicio es de 1.800.000 milisegundos (30 minutos), mientras que si usamos el Gestor de alarmas (AlarmManager) podemos aumentar la frecuencia de actualización y, además, es más eficiente desde el punto de vista energético para el dispositivo. Para utilizar este segundo método debemos definir un servicio similar al primer método pero, en lugar de llamarlo, hay que programarlo en este Gestor de alarmas (AlarmManager). En la Unidad 7 del curso de Iniciación se estudia cómo hacerlo.
154
Cuanto mayor sea la frecuencia de actualización del Widget, el dispositivo, “despertará” más a menudo para ejecutar esa tarea y consumirá más batería. Muchos Widget, al configurarlos, permiten al usuario que elija el intervalo de actualización; así, éste decide cómo quiere gastar la batería de su dispositivo.
Veamos cómo hemos definido el servicio de actualización en la clase ServicioActualizacionWidget.java: // Servicio que se ejecuta cada x segundos para actualizar el Widget o // se lanza desde la Actividad de configuración cuando ésta acaba public class ServicioActualizacionWidget extends Service { private static final String LOG = “es.mentor.unidad2.eje2.widgethomescreen”; @Override public void onStart(Intent intent, int startId) { Log.i(LOG, “Servicio iniciado”); // Obtenemos los datos de las preferencias boolean trabajando= PreferenciasWidget.obtenTrabajando( ServicioActualizacionWidget.this); String nombre= PreferenciasWidget.obtenNombre( ServicioActualizacionWidget.this); // Obtenemos la hora actual long hora = System.currentTimeMillis(); // Obtenemos de forma directa los IDs de los Widgets que se // pasan como parámetro de tipo Intent en la llamada al servicio // con el método onStart int[] intentWidgetIds = intent .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
U2 Multimedia y Gráficos en Android
// Ahora obtenemos todos los IDs de los Widgets de forma // indirecta mediante el gestor de Widget de Android AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this .getApplicationContext()); // Nombre del componente que define nuestro Widget ComponentName esteWidget = new ComponentName(getApplicationContext(), HorarioTrabajoWidgetProvider.class); // Obtenemos todos los Widgets de este tipo int[] losWidgetIds = appWidgetManager.getAppWidgetIds(esteWidget); // Mostramos un log con el número de Widgets obtenidos de forma // directa e indirecta: debe coincidir el número Log.w(LOG, “Número de Widgets en Intent: “ + String.valueOf(intentWidgetIds.length)); Log.w(LOG, “Número de Widgets de appWidgetManager: “ + String.valueOf(losWidgetIds.length)); // Formateador que usamos para mostrar la hora SimpleDateFormat curFormater = new SimpleDateFormat(“hh:mm”, Locale.getDefault()); // Cada vez que se ejecuta el servicio dejamos de trabajar // (hay que descansar cada 30 minutos :-) trabajando=!trabajando; // Actualizamos todos los widgets: podemos usar tanto la matriz // intentWidgetIds como losWidgetIds ya que deben contener los // mismos IDs de widgets. Se incluyen aquí los dos métodos para // que el alumno los conozca for (int widgetId : losWidgetIds) { // Objeto donde inflamos el layout de Widget RemoteViews remoteViews; // Log de control interno Log.i(nombre + “¿Está trabajando?: “, String.valueOf(trabajando)); // Si no trabajamos usamos el layout rojo para crear el // RemoteViews. Nota: podríamos haber también definido un // único layout y haber modificado sus Vistas internas if (!trabajando) { // Creamos la imagen con el layout remoteViews = new RemoteViews( this.getApplicationContext().getPackageName(), R.layout.widget_layout_rojo); // Establecemos el mensaje en la vista remota “texto” remoteViews.setTextViewText(R.id.texto, nombre + “ no trabaja - Hora salida: “ + curFormater.format(hora)); } else { // Si trabajamos usamos el layout verde para crear el // RemoteViews remoteViews = new RemoteViews(this .getApplicationContext().getPackageName(), R.layout.widget_layout_verde); // Establecemos el mensaje en la vista remota “texto” remoteViews.setTextViewText(R.id.texto, nombre + “ trabaja – Hora entrada:”+ curFormater.format(hora));
155
Aula Mentor
}
156
// Guardamos las preferencias PreferenciasWidget.guardarTrabajando( ServicioActualizacionWidget.this, trabajando); PreferenciasWidget.guardarHora( ServicioActualizacionWidget.this, hora); // Registramos el evento onClickListener mediante un // PendingIntent Intent clickIntent = new Intent(this.getApplicationContext(), HorarioTrabajoWidgetProvider.class); // Indicamos el tipo de acción de Intent clickIntent.setAction( AppWidgetManager.ACTION_APPWIDGET_UPDATE); // Indicamos todos los IDs de los Widgets porque onUpdate // del WidgetProvider lo necesita para actualizar todos los // Widgets clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, losWidgetIds); // Indicamos también el ID de este Widget porque onReceive del // WidgetProvider lo necesita para actualizarlo (si estuviera // desarrollado este evento) clickIntent. putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); // Definimos una Intención Pendiente para este widgetId PendingIntent pendingIntent = PendingIntent.getBroadcast( getApplicationContext(), widgetId, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); // Establecemos el evento onClick sobre el layout completo del // Widget para detectar el clic en cualquier sitio de éste. remoteViews.setOnClickPendingIntent(R.id.layout_widget, pendingIntent); // Indicamos al appWidgetManager que actualice la vista con el // nuevo remoteViews appWidgetManager.updateAppWidget(widgetId, remoteViews); } // end for // Paramos el servicio al acabar para que no se actualice // continuamente stopSelf(); } @Override public IBinder onBind(Intent intent) { // No es posible que un Cliente se conecte (Bind) al servicio return null; } }
Es importante destacar que, como hemos comentado repetidamente, las Vistas internas de un Widget se basan en un tipo especial llamado RemoteViews. En el código anterior puedes observar que para acceder a estas Vistas remotas que constituyen la interfaz del Widget construimos un nuevo objeto RemoteViews inflando su layout correspondiente. Una vez creado este objeto, podemos modificar el contenido de las Vistas internas mediante una serie de métodos set (uno para cada tipo de datos básicos) que permiten establecer las propiedades de cada Vista individual del Widget. Por ejemplo, remoteViews.set-
U2 Multimedia y Gráficos en Android
TextViewText(). Estos métodos reciben como parámetros el ID de la Vista y el valor que se desea establecer. Puedes consultar todos los métodos disponibles en la documentación oficial de Android sobre la clase RemoteViews. Después, vamos a implementar la funcionalidad del evento onClick sobre el Widget para forzar la actualización del mismo. Ya sabemos que a las Vistas contenidas en un Widget de Android son del tipo RemoteView y no podemos asociar eventos de la forma tradicional. Sin embargo, en su lugar, Android permite asociar a un evento (por ejemplo, el click sobre una Vista o Layout) un determinado mensaje (Pending Intent) de tipo broadcast que será ejecutado cada vez que se produzca dicho evento. Además, podemos configurar el Widget, que es un componente de tipo broadcast receiver, para que capture esos mensajes e implemente en su evento onReceive() las acciones necesarias que debe ejecutar tras capturar dicho mensaje. De esta forma, podremos simular la captura de eventos sobre un Widget. Vamos a estudiar el código anterior. En primer lugar, hacemos que se lance un Intent de tipo broadcast cada vez que se pulse el botón del widget. Para ello, creamos un nuevo Intent asociándole la acción de Android AppWidgetManager.ACTION_APPWIDGET_UPDATE (también podríamos haber definido una acción personalizada). Como parámetros del nuevo Intent insertaremos mediante putExtra() todos los IDs de los Widget y, separadamente, el ID del Widget actual de forma que más tarde podamos obtener el Widget en concreto que ha lanzado el mensaje (recuerda que podemos tener varias instancias del mismo widget en el escritorio). Por último, crearemos un mensaje del tipo PendingIntent mediante el método getBroadcast() y lo asociaremos al evento onClick del control llamando a setOnClickPendingIntent() pasándole el ID de la Vista interna del Widget que, en nuestro caso, es el Layout que lo define. Además, es muy importante destacar la sentencia hacia el final del código donde se invoca el método updateAppWidget() del WidgetManager para que se actualice correctamente la interfaz del Widget.
3.4.4 Interfaz de la Actividad de configuración del Widget: /res/layout/preferencias_widget.xml
En el fichero definimos la interfaz de configuración inicial del widget de forma similar a como haríamos con cualquier otra actividad Android diseñando su layout xml. En este caso, el diseño es muy sencillo con un cuadro de texto para introducir el nombre personalizado y dos botones, uno para aceptar la configuración y otro para cancelar, en cuyo caso el widget no se crea en el escritorio. Veamos su contenido:
158
3.4.5 Actividad de configuración de las preferencias: PreferenciasWidget.java
Una vez diseñada la interfaz de la Actividad de configuración tendremos que implementar su funcionalidad con código java en esta clase. En primer lugar, es necesario conocer el identificador de la instancia concreta del widget que se configurará con esta actividad. Este ID lo podemos obtener de un parámetro del Intent que ha lanzado la actividad con configuración. Para ello, usamos el método getIntent() para obtener un puntero al Intent y mediante el método getExtras()accedemos a su parámetro AppWidgetManager.EXTRA_APPWIDGET_ID. Veamos qué aspecto tiene el código: public class PreferenciasWidget extends Activity{ private static final String PREFS_NAME= “es.mentor.unidad2.eje2.widgethomescreen. HorarioTrabajoWidgetProvider”; private static final String PREF_PREFIX_KEY = “nombre”; private static final String PREF_PREFIX_KEY2 = “trabajando”; private static final String PREF_PREFIX_KEY3 = “hora”; int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; EditText txtValorInicial; Button btnAceptar, btnCancelar; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle);
U2 Multimedia y Gráficos en Android
// Establecemos el valor de retorno de la actividad a CANCEL // para que, en caso de que el usuario pulse el botón back // del dispositivo, se cancele la creación del widget. setResult(RESULT_CANCELED); // Establecemos la interfaz del layout a utilizar. setContentView(R.layout.preferencias_widget); // Definimos las referencias a las Vistas txtValorInicial = (EditText)findViewById(R.id.txtNombre); btnAceptar =(Button)findViewById(R.id.btnAceptar); btnAceptar.setOnClickListener(mOnClickListener); btnCancelar =(Button)findViewById(R.id.btnCancelar); //Implementación del botón “Cancelar” btnCancelar.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { //Devolvemos como resultado: CANCELAR (RESULT_CANCELED) finish(); } }); // Obtenemos el mensaje (Intent) del sistema que ha iniciado la // actividad de configuración. Este mensaje contiene el ID del // widget que hay que configurar. Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { // Obtenemos el ID o un ID inválido si no existe mAppWidgetId = extras.getInt( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } // Si el ID es inválido acabamos la actividad de configuración // -> No se crea Widget if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { finish(); } // Obtenemos el nombre inicial del usuario. // Aquí podríamos utilizar la variable mAppWidgetId si varios // widgets pudieran contener varios // valores diferentes y definiríamos un método del estilo obtenNombre(PreferenciasWidget.this, mAppWidgetId); // No es el caso del ejemplo donde todos muestran lo mismo String nombreinicial=obtenNombre(PreferenciasWidget.this); // Si no tenemos nombre no escribimos nada en el TextView correspondiente if (nombreinicial.equals(“Sin nombre”)) txtValorInicial.setText(“”); else txtValorInicial.setText(nombreinicial); } // Definimos el evento onClick del botón Aceptar View.OnClickListener mOnClickListener = new View.OnClickListener() { public void onClick(View v) { final Context context = PreferenciasWidget.this; // Cuando se pulsa el botón grabamos el valor escrito en // la caja de texto. No incluimos validación de formato.
159
Aula Mentor
String value = txtValorInicial.getText().toString(); if (value.isEmpty()) value=”Currante”; guardarNombre(context, value); // Al crear el Widget, por defecto no se trabaja guardarTrabajando(context, false); // Apuntamos la hora actual guardarHora(context, System.currentTimeMillis()); // A continuación, vamos a lanzar el servicio que // actualiza el contenido del Widget. // Para ello, usamos el gestor de Widgets de Android AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); // Actualizamos el Widget HorarioTrabajoWidgetProvider.actualizaWidget(context, appWidgetManager);
};
160
}
// Notificamos la terminación de la actividad de // configuración. Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); setResult(RESULT_OK, resultValue); // Finalizamos la Actividad finish();
// Métodos utilizados para gestionar las preferencias del widget. // Se utilizan preferencias compartidas SharedPreferences. static void guardarNombre(Context context, String value) { SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit(); prefs.putString(PREF_PREFIX_KEY, value); prefs.commit(); } static void guardarTrabajando(Context context, Boolean value) { SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit(); prefs.putBoolean(PREF_PREFIX_KEY2, value); prefs.commit(); } static void guardarHora(Context context, long value) { SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit(); prefs.putLong(PREF_PREFIX_KEY3, value); prefs.commit(); } static String obtenNombre(Context context) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); return prefs.getString(PREF_PREFIX_KEY, “Sin nombre”); }
U2 Multimedia y Gráficos en Android
static Boolean obtenTrabajando(Context context) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); return prefs.getBoolean(PREF_PREFIX_KEY2, true); } static long obtenHora(Context context) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); return prefs.getLong(PREF_PREFIX_KEY3, System.currentTimeMillis()); } }
En el código anterior podemos ver cómo establecemos el resultado por defecto que devuelve la Actividad de configuración mediante el método setResult(). Una actividad de configuración de un Widget debe siempre devolver un resultado RESULT_OK si la configuración es correcta o RESULT_CANCELED si el usuario sale de la configuración sin aceptar los cambios. En las sentencias anteriores establecemos al principio un resultado RESULT_CANCELED por defecto para asegurarnos de que, si el usuario abandona la configuración pulsando el botón “Volver” del dispositivo o pulsando el botón Cancelar de esta Actividad de configuración, no se añade el Widget al escritorio por error. El código del botón Aceptar es algo más complejo: - Guarda el nombre que ha introducido el usuario utilizando la API de Preferencias ya estudiada en el curso de Iniciación. - Actualiza la interfaz del Widget según la configuración establecida ejecutando el método actualizaWidget() de HorarioTrabajoWidgetProvider. Si incluimos una Actividad de configuración para un Widget, es necesario que esta misma Actividad sea la responsable de realizar la primera actualización del mismo (si es necesario). Es decir, tras finalizar la actividad de configuración Android no lanza automáticamente el evento onUpdate() del Widget (aunque sí se lanzará posteriormente y de forma periódica según la configuración del parámetro updatePeriodMillis del WidgetProvider), sino que tendrá que ser la propia actividad quien fuerce esta primera actualización. Para ello, basta con obtener una referencia al WidgetManager de nuestro contexto mediante el método AppWidgetManager.getInstance() y con esta referencia llamaremos al método estático de actualización del Widget HorarioTrabajoWidgetProvider.actualizaWidget(), que actualizará los datos de todos las Vistas internas. - Devuelve el resultado RESULT_OK incluyendo en el Intent de respuestas el ID del Widget. Por último, invocamos al método finish() para finalizar la actividad.
3.4.6 Definición de la aplicación: AndroidManifest.xml
Finalmente, para acabar, debemos declarar el Widget dentro del manifest de la aplicación. Así, incluimos los elementos siguientes dentro de la etiqueta :
161
Aula Mentor
162
El Widget se declarará como un elemento indicando la siguiente información: - Atributo name: referencia a la clase java del WidgetProvider. - Atributo icon: imagen que se muestra al usuario cuando desea añadir el Widget. - Atributo label: texto que se muestra al usuario cuando desea añadir el Widget. - Elemento : indica los “eventos” a los que responderá el widget, por ejemplo, aquí indicamos el evento APPWIDGET_UPDATE para detectar la acción de actualización. Nota: en este apartado podríamos incluir más elementos para capturar más eventos, incluso eventos personalizados por la propia aplicación. - Elemento : establecemos la referencia el XML que define la información del Widget. El servicio que actualiza el Widget se declara mediante la etiqueta . Para indicar la Actividad de configuración, incluimos una nueva etiqueta con el elemento utilizando el evento APPWIDGET_CONFIGURE para detectar la acción de configuración.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 2 (Control horario de trabajo) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado Widgets.
Para probarlo, podemos ejecutar el proyecto desde Eclipse ADT en el emulador de Android y esperar a que se ejecute la aplicación principal (sin desarrollar, ya que no hemos incluido
U2 Multimedia y Gráficos en Android
ninguna funcionalidad) e ir a la pantalla principal del emulador y añadir nuestro Widget al escritorio tal cómo lo haríamos en un dispositivo físico: - Hasta Android 2.x: pulsación larga sobre el escritorio o pulsación en la tecla Menú y seleccionar la opción Widgets seleccionando el Widget del curso. - Desde Android 3.0 en adelante: accedemos al menú principal, pulsamos la pestaña Widgets y buscamos el Widget en la lista y realizamos sobre él una pulsación larga hasta que el sistema nos permite arrastrarlo y soltarlo sobre el escritorio.
163
Prueba a añadir varias instancias al escritorio, actualizarlos haciendo click sobre ellos, desplazarlos por la pantalla, cambiar su tamaño y eliminarlos. Si ejecutas en Eclipse ADT este Ejemplo 2, verás que se muestra la siguiente aplicación en el Emulador:
Aula Mentor
Importante: al probar en Eclipse ADT un Widget que hemos desarrollado, a veces, es fundamental borrarlos del escritorio y volverlos a añadir ya que, según la documentación de Google, algunas veces no se actualizan correctamente cuando se instala una nueva versión con las últimas modificaciones que hemos hecho.
3.5 Colecciones de Vistas en Widgets Las Vistas ListView, GridView (que estudiaremos más adelante), StackView y AdapterViewFlipper se pueden utilizar en el interior de un Widget a partir de la versión Android 3.0 empleando las Vistas de Colecciones (Collections). Estas Vistas complejas contienen en su interior otras Vistas. Las Vistas de Colecciones se definen mediante dos layouts: uno para el Widget y el otro para definir cada elemento interno en la colección. Estos elementos internos se construyen utilizando instancias de la clase factorial RemoteViewsFactory que se obtiene del servicio del sistema RemoteViewsService. Este servicio requiere que la aplicación tenga el permiso android.permission.BIND_REMOTEVIEWS. Para conectar las Vistas con este servicio en el método onUpdate()del Widget es necesario definir un Intent que apunte al servicio y utilizar el método setRemoteAdapter() de la clase RemoteViews. En realidad, es como si definiéramos un adaptador para el listado interno. Fíjate en el siguiente ejemplo de código fuente del Ejemplo 3 de esta Unidad: 164
// Obtenemos el servicio StackViewService mediante un Intent // que nos permite acceder a las Vistas internas Intent intent = new Intent(context, StackWidgetService.class); // Incluimos el ID del Widget en cuestión intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); // Obtenemos el RemoteViews RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout); // Asociamos al StackView del Widget el intent anteriormente creado // que se define en el layout del Widget rv.setRemoteAdapter(R.id.stack_view, intent); // La Vista vacía (empty view) se muestra cuando la colección no tiene // elementos que mostrar // que se define en el layout del Widget rv.setEmptyView(R.id.stack_view, R.id.vista_vacia); // Indicamos a Android que actualice el Widget. ¡Muy importante! appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 3 (El tiempo) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado Widgets con colecciones de Vistas.
U2 Multimedia y Gráficos en Android
Para probarlo, podemos ejecutar el proyecto desde Eclipse ADT en el emulador de Android y esperar a que se ejecute la aplicación principal (sin desarrollar, ya que no hemos incluido ninguna funcionalidad) e ir a la pantalla principal del emulador y añadir nuestro Widget al escritorio tal cómo lo haríamos en un dispositivo físico:
165 Si arrastramos la imagen superior hacia arriba veremos cómo van rotando las imágenes del tiempo en el Widget.
3.6 Activando Widgets en la pantalla de Bloqueo Desde la versión 4.2 de Android en adelante es posible incluir Widgets en la pantalla de bloqueo (Lock Screen) del dispositivo. Para ello, basta con incluir en el archivo XML que define el Widget el atributo android:widgetCategory de esta forma: ...
En el ejemplo anterior indicamos que el Widget se puede incluir tanto en la pantalla de bloqueo como en el escritorio normal. Además, es posible detectar en tiempo de ejecución en qué pantalla se está ejecutando el Widget, basta con añadir el método onUpdate() del WidgetProvider estas sentencias:
Aula Mentor
Bundle options = appWidgetManager.getAppWidgetOptions(widgetId); int categoria = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1); boolean esLockScreen = categoria == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
Así, es posible que un mismo Widget presente diferente aspecto si se ejecuta en la pantalla de bloqueo o en el escritorio normal. De forma análoga, es posible definir un atributo similar a android:initialLayout que define el layout que se infla por primera vez al crear el Widget; pero asociado únicamente a la pantalla de bloqueo mediante el nuevo atributo android:initialKeyguardLayout cuando se añade a ésta.
Te animamos a que programes tus propios Widgets en Android para extender las funcionalidades de los dispositivos mediante estos programas, a veces, no tan pequeños o simples.
4. Creación de fondos de pantalla animados 166
Hoy en día Android dispone de infinitas posibilidades de personalización de modo que es fácil que no existan dos dispositivos completamente iguales. En este apartado vamos a estudiar cómo desarrollar un fondo de pantalla animado (del inglés, Live Wallpaper) para este sistema operativo. Aunque los fondos de pantalla animados están un poco pasados de moda debido a que consumen batería y, en consecuencia, afectan a la autonomía del dispositivo, por lo que muchos usuarios han dejado de utilizarlos. Sin embargo, muchos usuarios siguen buscando este tipo de fondos para personalizar sus dispositivos. Los Live Wallpapers son fondos de pantalla animados e interactivos de Android similares a las aplicaciones de Android que pueden incluir casi la misma funcionalidad que estas últimas.
4.1 Ejemplo de Creación de un fondo de pantalla animado Para iniciar un proyecto que implemente un fondo de pantalla animado debemos crear en Eclipse ADT un proyecto normal de tipo “Android”. El Ejemplo 4 de esta Unidad consta de los siguientes archivos: - Fichero de configuración del fondo animado: /res/xml/miwallpaper.xml - Servicio que implementa el fondo animado: MiServicioWallpaper.java - Interfaz de la Actividad de configuración del fondo animado: /res/xml/preferencias. xml
- Actividad de configuración de las preferencias: PreferenciasActivity.java - Actividad principal del usuario: EstablecerWallpaperActivity.java - Definición de la aplicación: AndroidManifest.xml
U2 Multimedia y Gráficos en Android
4.2 Ejemplo de implementación de un fondo animado En esta parte, vamos a ver cómo se implementa un Widget estudiando los ficheros que hemos nombrado anteriormente.
4.2.1 Fichero de configuración del fondo animado: /res/xml/miwallpaper.xml
Para crear un fondo de pantalla animado es necesario componer un archivo XML que lo describa. Si abrimos en Eclipse ADT este archivo veremos que contiene lo siguiente:
Este archivo debe incluir la descripción del fondo animado (atributo android:description) y puede contener una vista previa o icono del fondo (atributo android:thumbnail) y la referencia a la Actividad de configuración (atributo android:settingsActivity) que permite personalizar el fondo de pantalla animado. Como puedes ver, en Android se pueden establecer preferencias hasta en los fondos animados.
4.2.2 Servicio que implementa el fondo animado: MiServicioWallpaper.java
En este archivo se implementa el comportamiento del fondo animado mediante un servicio de Android que se debe extender de la clase WallpaperService, que es la clase base del sistema operativo para fondos de pantalla animados. Debemos implementar el método abstracto onCreateEngine() que tiene que devolver un objeto del tipo WallpaperService.Engine. ¿Por qué se hace esto y no se utiliza el servicio directamente? El servicio de fondos de pantalla puede tener múltiples instancias en ejecución, por ejemplo, como fondo de pantalla funcionando y como una vista previa, cada uno de los cuales debe tener su propia instancia del motor separadamente. Este objeto Engine controla el ciclo de vida de los eventos, animaciones y de dibujo del fondo animado a través de,los métodos siguientes, entre otros: - onCreate(): se lanza al crearlo. - onVisibilityChanged(): aparece cuando el fondo animado cambia su visibilidad. Por ejemplo, cuando el usuario ejecuta una aplicación y el fondo de pantalla deja de estar visible y viceversa. - onTouchEvent(): se ejecuta cuando el usuario toca el fondo de pantalla. - onSurfaceDestroyed(): ejecutado cuando se destruye el fondo de tipo SurfaceHolder.
167
Aula Mentor
- onSurfaceChanged(): se aplica cuando cambia el fondo del dispositivo como un cambio
de orientación de éste.
Veamos ahora el código fuente de este servicio de Android: public class MiServicioWallpaper extends WallpaperService { // Método abstracto que debemos implementar que // devuelve un objeto extendido de Engine que es // realmente el que implementa el fondo animado @Override public Engine onCreateEngine() { return new MiWallpaperEngine(); } // Clase que crea el fondo animado e implementa // OnSharedPreferenceChangeListener para detectar cambios en // las preferencias private class MiWallpaperEngine extends Engine implements SharedPreferences.OnSharedPreferenceChangeListener {
168
// // // // //
Para actualizar el fondo animado y no mostrar un mensaje de error del tipo ANR, vamos a usar la clase Runnable con su Handler asociado. Así, podemos dibujar el fondo con un proceso en segundo plano sin bloquear el hilo principal
// Gestor de runnables private final Handler handler = new Handler();
// Definimos un objeto Runnable que es el que se // encargará de procesar la tarea que actualiza el // fondo animado private final Runnable drawRunnable = new Runnable() { @Override public void run() { dibuja(); } };
// Lista con todos los círculos que se dibujan en el // fondo animado private List circulos; // Variable que se usa para dibujar private Paint paint = new Paint(); // Largo y Ancho de la pantalla private int width; int height; // Indica si el fondo animado está visible para el usuario private boolean visible = true; // Nº máximo de círculos que muestra el fondo animado private int numeroMax; // Flag que indica si el fondo animado es sensible a los // toques en pantalla del usuario private boolean touchActivado;
U2 Multimedia y Gráficos en Android
// Constructor de la clase public MiWallpaperEngine() { // Accedemos a las preferencias del fondo animado SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(MiServicioWallpaper.this); // Recuperamos preferencias numeroMax = Integer .valueOf(prefs.getString(“numeroMaxCirculos”, “10”)); touchActivado = prefs.getBoolean(“touch”, false); // Definimos el listado de círculos mediante la clase // Punto (coordenada en la pantalla) circulos = new ArrayList(); // Indicamos a paint que suavice los bordes de todo // lo que dibuje paint.setAntiAlias(true); // Forma de unir las líneas (redondeada) paint.setStrokeJoin(Paint.Join.ROUND); // Indicamos al handler que ejecute el Runnable handler.post(drawRunnable); // Creamos un listener que notifica si hay un cambio // en las preferencias del liveWallpaper prefs.registerOnSharedPreferenceChangeListener(this); }
// Evento que aparece cuando el fondo animado cambia su // visibilidad @Override public void onVisibilityChanged(boolean visible) { // Guardamos el estado this.visible = visible; // Si está visible, dibujamos el fondo de pantalla if (visible) { handler.post(drawRunnable); // Si no lo está, paramos el proceso de dibujo } else { handler.removeCallbacks(drawRunnable); } } // Evento ejecutado cuando se destruye el fondo @Override public void onSurfaceDestroyed(SurfaceHolder holder) { super.onSurfaceDestroyed(holder); this.visible = false; // Paramos de dibujar el fondo animado handler.removeCallbacks(drawRunnable); } // Evento que se produce cuando cambia el fondo del dispositivo // como un cambio de orientación de éste @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Guardamos el ancho y largo de la pantalla this.width = width; this.height = height; super.onSurfaceChanged(holder, format, width, height);
169
Aula Mentor
170
} // Evento que se ejecuta cuando el usuario toca el fondo de // pantalla @Override public void onTouchEvent(MotionEvent event) { // Si está activado el toque en preferencias if (touchActivado) { // Obtenemos las coordenadas del toque float x = event.getX(); float y = event.getY(); // Obtenemos la referencia al SurfaceHolder donde se // ejecuta el fondo animado SurfaceHolder holder = getSurfaceHolder(); // Definimos un canvas donde vamos a dibujar los círculos Canvas canvas = null; try { // Obtenemos el canvas bloqueando el fondo SurfaceHolder // para que el usuario no pueda manipularlo canvas = holder.lockCanvas(); // Si lo hemos obtenido correctamente entonces... if (canvas != null) { // Definimos el fondo de color negro canvas.drawColor(Color.BLACK); // Si no podemos añadir 5 círculos borramos los 5 // primeros while (circulos.size()+5 > numeroMax) { circulos.remove(0); } // Añadimos nuevos círculos alrededor del punto de // toque circulos.add(new MiCirculo(x, y, colorAleatorio(), radioAleatorio())); circulos.add(new MiCirculo(x, y-30, colorAleatorio(), radioAleatorio())); circulos.add(new MiCirculo(x+30, y, colorAleatorio(), radioAleatorio())); circulos.add(new MiCirculo(x, y+30, colorAleatorio(), radioAleatorio())); circulos.add(new MiCirculo(x-30, y, colorAleatorio(), radioAleatorio())); // Dibujamos los círculos dibujaCirculos(canvas, circulos); } } finally { // ¡Muy importante! Hay que desbloquear el fondo SurfaceHolder if (canvas != null) holder.unlockCanvasAndPost(canvas); } super.onTouchEvent(event); } } // Método local que dibuja los círculos en el fondo private void dibuja() { // Obtenemos el fondo SurfaceHolder SurfaceHolder holder = getSurfaceHolder(); // Definimos un canvas donde vamos a dibujar los círculos
U2 Multimedia y Gráficos en Android
Canvas canvas = null; try { // Obtenemos el canvas bloqueando el fondo SurfaceHolder para // que el usuario no pueda manipularlo canvas = holder.lockCanvas(); // Si lo hemos obtenido correctamente entonces... if (canvas != null) { // Si hemos llegado al nº máximos de círculos // borramos el primero de todos para poder añadir otro if (circulos.size() == numeroMax) { circulos.remove(0); } // Añadimos el círculo int x = (int) (width * Math.random()); int y = (int) (height * Math.random()); circulos.add(new MiCirculo(x, y, colorAleatorio(), radioAleatorio())); // Finalmente, dibujamos los círculos dibujaCirculos(canvas, circulos); } } finally { // ¡Muy importante! Hay que desbloquear el fondo SurfaceHolder if (canvas != null) holder.unlockCanvasAndPost(canvas); } // Quitamos de la cola de ejecución el Runnable handler.removeCallbacks(drawRunnable); // Añadimos el Runnable a la cola de ejecución para // que se ejecute cada 3 segundos if (visible) { handler.postDelayed(drawRunnable, 3000); } } // Método que dibuja los círculos en un canvas private void dibujaCirculos(Canvas canvas, List circulos) { // Random que usamos para obtener color y radio // de los círculos aleatorios Random rnd = new Random(); // El fondo es negro canvas.drawColor(Color.BLACK); // Recorremos todos los puntos del listado for (MiPunto circulo : circulos) { // Establecemos el color paint.setColor(circulo.color); // Dibujamos un círculo en el canvas canvas.drawCircle(circulo.x, circulo.y, circulo.radio, paint); } } // Métodos que devuelven un color y un radio aleatorios para // dibujar los círculos private int colorAleatorio() { return Color.argb(255, (int) (256 * Math.random()), (int) (256 * Math.random()), (int) (256 * Math.random()));
171
Aula Mentor
} private int radioAleatorio() { return (int) (30 * Math.random()) + 5; } // Ocurre cuando hay un cambio en las preferencias del // liveWallpaper @Override public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { // Actualizamos las preferencias if (key.equals(“numeroMaxCirculos”)) numeroMax = Integer.valueOf(sharedPreferences.getString( “numeroMaxCirculos”, “10”)); else if (key.equals(“touch”)) touchActivado =sharedPreferences.getBoolean(“touch”,false); } } }
En el código anterior se ha nombrado mucho el objeto SurfaceHolder de Android. Esta clase es una Interfaz abstracta que permite controlar el tamaño, el formato, editar los píxeles y gestionar los cambios de una superficie, en este caso, es la superficie del fondo de pantalla de un dispositivo Android. Esta clase hereda propiedades y métodos de la clase SurfaceView de Android que hemos estudiado en profundidad en la Unidad 1. 172
Mediante el método getSurfaceHolder() obtenemos acceso al SurfaceHolder del fondo animado. Posteriormente utilizamos los métodos siguientes de esta clase: - holder.lockCanvas(): obtenemos el Canvas (lienzo donde vamos a dibujar) y bloqueamos el fondo SurfaceHolder para que el usuario no pueda manipularlo e intente dibujar infinitamente haciendo clic sobre el fondo de pantalla. Hay que tener en cuenta que cada vez que bloqueamos esta superficie es necesario dibujar de nuevo todos los elementos que la componen. - holder.unlockCanvasAndPost(): desbloquea el fondo SurfaceHolder.
¡Muy importante! Hay que desbloquear el fondo SurfaceHolder una vez hayamos acabado de dibujarlo. Si no lo hacemos, se quedará congelado y ya no será animado.
4.2.3 Interfaz de la Actividad de configuración del fondo animado: /res/xml/preferencias.xml
En este fichero establecemos las propiedades de la ventana de preferencias del fondo animado. En este ejemplo vamos a usar la gestión de preferencias de Android mediante la clase PreferenceScreen:
Con el código anterior definimos una ventana con un CheckBox y un campo de tipo texto que indica el número máximo de círculo que dibujará el fondo animado.
4.2.4 Actividad de configuración de las preferencias: PreferenciasActivity.java
En esta clase definimos el comportamiento de la ventana de preferencias. Como puedes observar hemos utilizado Fragmentos para implementar que estudiaremos en el siguiente apartado de teoría de esta Unidad. Las sentencias que forman el código fuente son auto explicativas: public class PreferenciasActivity extends PreferenceActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // A partir de la API 4.1 hay que usar PreferenceFragment getFragmentManager().beginTransaction().replace( android.R.id.content, new MiPreferenceFragment()).commit(); // Si no disponemos de ella, usaríamos esto: //addPreferencesFromResource(R.xml.preferencias); } // Definimos el fragmento de tipo Preference public static class MiPreferenceFragment extends PreferenceFragment { // Constructor de la clase @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Añadimos al fragmento el layout de preferencias addPreferencesFromResource(R.xml.preferencias); // Buscamos la preferencia numeroMaxCirculos Preference nCirculosPreference = findPreference(“numeroMaxCirculos”); // Asignamos listener que valida el formato correcto nCirculosPreference.setOnPreferenceChangeListener( compruebaNumeroListener); } // Comprobamos que la preferencia introducida es correcta; si // no, Android no la guarda automáticamente Preference.OnPreferenceChangeListener compruebaNumeroListener = new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { // Comprobamos que el nº introducido es un entero cuyo
173
Aula Mentor
// valor es > 5 if (newValue != null && newValue.toString().length() > 0 && newValue.toString().matches(“\\d*”) && Integer.valueOf(newValue.toString()) > 4) { return true; }
}
// Si no, mostramos un mensaje de error al usuario Toast.makeText(getActivity().getBaseContext(), “Error: no has introducido un número o el número es menor que 5”, Toast.LENGTH_SHORT).show(); return false; } }; } // end clase MiPreferenceFragment
4.2.5 Actividad principal del usuario: EstablecerWallpaperActivity.java
Aunque no es necesario definir esta Actividad de arranque de la aplicación, hemos decidido hacerlo para que el alumno vea cómo se puede utilizar un Intent para invocar el cambio de fondo de pantalla y que el usuario seleccione el que hemos desarrollado en esta aplicación. El código es muy sencillo: 174
// Actividad principal de la aplicación public class EstablecerWallpaperActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } // El usuario hace clic sobre el botón de la actividad principal public void onClick(View view) { // Creamos un nuevo Intent que llama al servicio de cambio de // fondo de pantalla del dispositivo Intent intent = new Intent( WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); // Indicamos que queremos activar el fondo animado de esta // aplicación intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, new ComponentName(this, MiServicioWallpaper.class)); // Iniciamos la nueva actividad con ese intent startActivity(intent); } }
4.2.6 Definición de la aplicación: AndroidManifest.xml
Finalmente, como toda aplicación Android, debemos establecer un servicio:
U2 Multimedia y Gráficos en Android
En la declaración anterior hemos establecido un servicio con el permiso android.permission. BIND_WALLPAPER y registrado el intent-filter android.service.wallpaper.WallpaperService. Al principio de este fichero puedes ver que aparece la propiedad:
Esto evita que el fondo animado se instale en dispositivos que no tienen hardware compatible. Además, hemos definido la Actividad de Preferencias y la principal de la aplicación.
175
Aula Mentor
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 4 (Live Wallpaper de curso Mentor) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos diseñado un Fondo de pantalla animado.
Si ejecutas en Eclipse ADT este Ejemplo 4, verás que se muestra el siguiente fondo animado en el Emulador:
176
Prueba a arrastrar el dedo sobre el mismo y verás cómo se actualiza el fondo de pantalla. En Internet, existen múltiples listados para fondos animados de pantalla: Veamos un conjunto de ejemplos según su funcionalidad: - Launchers: este tipo de fondos de pantalla permite lanzar aplicaciones y realizar múltiples acciones de forma rápida. Un ejemplo es Launcher Wall:
U2 Multimedia y Gráficos en Android
Acceso a Contactos: fondo que muestra los contactos de una forma atractiva y rápida con los que podremos interactuar. Un ejemplo es Contact Pro Live Wallpaper:
- Relojes: existen infinidad de fondos animados de este tipo. Vemos algunos:
WP Clock
Lightwell Live
Superclock
177
Aula Mentor
Analogy
178
TextClock
• Predicción del tiempo: existen múltiples fondos de este tipo como GoWeather que, además de ser un Widget estupendo, dispone también de un excelente Fondo Animado que cambia según el tiempo que hace.
U2 Multimedia y Gráficos en Android
- Notificaciones: muestran notificaciones como fondo de pantalla.
Bubbleator
Notification Bubbles
Aunque este listado pueda parecer muy completo, Google Play está lleno de fondos animados cada día que proporcionan las funcionalidades más inverosímiles. Animamos al alumno a que busque o, mejor, que desarrolle su propio fondo animado: Sirva la lista anterior de ejemplo para comenzar.
5. Fragmentos Cuando surgieron los dispositivos con pantallas de gran tamaño de tipo Tableta u ordenadores, Google tuvo que desarrollar la versión 3.0 de Android específicamente diseñada para solucionar el problema de la adaptación de la interfaz gráfica de las aplicaciones a ese nuevo tamaño de pantallas. Una interfaz de usuario diseñada para un teléfono móvil no se adaptaba fácilmente a una pantalla 5 ó 7 pulgadas. La solución fue implementar un nuevo tipo de componente denominado Fragment. Un Fragment no puede considerarse ni una Actividad, ni una Vista, es algo parecido a un contenedor. Un Fragment es un pedazo de la interfaz de usuario que se puede añadir o eliminar de la interfaz global de usuario de forma independiente al resto de elementos de la Actividad. Esto permite que pueda reutilizarse en otras Actividades. Así, podemos dividir la interfaz de usuario en varios segmentos o fragmentos; de ahí su nombre, de forma que podamos diseñar diversas configuraciones de pantalla, dependiendo del tamaño y orientación de ésta sin tener que duplicar código reutilizando los distintos fragmentos para cada una de las posibles configuraciones de pantalla. Por ejemplo, supongamos que estamos diseñando una aplicación de correo electrónico en la que queremos mostrar la lista de mensajes recibidos con los campos típicos “De” y “Asunto” y, por otro lado, mostrar el contenido completo del mensaje seleccionado. En un teléfono móvil, lo habitual es tener una primera Actividad que muestra el listado completo de los mensajes y, cuando el usuario selecciona uno de ellos, ejecutar una nueva Actividad que muestre el
179
Aula Mentor
contenido de dicho mensaje. Sin embargo, en una tableta puede existir espacio más que suficiente para tener ambas partes de la interfaz en la misma pantalla. Por ejemplo, en una tableta en posición apaisada podríamos tener una columna a la izquierda con el listado de mensaje y destinar la zona derecha a presentar el detalle del mensaje seleccionado, todo esto sin necesidad de cambiar de Actividad. En las versiones de Android anteriores a la 3.0 (sin Fragments) podríamos haber implementado diferentes Actividades con sus respectivos layouts para cada configuración de pantalla, pero esto nos hubiera obligado a duplicar gran parte del código fuente en cada Actividad. Si, en su lugar, utilizamos fragmentos, podríamos desarrollar un fragmento que muestra el listado de mensajes y otro fragmento que muestra el contenido del mensaje seleccionado, cada uno de ellos acompañado de su lógica de aplicación y definir únicamente varios layouts en función del tamaño de la pantalla que incluya los fragmentos necesarios en cada momento.
5.1 Cómo se implementan los Fragmentos
180
En el Ejemplo 5 de este Unidad vamos a simular la aplicación de correo que hemos descrito, adaptándola a tres configuraciones distintas de pantalla: normal, grande horizontal y grande vertical. En el primer caso, ubicaremos el listado de mensajes en una Actividad y, en otra Actividad, el detalle. Sin embargo, en el segundo y tercer caso estos dos elementos visuales estarán en la misma Actividad, ocupando la parte derecha/izquierda para la pantalla en el modo apaisado y la parte arriba/abajo en el modo vertical. Por lo tanto, en estos dós últimos casos hemos desarrollado dos fragmentos: uno para el listado de mensajes y el otro para mostrar su contenido. De la misma forma que una Actividad, un Fragmento se infla a partir de un fichero de layout XML para la interfaz visual (en la carpeta /res/layout) y una clase Java con las sentencias que incluyen sus funcionalidades. El primero de los Fragmentos creados contiene una Vista ListView con su Adaptador personalizado que muestra dos campos por fila: “De” y “Asunto”. Durante el curso de Iniciación ya estudiamos este tipo de elemento, por lo que aquí no daremos indicaciones sobre su uso. Si abrimos su layout XML fragmento_listado.xml vemos el siguiente contenido típico:
Como hemos comentado, si un fragmento incluye lógica, debe tener asociada su propia clase java que indique las sentencias que debe ejecutar y que se extiende de la clase Fragment.
U2 Multimedia y Gráficos en Android
Aunque los fragmentos están disponibles sólo a partir de la versión 3.0 de Android, sin embargo este sistema operativo permite utilizar esta característica en versiones anteriores incluyendo la librería de compatibilidad android-support en el proyecto:
Nota: esta librería se puede incluir manualmente en el proyecto mediante la opción “Add Support Library…” del menú contextual del proyecto:
181
Atención: debemos estar seguros de que la librería de compatibilidad aparece en el “Android SDK Manager” instalada correctamente.
Aula Mentor
Veamos dónde se comprueba que la librería está disponible en el entorno de desarrollo Eclipse ADT:
182
Usando este truco, podemos utilizar Fragmentos en la mayoría de versiones de Android, incluso desde la versión 1.6. A continuación, veamos el aspecto de la clase FragmentListado asociada al fragmento del listado: public class FragmentListado extends Fragment { // Matriz que simula un listado de mensajes private Mensaje[] datos = new Mensaje[]{ new Mensaje(“Pedro del Bosque”, “Petición de información”, “Hola.\n\n Por favor, remíteme la información lo antes posible.\n\n ¡Gracias!”), new Mensaje(“Alicia Navas”, “Hola!”, “Hola!\n\n Te espero en la fiesta.\n\n Besos!”), new Mensaje(“Daniel Fernández”, “Sobre viaje”, “¿Al final
U2 Multimedia y Gráficos en Android
vienes el fin de semana?\n\n Saludos”), new Mensaje(“Jaime del Monte”, “Más trabajo”, “Por favor, ponte en contacto conmigo.\n\n Gracias”)}; // ListView donde mostramos los mensajes private ListView listado; // Listener que detecta cuándo un usuario hace clic sobre un mensaje private MensajesListener listener; // Método equivalente al onCreate() de una Actividad @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Devolvemos el xml inflado que define el listado return inflater.inflate(R.layout.fragmento_listado, container, false); } // Método que se ejecuta cuando la Actividad // contenedora del fragmento está completamente creada // Aquí es donde debemos asociar el adaptador al listado @Override public void onActivityCreated(Bundle state) { super.onActivityCreated(state); // Buscamos el listado del layout listado = (ListView)getView().findViewById(R.id.listado); // Asociamos el adaptador listado.setAdapter(new AdaptadorMensajes(this)); // Definimos el evento onClic del listado listado.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView list, View view, int pos, long id) { if (listener!=null) { // Pasamos como parámetro la posición del elemento // seleccionado listener.onMensajeSeleccionado( (Mensaje)listado.getAdapter().getItem(pos)); } } }); // end onItemClick } // Clase que define el adaptador del Listado class AdaptadorMensajes extends ArrayAdapter { // Variable para guardar el contexto de la Actividad Activity contexto;
}
// Constructor del Adaptador AdaptadorMensajes(Fragment context) { // Usamos el layout correspondiente del listado super(context.getActivity(), R.layout.listitem_mensaje, datos); this.contexto = context.getActivity();
// Método que define la forma de dibujar de las opciones
183
Aula Mentor
public View getView(int position, View convertView, ViewGroup parent) { // Se infla el layout y se completa con información las Vistas // internas de la opción. // Atención: en el curso de Iniciación vimos que es // conveniente reutilizar las vistas dentro de un listado. // Por simplificación del código fuente no lo hemos hecho. LayoutInflater inflater = contexto.getLayoutInflater(); View item = inflater.inflate(R.layout.listitem_mensaje, null);
TextView lblDe = (TextView)item.findViewById(R.id.lblDe); lblDe.setText(datos[position].getDe());
TextView lblAsunto = TextView)item.findViewById(R.id.lblAsunto); lblAsunto.setText(datos[position].getAsunto());
return(item); } } // Definimos una interfaz que se implementará en la clase principal // de la aplicación y que tendrá en cuenta el tamaño de la pantalla // disponible public interface MensajesListener { void onMensajeSeleccionado(Mensaje mensaje); }
184
public void setMensajesListener(MensajesListener listener) { this.listener=listener; } }
El fichero Mensaje.java contiene una sencilla clase que almacena los campos De, Asunto y Texto de un mensaje. Puedes abrirla en Eclipse ADT y ver su contenido. Si analizamos con detenimiento la clase FragmentListado anterior es fácil advertir que existen muy pocas diferencias de cuando desarrollamos un listado de opciones con su adaptador personalizado en un Actividad normal. En este caso, las diferencias radican en los métodos que sobrescribimos. En el caso de los fragmentos normalmente son los siguientes: onCreateView() y onActivityCreated(). Veamos para qué se usan. - onCreateView(): es el método equivalente al onCreate() de una Actividad. Es decir, aquí inflamos el layout asociado al fragmento. - onActivityCreated(): método que se ejecuta cuando la Actividad contenedora del fragmento está creada por completo. En el ejemplo del curso, aprovechamos este método para obtener la referencia a la Vista ListView y asociarle su adaptador correspondiente. Ahora vamos a explicar el segundo fragmento que, como comentamos, se encarga de mostrar el detalle del mensaje seleccionado. La definición de este fragmento es más simple que la del anterior. Si accedemos a su layout XML fragmento_detalle.xml vemos el siguiente contenido:
Igualmente, la clase java DetalleActivity asociada se limita a inflar el layout de la interfaz. Además, se incluye el método público mostrarDetalle(), que utilizaremos para asignar el contenido a las Vistas que correspondan. Veamos su aspecto: // Fragmento que define el detalle del Mensaje public class FragmentDetalle extends Fragment { // Inflamos la Vista @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragmento_detalle, container, false); }
// Definimos el método que asigna el texto del cuerpo del mensaje public void mostrarDetalle(String texto) { TextView cuerpoMensaj = (TextView)getView().findViewById(R.id.cuerpoMensaj); cuerpoMensaj.setText(texto); } }
Una vez definidos los dos fragmentos, hay que desarrollar las Actividades de la aplicación, con sus respectivos layouts que utilizan los fragmentos anteriores. Para la Actividad principal hemos diseñado tres layouts diferentes: - un layout cuando la aplicación se ejecute en una pantalla de tamaño normal, como un teléfono móvil. Se encuentra en la carpeta por defecto /res/layout. - dos layouts para pantallas mayores (uno para orientación apaisada y otro para la vertical). Se hallan en las carpetas /res/layout-large (pantalla grande con orientación apaisada) y / res/layout-large-port (pantalla grande con orientación vertical) respectivamente. Notamos que todos estos layouts se denominan con el mismo nombre activity_main.xml; sin embargo, su uso lo marca la carpeta donde colocamos cada uno. Por lo tanto, Android elegirá automáticamente el layout en función del tamaño y orientación de la pantalla.
Atención: la terminación “–port” del directorio layout de pantalla grande con orientación vertical se debe a la palabra portrait (del inglés, retrato).
185
Aula Mentor
Para el caso de pantalla normal, la Actividad principal mostrará sólo el listado de mensajes; por lo tanto, el layout incluirá únicamente el fragmento FragmentListado:
Para incluir un fragmento en un layout, utilizamos la etiqueta asignando el atributo class a la ruta completa de la clase java correspondiente al fragmento. En este caso de pantalla normal, la vista de detalle se muestra en una segunda Actividad cuyo layout se denomina activity_detalle.xml:
Se trata de un layout análogo al anterior, pero indicamos el fragmento de detalle. En el caso de pantalla grande apaisada el layout tiene el siguiente aspecto en el directorio layout-large: 186
En este caso, al haber mucha pantalla disponible, incluimos los dos fragmentos dentro de un LinearLayout horizontal, asignando al primer fragmento un peso (propiedad layout_weight) de 30 y al segundo de 70 para que la columna izquierda con el listado de mensajes ocupe un 30% de la pantalla y la de detalle ocupe el resto. Por último, para el caso de la pantalla grande vertical (directorio layout-large-port) utilizamos un LinearLayout vertical.
Ahora podemos ejecutar la aplicación en el emulador y comprobar que se selecciona automáticamente el layout correcto dependiendo de las características del AVD (Dispositivo Virtual de Android) que estemos utilizando.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 5 (Mensajes) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado Fragmentos. 187 Para poder ver las diferencias debes crear dos AVDs, uno con pantalla normal y otro grande. Para cambiar la orientación de la pantalla de un dispositivo virtual debes usar el atajo de teclado [Ctrl+F12]. Si ejecutas en Eclipse ADT este ejemplo, verás que se muestra el siguiente fondo animado en el Emulador: Pantalla normal (dispositivo del curso):
Aula Mentor
Pantalla grande vertical (Nexus 7 de 7.3 pulgadas):
188 Pantalla grande apaisada (Nexus 7 de 7.3 pulgadas):
U2 Multimedia y Gráficos en Android
Como vemos en las imágenes anteriores, la interfaz se adapta perfectamente a la pantalla en cada caso mostrando uno o dos fragmentos y distribuyéndolos horizontal o verticalmente según el espacio disponible en la pantalla. Todavía no hemos descrito la lógica de la aplicación, es decir, lo que sucede cuando el usuario selecciona un mensaje del listado. Para ello, en la clase FragmentListado en el método onActivityCreated() asignamos el evento onItemClick() de la lista de mensajes teniendo en cuenta que debemos tener en cuenta el tamaño de la pantalla, es decir, si se visualiza el fragmento de detalle: - Si el fragmento de la clase FragmentDetalle de detalle está visible, hay que obtener su referencia e invocar su método mostrarDetalle() que mostrará el texto del mensaje seleccionado. - En caso contrario, hay que iniciar la Actividad secundaria DetalleActivity para que se muestre el detalle del mensaje. No obstante, hemos comentado anteriormente que un fragmento se caracteriza por ser una porción completa de interfaz reutilizable en distintas partes del código fuente y diseñada de forma independiente del resto de la interfaz de usuario. Es decir, a priori, no tiene relación con ningún otro fragmento de la misma aplicación. Teniendo en cuenta esta característica, no es recomendable tratar el evento en el propio fragmento, sino definir y lanzar un evento personalizado cuando el usuario selecciona un mensaje de la lista y delegar la lógica del evento en la Actividad contenedora, ya que ésta sí conoce qué fragmentos componen su interfaz. Para hacer esto que parece tan complejo, primero definimos un evento personalizado en la clase FragmentListado para la Vista de listado mediante el método onMensajeSeleccionado() del listener MensajesListener. Hemos declarado en esta clase el atributo onMensajeSeleccionado() de tipo interfaz y definimos su método setMensajesListener(). Este código permite asignar el evento desde fuera de la propia clase. Veamos cómo queda: public class FragmentListado extends Fragment { //... // Listener que detecta cuándo un usuario hace clic sobre un mensaje private MensajesListener listener; //.. @Override public void onActivityCreated(Bundle state) { super.onActivityCreated(state); // Buscamos el listado del layout listado = (ListView)getView().findViewById(R.id.listado); // Asociamos el adaptador listado.setAdapter(new AdaptadorMensajes(this)); // Definimos el evento onClic del listado listado.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView list, View view, int pos, long id) { if (listener!=null) { // Lanzamos el evento onMensajeSeleccionado y le pasamos // como parámetro la posición del elemento // seleccionado
189
Aula Mentor
listener.onMensajeSeleccionado( (Mensaje)listado.getAdapter().getItem(pos)); } } }); // end onItemClick } // Definimos una interfaz que se implementará en la clase principal // de la aplicación y que tendrá en cuenta el tamaño de la pantalla // disponible public interface MensajesListener { void onMensajeSeleccionado(Mensaje mensaje); } public void setMensajesListener(MensajesListener listener) { this.listener=listener; } }
190
Como vemos, una vez definido este truco, debemos en el evento onItemClick() de la lista lanzar nuestro evento personalizado onMensajeSeleccionado() pasándole como parámetro el contenido del mensaje, que hemos obtenido mediante el adaptador con el método getAdapter() y recuperamos el elemento con getItem(). Ahora ya tenemos todas las piezas del puzle que debemos encajar en la clase java MainActivity que define la Actividad principal. Para ello, en su método onCreate(), obtendremos una referencia al fragmento de listado mediante el método getFragmentById() del gestor de fragmentos (Fragment Manager es el componente del sistema operativo encargado de gestionar los fragmentos de una aplicación) y le asignaremos el evento mediante su método setMensajesListener() que acabamos de definir anteriormente: // Clase principal que contiene los fragmentos e implementa la // interfaz Mensajes Listener public class MainActivity extends FragmentActivity implements MensajesListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Definimos el fragmento del listado mediante // referencia a su id FragmentListado frgListado =(FragmentListado)getSupportFragmentManager() .findFragmentById(R.id.FrgListado); // Establecemos el listener del listado frgListado.setMensajesListener(this); } // Se implementa el método onMensajeSeleccionado del listener // MensajesListener @Override public void onMensajeSeleccionado(Mensaje mensaje) { // Buscamos si Android está mostrando el detalle del mensaje // buscando si está definido el fragmento de detalle
U2 Multimedia y Gráficos en Android
boolean hayDetalle = (getSupportFragmentManager().findFragmentById (R.id.FrgDetalle) != null); // Si se ven los dos fragmentos entonces cuando el usuario haga // clic en un mensaje se muestra su detalle if(hayDetalle) { ((FragmentDetalle)getSupportFragmentManager() .findFragmentById(R.id.FrgDetalle)).mostrarDetalle( mensaje.getTexto()); } // Si no existe el fragmento de detalle lanzamos la actividad de // detalle con un Intent pasando el texto como una propiedad en // éste else { Intent i = new Intent(this, DetalleActivity.class); i.putExtra(DetalleActivity.EXTRA_TEXTO, mensaje.getTexto()); startActivity(i); } } }
Se puede observar en el código anterior que esta Actividad principal implementa la interfaz MensajesListener, por lo que nos basta indicar this al método setMensajesListener(). Además, esta Actividad no se hereda de la clase Activity como suele ser habitual, sino de FragmentActivity ya que se utiliza la librería de compatibilidad android-support para utilizar fragmentos conservando la compatibilidad con versiones de Android anteriores a la 3.0. En caso de no necesitar esta compatibilidad se puede seguir heredando de Activity sin inconvenientes. Finalmente, si prestamos atención al método onMensajeSeleccionado(), vemos que se ejecutará cada vez que el fragmento del listado indique que se ha seleccionado un determinado mensaje de la lista y aplicará la lógica ya mencionada, es decir, si en la pantalla existe el fragmento de detalle simplemente lo actualizaremos mediante su método mostrarDetalle() y, en caso contrario, abriremos la nueva Actividad DetalleActivity utilizando un nuevo Intent con la referencia a dicha clase y añadiendo como propiedad un campo extra de texto con el contenido del mensaje seleccionado. Acabamos iniciando la Actividad con el método startActivity(). El código de la segunda Actividad DetalleActivity se limita a recuperar esta propiedad extra pasada en el Intent y mostrarla en el fragmento de detalle mediante su método mostrarDetalle(): public class DetalleActivity extends FragmentActivity { // Definimos una constante que usaremos en Intent para pasar la // información al fragmento public static final String EXTRA_TEXTO = “es.mentor.unidad2.eje5.fragmentos.EXTRA_TEXTO”; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_detalle); // Buscamos el fragmento de detalle que es un fragmento FragmentDetalle detalle =
191
Aula Mentor
(FragmentDetalle)getSupportFragmentManager() .findFragmentById(R.id.FrgDetalle); // Cargamos el texto del detalle en este fragmento detalle.mostrarDetalle(getIntent().getStringExtra(EXTRA_TEXTO)); } }
Animamos al alumno o alumna a que vuelva a probar la aplicación en Eclipse ADT para comprobar el correcto funcionamiento de la selección de mensajes en las distintas configuraciones de pantalla. Una vez hemos visto cómo crear y utilizar un fragmento, vamos a estudiar algunas de sus funcionalidades avanzadas que pueden interesar al programador.
5.2 Ciclo de vida de un Fragmento De igual forma que ocurre con una Actividad en Android, un Fragmento dispone de su propio ciclo de Vida. A continuación, se muestra el ciclo de vida de un Fragmento en función del estado de la Actividad que lo contiene:
192
U2 Multimedia y Gráficos en Android
Como puedes ver, el ciclo de vida de un Fragmento está muy relacionado con el ciclo de vida de la Actividad que lo contiene. Cada invocación que hace el sistema operativo de un evento de una Actividad provoca que se invoquen a su vez eventos de los Fragmentos que contiene. Por ejemplo, cuando la Actividad recibe el evento onPause(), todos sus fragmentos reciben también el mismo evento onPause(). Además, los Fragmentos disponen de algunos eventos más en su ciclo de vida: - onAttach(Activity): Android lo invoca cuando el fragmento se asocia con la Actividad. - onCreateView(LayoutInflater, ViewGroup, Bundle): Android lo invoca cuando hay que crear las Vistas internas del fragmento. - onActivityCreated(Bundle): Android lo invoca cuando la Actividad ya está creada. - onDestroyView(): Android lo invoca cuando se van a destruir las Vistas del fragmento. - onDetach(): Android lo invoca cuando el fragmento se desliga de la Actividad. El resto de eventos se usan de la misma forma que en una Actividad.
Atención: para sobreescribir cualquier método del ciclo de vida (con excepción del método onCreateView()) debemos siempre llamar al método de la clase superior (mediante super.nombreMetodo()). Si no lo hacemos, al ejecutar la aplicación veremos un error de ejecución.
5.2.1 Cómo guardar el estado de un Fragmento De igual forma que ocurre con una Actividad en Android, un Fragmento dispone del método onSaveInstanceState(Bundle) que permite al programador guardar el estado actual de las Vistas de éste. Sin embargo, no tiene el método onRestoreInstanceState(). Veamos cómo gestionar el estado de un Fragmento:
- Por defecto, Android almacena el estado de todas las Vistas de un fragmento que tengan asociado un ID, es decir, su contenido o características se pueden modificar. - Es posible sobreescribir el método onSaveInstanceState(Bundle) para añadir información adicional del fragmento. - Cuando, por ejemplo, el usuario vuelve a abrir una aplicación y el sistema operativo crea el fragmento, se utiliza el parámetro Bundle, guardado con el método anterior, como parámetro en los métodos onCreate(), onCreateView() y onActivityCreated() para que el programador devuelva el Fragmento al estado almacenado.
Como puedes ver, la forma de guardar el estado de un Fragmento es similar a la de guardar una Actividad.
5.2.2 Cómo mantener los Fragmentos cuando la Actividad se recrea automáticamente Como sabes, por defecto, cuando ocurre un cambio dinámico y automático de la configuración del dispositivo (por ejemplo, un cambio de orientación de la pantalla de éste), la Actividad de una aplicación Android se recrea (se destruye y se vuelve a crear). Esto le sucede también a los Fragmentos que contenga dicha Actividad, que se destruyen y se vuelven a recrear auto-
193
Aula Mentor
máticamente. Sin embargo, la clase Fragment dispone del método setRetainInstance(boolean) que permite al programador indicar con el parámetro true que se desea no destruir la instancia del Fragmento cuando esto ocurra. Podemos invocar este método en el evento onCreate() del Fragmento. Examinemos sus características y cómo se utiliza. Si indicamos con la sentencia setRetainInstance(true) que deseamos que Android no destruya el Fragmento en los cambios de configuración automáticos, ocurre lo siguiente: - Los métodos onDestroy() y onCreate() no se invocarán de nuevo cuando se produzca la recreación de la Actividad. - El resto de eventos del ciclo de vida de un Fragmento se siguen invocando igualmente y en la misma secuencia. - El parámetro Bundle de los métodos onCreateView() y onActivityCreated() en nulo (null) porque el Fragmento no se recrea.
5.2.3 Cómo buscar Fragmentos
194
Como hemos comentado anteriormente, el componente del sistema operativo encargado de gestionar los fragmentos de una aplicación es FragmentManager. Este gestor de fragmentos dispone de los siguientes métodos para encontrar un fragmento incluido en la Actividad: - findFragmentById(int id): encuentra un fragmento por el ID especificado como parámetro. - findFragmentByTag(String tag): encuentra un fragmento por el tag indicado como parámetro. Un tag es una etiqueta que podemos asociar para distinguir distintos fragmentos que tengan el mismo ID, es decir, el mismo layout. Ambos métodos devuelven una referencia al fragmento o null si no existe dentro de la Actividad.
5.2.4 Otras operaciones sobre Fragmentos (Transacciones) Es posible llevar a cabo otras operaciones dinámicas con Fragmentos en tiempo de ejecución que modifican la interfaz del usuario, tales como, mostrar u ocultar un fragmento. Android denomina transacción (transaction) a un cambio de este tipo. Estudiemos qué tipo de operaciones podemos realizar durante una transacción: - add(): añade un Fragmento a la Actividad. - remove(): quita un fragmento de la actividad. Esta operación destruye el fragmento salvo que utilicemos la pila de transacciones que estudiaremos más adelante en este apartado. - replace(): sustituye un fragmento por otro. - hide(): oculta un fragmento de la interfaz de usuario sin necesidad de destruirlo, pasando el fragmento a ser no visible. - show(): muestra un fragmento que no está visible. - detach() : separa un fragmento de la interfaz de usuario destruyendo sus vistas internas pero manteniendo su instancia. - attach(): une de nuevo un fragmento separado a la interfaz de usuario recreando sus vistas internas.
U2 Multimedia y Gráficos en Android
No es posible utilizar los métodos remove(), replace(), detach() y attach() en un fragmento estático utilizado directamente en el layout de una Actividad.
Veamos ahora los pasos para realizar transacciones con Fragmentos: - Mediante el método FragmentManager.beginTransaction() obtenemos una instancia a la clase FragmentTransaction que va a gestionar toda la transacción. - Llevamos a cabo las operaciones de movimiento de fragmentos que deseemos utilizando la instancia de la clase FragmentTransaction. Todas estas operaciones devuelven de nuevo la instancia original modificada de forma que es posible encadenar operaciones en una única sentencia. - Invocamos el método commit() para que los cambios en la Actividad se hagan efectivos.
Sólo es posible utilizar el método commit() mientras la Actividad (y, por lo tanto, el Fragmento) no se encuentre guardando su estado con el método onSaveInstanceState. La sentencia commit mostrará una excepción si la ejecutamos después de haber guardado el estado de la Actividad.
Veamos un ejemplo de código fuente:
FragmentManager fragmentManager = getFragmentManager() // Si usamos la librería de compatibilidad también podemos escribir: // FragmentManager fragmentManager = getSupportFragmentManager() fragmentManager.beginTransaction() .remove(fragmento1) .add(R.id.fragmento_layout, fragmento2) .show(fragmento3) .hide(fragmento4) .commit();
Como puedes observar en el código anterior, hemos encadenado operaciones en una única sentencia. Es muy importante no olvidarse nunca de la orden commit(), ya que si no la usamos, no se realizan los cambios. Además, debes saber que el orden en que realices las operaciones determina el orden en que Android las realizará en la interfaz del usuario.
5.2.5 Cómo Gestionar la pila (Back Stack) de Fragmentos En el curso de iniciación de Android vimos que el sistema operativo organiza las Actividades en una pila de ejecución (en inglés stack) donde se van apilando las actividades que el usuario va invocando. De esta forma, el usuario puede moverse en esta pila utilizando la tecla retroceso del dispositivo . De forma similar, Android también dispone de una pila de ejecución llamada back stack para almacenar los Fragmentos. Si se añade mediante una transacción un fragmento a la pila, entonces el usuario puede ir al fragmento anterior mediante la tecla retroceso del dispositivo. Cuando el usuario llega al
195
Aula Mentor
primer fragmento de la pila, si pulsa de nuevo la tecla de retroceso, entonces se destruye la Actividad. Para añadir una transacción a la pila debemos utilizar el método FragmentTransaction.addToBackStack(String) antes de invocar el método commit(). El argumento de tipo String es un nombre opcional para identificar el estado de la pila que supone ese cambio; por ejemplo, podemos indica el texto “pantalla_inicial”, para luego poder volver fácilmente a ese estado. Lo más frecuente es que tenga el valor null. Además, la clase FragmentManager dispone del método popBackStack(String), que permite al programador volver al estado anterior si no se indica parámetro o al estado marcado por este parámetro (por ejemplo “pantalla_inicial”). Si se añaden multiples cambios en una transacción a una pila de ejecución y aplicamos el commit() al final, entonces estamos añadiendo una única transacción, y si el usuario pulsa la tecla volver del dispositivo, se desharán todos estos cambios de una sola vez. Si quitamos o reemplazamos un fragmento y, después, llamamos el método addToBackStack(), el sistema operativo invocará los métodos onPause(), onStop() y onDestroyView() del fragmento cuando los añada a la pila de ejecución. Cuando el usuario vuelva a este fragmento, entonces el sistema invocará los métodos onCreateView(), onActivityCreated(), onStart()y onResume()del fragmento. A continuación, se muestra un ejemplo que añade a la pila de ejecución un nuevo fragmento:
196
// Definimos un nuevo fragmento Fragment nuevoFragment = new Fragment(); FragmentTransaction ft = getFragmentManager().beginTransaction(); // Añadimos una animación de entrada y salida del fragmento ft.setCustomAnimations(R.animator.fragment_slide_left_enter, R.animator.fragment_slide_left_exit, R.animator.fragment_slide_right_enter, R.animator.fragment_slide_right_exit); // Reemplazamos un fragment por otro ft.replace(R.id.simple_fragment, nuevoFragment); // Añadimos a la pila la transacción ft.addToBackStack(null); ft.commit();
Hemos aprovechado este ejemplo para incluir el método setCustomAnimations(param1, param2, param3, param4) de la clase FragmentTransaction que permite añadir una ani-
mación cuando se muestra u oculta un fragmento según estos parámetros: - Parámetro 1: ocurre cuando aparece el fragmento. En este caso hemos indicado R.animator. fragment_slide_left_enter, que desliza el fragmento desde la parte izquierda de la pantalla hasta mostrarlo. - Parámetro 2: ocurre cuando desaparece el fragmento. En este caso hemos indicado R.animator.fragment_slide_left_exit, que desliza el fragmento desde la pantalla hacia la izquierda hasta que desaparece. - Parámetro 3: ocurre cuando aparece el fragmento porque lo añadimos a la pila de ejecución. En este caso hemos indicado R.animator.fragment_slide_right_enter, que desliza el fragmento desde la parte izquierda de la pantalla. - Parámetro 4: ocurre cuando desaparece el fragmento porque se ha quitado de la pila de ejecución (por ejemplo, el usuario ha pulsado la tecla volver del dispositivo). En este caso hemos indicado R.animator.fragment_slide_right_exit, que desliza el fragmento desde la parte izquierda de la pantalla.
U2 Multimedia y Gráficos en Android
5.2.6 Cómo utilizar Fragmentos sin layout Como hemos indicado, no es obligatorio que un Fragmento tenga asociado una interfaz de usuario, es decir, un layout. Por ejemplo, un Fragmento se puede utilizar para mantener información de estado o gestionar hilos (threads). Por lo tanto, no necesita disponer de interfaz de usuario y no es necesario sobreescribir el método onCreateView(). Es decir, mediante un fragmento podemos ejecutar sentencias en segundo plano. Para añadir un fragmento a la Actividad debemos usar el método FragmentTransaction.add(Fragment, String), donde el segundo parámetro indica una etiqueta (tag) para identificar el fragmento. Fíjate en el siguiente ejemplo de código fuente: FragmentManager fragmentManager = getFragmentManager(); // También podemos escribir: FragmentManager fragmentManager = // getSupportFragmentManager() FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); BackgroundFragment fragmento = new BackgroundFragment(); fragmentTransaction.add(fragmento, “thread_manager”); fragmentTransaction.commit();
5.2.6.1 Comunicación entre Fragmentos y con la Actividad Como sabes, un fragmento está directamente ligado a la Actividad que lo contiene. Por lo tanto, es posible una comunicación entre ambos: - La Actividad puede llamar a todos los métodos públicos del fragmento mediante una referencia al objeto del fragmento. Si no se guarda la referencia del fragmento una vez creado, es posible utilizar los métodos findFragmentById() o findFragmentByTag() de la clase FragmentManager como hemos visto anteriormente. - El fragmento puede accede a la Actividad que lo contiene a través del método Fragment. getActivity(). Por ejemplo, es posible obtener una referencia a una Vista de la Actividad principal así: View listView = getActivity().findViewById(R.id.lista);
Atención: La clase Fragment no deriva de la clase Context. Por lo tanto, si el fragmento necesita obtener la referencia al contexto de la Actividad (por ejemplo, para mostrar un mensaje Toast) podemos invocar el método getApplicationContext() de la Actividad que contiene el fragmento.
Desde el punto de vista de programación es importante evitar que las Actividades y Fragmentos sean muy interdependientes entre sí ya que esto reduce las posibilidades de poder reusar el fragmento en otra Actividad o aplicación. Lo recomendado es que sea la Actividad la que interaccione con el Fragmento. Para conseguir este objetivo es recomendable definir interfaces y listerners en el fragmento que luego implementará la clase que contiene la Actividad. En el Ejemplo 5 de esta Unidad hemos utilizado este método que recordamos:
197
Aula Mentor
Código fuente del Fragmento: public class FragmentListado extends Fragment { ... // Listener personalizado que detecta cuándo un usuario hace clic // sobre un mensaje private MensajesListener listener; ... // Definimos una interfaz que se implementará en la clase principal // de la aplicación y que tendrá en cuenta el tamaño de la pantalla // disponible public interface MensajesListener { void onMensajeSeleccionado(Mensaje mensaje); } public void setMensajesListener(MensajesListener listener) { this.listener=listener; } }
Código fuente de la Actividad:
198
// Clase principal que contiene los fragmentos e implementa la // interfaz Mensajes Listener public class MainActivity extends FragmentActivity implements MensajesListener { ... // Se implementa el método onMensajeSeleccionado del listener // MensajesListener @Override public void onMensajeSeleccionado(Mensaje mensaje) { ... } ... }
5.2.7 Recomendaciones a la hora de programar Fragmentos - Diseña la aplicación para que la Actividad sea el componente intermediario entre los fragmentos, ya que a éstos últimos no es posible asociarles intenciones, es decir, un Intent no puede llamar directamente a un fragmento. - Todos los fragmentos deben tener definido un constructor por defecto. - Los fragmentoss generalmente no deben implementar constructores adicionales o sobreescribir el ya existente de la clase heredada. - El primer método de un fragmento donde podemos escribir una sentencia para que la ejecute Android es el método onAttach(). - Una vez se ha creado un fragmento, es posible inicializarlo desde la Actividad mediante parámetros que se pasan en su método setArguments(Bundle). Fíjate en el siguiente ejemplo: Código fuente de la Actividad: // Creamos los parámetros con el nombre ID y CAMPO
U2 Multimedia y Gráficos en Android
Bundle arguments = new Bundle(); arguments.putInt(“ID”, 1); arguments.putString(“CAMPO”, “Texto del campo”); // Instanciamos el fragmento mFragment = Fragment.instantiate(mActivity, FragmentoClass.getName()); mFragment.setArguments(arguments); // Añadimos a la Actividad ft.add(android.R.id.content, mFragment); En el código anterior hemos utilizado el método instaciate de la clase Fragment que es equivalente a escribir: FragmentoClass mFragment = new FragmentoClass();
En el fragmento podemos usar ahora estos parámetros como deseemos a través del método getArguments(); por ejemplo, en el método onActivityCreated: @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (getArguments().containsKey(“ID”)) { ... } } ...
5.2.8 Implementar diálogos con Fragmentos A partir de la version 3.0 de Android, las clásicas ventanas de diálogo (clase Dialog) están en desuso (en inglés, se denomina deprecated) en favor de los fragmentos. Este cambio tiene mucho sentido si pensamos que las ventanas de dialogo son componentes que se reutilizan mucho. La clase DialogFragment de Android define la clase básica que implementa un fragmento que muestra una ventana de diálogo flotante en una Actividad. Este fragmento incluye un objeto Dialog que contiene el diseño de la ventana de diálogo. La aplicación debe gestionar la ventana de diálogo de tipo fragmento mediante los métodos disponibles en en la clase DialogFragment.
Es posible implementar una clase DialogFragment de forma que se puede utilizar únicamente como una ventana de diálogo o como un fragmento “normal” dentro de una Actividad.
Si sólo vas a utilizar el fragmento como una ventana de diálogo, debemos sobreescribir el método onCreateDialog() devolviendo una instancia de la clase Dialog o de sus subclases. Si vas a emplear el nuevo fragmento como diálogo y como fragmento propiamente dicho dentro de una Activiad, debes sobreescribir el método onCreateView() y devolver la Vista creada. La clase DialogFragment dispone del método sobrecargado (overload) show() que
199
Aula Mentor
muestra la ventana de diálogo como una transacción, es decir, crea una transacción en el objeto FragmentManager, añade el fragmento y ejecuta commit(). Cuando se cierra la ventana de diálogo, se crea otra transacción para quitar el fragmento de la ACtividad. Como no podía ser de otra forma, la clase DialogFragment dispone del método dismiss() para cerrar el fragmento explícitamente. De igual forma, este método se ejecuta mediante transaciones. A continuación, vamos a ver un sencillo ejemplo de ventana de diálogo, utilizando fragmentos, que pide confirmación para realizar cualquier operación. Código fuente del fragmento: public class ConfirmacionDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { // Como hemos dicho, la Actividad debe gestionar el listener private ConfirmacionDialogFragmentListener listener; // Contructor: ventana estática de diálogo public static ConfirmacionDialogFragment newInstance(String titulo){ ConfirmacionDialogFragment frag = new ConfirmacionDialogFragment(); // Leemos los argumentos que pasa la Actividad que lo contiene Bundle args = new Bundle(); args.putString(“titulo”, titulo); frag.setArguments(args); return frag; }
200
// Definimos el listener con los 2 métodos de callback que debe // definir la Actividad public interface ConfirmacionDialogFragmentListener { public void onPositiveClick(); public void onNegativeClick(); }
// Método para establecer el listener anterior desde la Actividad public void setConfirmacionDialogFragmentListener( ConfirmacionDialogFragmentListener listener) { this.listener = listener; } // Evento que se lanza cuando se crea la ventana de diálogo @Override public Dialog onCreateDialog(Bundle savedInstanceState) { String titulo = getArguments().getString(“titulo”); // Creamos el típico diálogo del tipo AlertDialog con el título return new AlertDialog.Builder(getActivity()) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(titulo) .setPositiveButton(android.R.string.ok, this) .setNegativeButton(android.R.string.cancel, this) .create(); } // Evento que sucede cuando el usuario hace click en un botón ya que // se está implementando el método DialogInterface.OnClickListener @Override
U2 Multimedia y Gráficos en Android
public void onClick(DialogInterface dialog, int which) { if (listener != null) { switch (which) { case DialogInterface.BUTTON_POSITIVE: listener.onPositiveClick(); default: listener.onNegativeClick(); } } } }
Código fuente de la Actividad: public class SimpleConfirmacionDialogFragmentActivity // Se debe extender de la clase FragmentActitity extends FragmentActivity // Implementa ConfirmacionDialogFragmentListener y OnClickListener // del botón principal de esta Actividad implements OnClickListener, ConfirmacionDialogFragmentListener { // Contructor típico de la Actividad @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Definimos el botón boton_dialog en el layout de la Actividad Button botonMuestraDialog = (Button) findViewById(R.id.boton_dialog); // Asignamos el evento onClick de este botón botonMuestraDialog.setOnClickListener(this); }
// Definimos el evento onClick del botón botón_dialog @Override public void onClick(View v) { // Creamos el fragmento ConfirmacionDialogFragment confirmacionDialog = ConfirmacionDialogFragment.newInstance(“Título”); // Definimos los listener confirmacionDialog.setConfirmacionDialogFragmentListener(this); // Mostramos la ventana de diálogo confirmacionDialog.show(getSupportFragmentManager(), null); } // Definimos la implementación de los métodos onPositiveClick y // onNegativeClick mostrando un mensaje de tipo Toast @Override public void onPositiveClick() { Toast.makeText(this, android.R.string.ok, Toast.LENGTH_LONG).show(); } @Override public void onNegativeClick() {
201
Aula Mentor
Toast.makeText(this, android.R.string.cancel, Toast.LENGTH_LONG).show(); }
}
5.2.9 Otras clases de Fragmentos Existen en Android subclases adicionales extendidas de Fragment desarrolladas para usos muy comunes y que facilitan el trabajo al programador. Veamos las más importantes: - ListFragment: fragmento que proporciona una lista del tipo ListView. Es análogo a la clase ListActivity. - WebViewFragment: fragmento que crea y gestiona automáticamente la clase WebView para presentar un mini navegador de Internet.
5.3 Barra de Acción (Action Bar) La Barra de Acción de Android o, del inglés, Action Bar es, como su propio nombre indica, una barra que aparece en la parte superior de una aplicación. Puede incluir un icono, el título de la Actividad y varios botones de acción o un menú extendido desplegable (en inglés, se denomina overflow). Este menú extendido, a su vez, incluye más acciones cuando éstas no caben como botones en el espacio disponible de la pantalla del dispositivo o el programador decide por motivos de diseño que se deben ocultar de la barra. 202
Vemos un ejemplo visual de Barra de acción:
Icono y Título
Botones
Menú Extendido
Desgraciadamente, la Barra de acción de Android no está incluida en la librería de compatibilidad android-support que hemos utilizado más arriba. Es decir, sólo funciona a partir de la versión Android 3.0 de forma nativa. Sin embargo, Google no se ha olvidado del todo de las versiones anteriores de Android y propone una alternativa para compatibilizar las aplicaciones con versiones anteriores a la 3.0 mediante un ejemplo que incluye en el SDK llamado ActionBarCompat. Puedes encontrar su código fuente en la siguiente carpeta de la instalación del SDK: \samples\ android-XX\ActionBarCompat. Además, a modo de información, indicamos que existe una librería muy utilizada entre los programadores llamada ActionBarSherlock que proporciona al desarrollador una implementación alternativa de este componente y es compatible con Android 2.0 o superior. Aunque, como hemos dicho, es posible utilizar esta funcionalidad en versiones de Android anteriores a la 3.0, en este apartado vamos a mostrar el uso de la funcionalidad nativa de la Barra de acción de Android. Cuando generamos un proyecto nuevo con una versión de Android 3.0 o superior, Eclipse ADT añade automáticamente por defecto a la nueva aplicación su barra de acción correspon-
U2 Multimedia y Gráficos en Android
diente. Si a continuación, lo ejecutamos directamente sobre un AVD veremos que ya incluye la barra de acción con el menú de acción “Settings” según muestra la siguiente imagen:
A continuación, vamos a estudiar cómo se implementa una barra de acción en un proyecto Android. En primer lugar, lo usual es que una barra muestre el icono de la aplicación (definido en el fichero AndroidManifest mediante el atributo android:icon del elemento ) y el título de la Actividad actual (definido también en el archivo AndroidManifest mediante el atributo android:label del elemento ). Los botones y los menús de acción se definen de la misma forma que los típicos menús de una aplicación de Android; de hecho, se usa la misma implementación. Es decir, el programador define el menú de la aplicación y es el sistema operativo el que decide cómo lo muestra: como un menú clásico para las versiones 2.X o anteriores (no se visualiza la barra de acción al no ser compatible) o como una barra de acción si se ejecuta en Android 3.0 o superior. De esta forma, Android mantiene la compatibilidad del menú con todas sus versiones, aunque dependiendo de la versión muestra la barra de acción o, en su defecto, el clásico menú de aplicación. Aunque en el curso de Iniciación de Android ya estudiamos cómo definir los Menús de aplicación, vamos a repasar cómo se diseña indicando las diferencias que añade la barra de acción. Como sabes, un menú se define mediante un fichero XML que se almacena en la carpeta /res/menú del proyecto. Este menú se especifica mediante el elemento raíz que contiene una serie de elementos que constituyen cada una de las opciones del menú. Estos elementos pueden incluir varios atributos que lo especifican. Entre ello, destacamos los siguientes (ya deberías conocer la mayoría): - android:id: ID identificativo de la opción que, por ejemplo, podremos usar para saber en qué opción hace clic el usuario. - android:title: texto que muestra la opción. - android:icon: icono asociado a la acción. - android:showAsAction: sólo se usa si se muestra la barra de acción. Este atributo indica si la opción del menú se mostrará como un botón de acción o como parte del menú extendido (overflow). Puede contener uno o varios valores simultáneamente, entre los siguientes: • ifRoom: la opción se muestra como botón de acción sólo si hay espacio disponible en la pantalla; si no lo hay, la opción aparecerá en el menú extendido. • withText: indica que se mostrará el texto al lado del icono si se está mostrando como botón de acción. • never: la opción se mostrará siempre como parte del menú extendido. • Always: la opción se mostrará siempre como botón de acción. Atención: este valor puede provocar que los elementos se solapen si no hay espacio suficiente para ellos en la pantalla. Recomendamos al programador que la utilice con mesura.
203
Aula Mentor
Además de poder definir mediante un archivo XML el menú, es posible definir el tipo de menú dinámicamente mediante sentencias Java de la clase MenuItem con su método menuItem. setShowAsAction(int actionEnum) e indicando una de las siguientes acciones: - SHOW_AS_ACTION_ALWAYS - SHOW_AS_ACTION_IF_ROOM - SHOW_AS_ACTION_NEVER - SHOW_AS_ACTION_WITH_TEXT Por ejemplo, si creas con Eclipse ADT un proyecto nuevo, verás que el menú definido por defecto (llamado normalmente /res/menu/activity_main.xml) tiene este contenido:
En el código anterior puedes ver que aparece un menú con una única opción denominada “Settings” y con el atributo showAsAction=”never”, es decir, la opción aparece en el menú extendido. En el Ejemplo 6 del curso hemos definido la típica barra de acción con las opciones: “Nuevo”, “Guardar” y “Opciones”:
204
En el código anterior se puede observar que la segunda opción combina varios valores de showAsAction utilizando el carácter “|“. Como puedes ver, la opción “Guardar” se muestra como botón si hay espacio, la segunda es igual que la primera pero añade el texto y la tercera opción no aparece nunca, es decir, es un menú.
U2 Multimedia y Gráficos en Android
En el código anterior hemos empleado las imágenes del propio sistema operativo @android:drawable/ic_menu_save y @android:drawable/ic_menu_add para establecer el icono de los botones. Puedes encontrar todas las imágenes disponibles por defecto en Android en tu SDK de Android en el directorio (calidad media): path_sdk_Android\platforms\android-17\data\res\drawable-mdpi
Una vez definido el menú mediante su correspondiente fichero XML, hay que asociarlo a la Actividad principal utilizando el típico método OnCreateOptionsMenu(). Para ello, de igual forma que se hace con un menú, vamos a inflarlo llamando al método inflate() e indicando el ID del fichero XML donde se ha definido dicho menú: public class MainActivity extends Activity { ...
@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflamos el menú -> Infla la barra de acción. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } }
Ahora podemos ejecutar la aplicación en el emulador y comprobar que se selecciona automáticamente el layout correcto dependiendo de las características del AVD (Dispositivo Virtual de Android) que estemos utilizando.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 6 (Barra de acción) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos empleado una Barra de acción en lugar de menús de aplicación.
205
Aula Mentor
Si ejecutas la aplicación, puedes ver que, dependiendo del tamaño de pantalla del AVD, aparecen diferentes opciones en la barra de acción, por ejemplo, en el modo vertical sólo se visualiza:
En este caso hemos ejecutado el proyecto en un AVD de 800 dpi de resolución de pantalla y vemos que aparecen como botones de acción las dos opciones que hemos marcado como showAsAction=”ifRoom”, pero no aparece el texto de la segunda opción ni el menú extendido (overflow) ya que no hay espacio disponible con esta pantalla en vertical. Tampoco se visualiza el título completo de la aplicación. Sin embargo, si rotamos la pantalla del emulador al modo horizontal (pulsando la combinación de teclado [Ctrl + F12]) vemos lo siguiente:
206
Ahora sí se ve el texto de la opción “Nuevo” tal y como lo habíamos indicado el atributo showAsAction =”withText”. Animamos al alumno a que cree varios dispositivos virtuales con diferentes tamaños de pantalla y compruebe cómo cambia la distribución y la aparición de las distintas opciones en función del tamaño de la pantalla. Una vez que ya hemos definido los elementos de la barra de acción podemos implementar el comportamiento de éstos cuando el usuario haga clic sobre ellos. Esto se implementa de la misma manera que se sigue con los menús tradicionales, es decir, ha que reescribir el método OnOptionsItemSelected()según la funcionalidad de la aplicación. En este ejemplo mostramos un mensaje sencillo de tipo Toast al usuario: @Override // En función de la opción seleccionada mostramos un Toast en la // aplicación public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_nuevo: Toast.makeText(getApplicationContext(), “Has pulsado en acción Nuevo”, Toast.LENGTH_SHORT).show(); return true; case R.id.menu_guardar: Toast.makeText(getApplicationContext(), “Has pulsado en acción Guardar”, Toast.LENGTH_SHORT).show(); return true; case R.id.menu_opciones: Toast.makeText(getApplicationContext(), “Has pulsado en acción Opciones”, Toast.LENGTH_SHORT).show(); return true;
U2 Multimedia y Gráficos en Android
} }
default: return super.onOptionsItemSelected(item);
5.3.1 Cómo integrar pestañas en la Barra de acción Hasta ahora hemos estudiado cómo introducir una Barra de acción en las aplicaciones de Android. Pero, además, Android permite que el programador pueda añadir pestañas a la citada barra de forma sencilla. Es más, el interior de estas pestañas pueden ser fragmentos. El hecho de integrar pestañas en la misma barra presenta una ventaja adicional ya que Android va a adaptar automáticamente la interfaz del usuario a los distintos tamaños y configuraciones de pantalla mejorando la visualización de la aplicación, sobre todo en los dispositivos de pantalla grande.
Por ejemplo, si Android detecta que hay suficiente espacio disponible en la Barra de acción, integrará las pestañas dentro de la propia barra de forma que no ocupan espacio extra en una fila inferior. Por el contrario, si no hubiera espacio suficiente situaría las pestañas debajo de la barra. A continuación, vamos a ver cómo se integran pestañas. Lo primero que vamos a hacer es crear un nuevo fragmento por cada pestaña que tenga la aplicación. Lo usual en las aplicaciones con pestañas es que cada una de ellas contenga diferente funcionalidad. En este ejemplo hemos definido dos pestañas que cargan dos fragmentos. En este apartado ya hemos visto cómo se implementan los fragmentos: con un fichero layout xml y su clase Java asociada: Pestaña 1: - Tab1Fragmento.java - fragmento1.xml
Pestaña 2: - Tab2Fragmento.java - fragmento2.xml Por motivos pedagógicos, la interfaz de los fragmentos será minimalista y contendrán únicamente la etiqueta de texto “Pestaña 1” o “Pestaña 2” para poder detectar el cambio de pestaña. Por ejemplo, para la pestaña 1 el fichero fragmento1.xml contiene:
207
Aula Mentor
Su clase java asociada Tab1Fragmento.java no desarrolla ninguna funcionalidad, por lo que el código se limita a inflar el layout: public class Tab1Fragmento extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragmento1, container, false); } }
Hemos definido la pestaña 2 de forma similar. Desde Eclipse ADT puedes abrir el proyecto y observar su contenido. Así, ya disponemos del contenido de las pestañas y vamos a añadirlas al Ejemplo del curso y enlazarlas en la barra de acción asignándoles un listener desde el que se responde a los eventos que se produzcan. Veamos el contenido del listener que incluye el código que gestiona los eventos clásicos de pestañas como la selección, reselección y deselección. Para esto, hemos definido la nueva clase MiTabListener que se extiende de ActionBar.TabListener en la que reescribimos los métodos de sus eventos onTabSelected(), onTabUnselected() y onTabReselected(). Veamos su código fuente:
208
// Listener que se ejecuta cada vez que se produce un cambio // de pestaña en la Barra de acción public class MiTabListener implements ActionBar.TabListener { // Variables que almacenan el fragmento que debemos cargar // y la actividad que lo ejecuta private Fragment fragment; private Activity actividad; // Definimos el constructor de la clase public MiTabListener(Fragment fg, Activity actividad) { this.fragment = fg; this.actividad= actividad; } // Evento que ocurre cuando se vuelve a seleccionar una pestaña // pero ya se está visualizando @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { Toast.makeText(actividad.getApplicationContext(), “Pestaña seleccionada de nuevo”, Toast.LENGTH_SHORT).show(); } // Eventos que ocurre cuando seleccionamos o deseleccionamos una // pestaña @Override public void onTabSelected(Tab tab, FragmentTransaction ft) { // Mostramos un mensaje Toast.makeText(actividad.getApplicationContext(), “Pestaña “ + tab.getText() + “ seleccionada”,Toast.LENGTH_SHORT).show(); // Reemplazamos en el contenedor (LinearLayout en el archivo xml
U2 Multimedia y Gráficos en Android
}
// de la Actividad principal) por el fragmento que debemos // activar ft.replace(R.id.contenedor, fragment);
@Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { // Mostramos un mensaje Toast.makeText(actividad.getApplicationContext(), “Pestaña “ + tab.getText() + “ sin seleccionar”,Toast.LENGTH_SHORT).show(); // Quitamos el fragmento del contenedor. ft.remove(fragment); } }
Estos métodos lo único que hacen es mostrar u ocultar los fragmentos en función de la pestaña seleccionada por el usuario. Así, en el evento onTabSelected() se reemplaza el fragmento que esté visible en la Actividad principal por el de la pestaña seleccionada. Por el contrario, en el método onTabUnselected() se oculta el fragmento asociado a la pestaña que pierde el foco. Ambos eventos utilizan el parámetro del tipo FragmentTransaction que permite gestionar los fragmentos de la actividad. En el primer caso, invoca su método replace() y, en el segundo, su método remove(). Hemos añadido además un mensaje para mostrar al usuario que se ha realizado un cambio sobre las pestañas. Una vez implementado este listener, en el evento onCreate() de la Actividad principal de la aplicación, tenemos que crear las pestañas, asociar sus respectivos fragmentos y “engancharlas” a la barra de acción. Veamos el código fuente de esta clase: public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
// Obtenemos una referencia a la actionbar (Barra Acción) ActionBar barra = getActionBar();
// Establecemos el modo de navegación por pestañas barra.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); // Podemos ocultar el título de la actividad si es necesario // barra.setDisplayShowTitleEnabled(false);
// Creamos las pestañas ActionBar.Tab tab1 = barra.newTab().setText(getString(R.string.tab_1)); ActionBar.Tab tab2 = barra.newTab().setText(getString(R.string.tab_2));
209
Aula Mentor
// Creamos los fragmentos de cada pestaña Fragment tab1frag = new Tab1Fragmento(); Fragment tab2frag = new Tab2Fragmento(); // Asociamos los listener a las pestañas tab1.setTabListener(new MiTabListener(tab1frag, this)); tab2.setTabListener(new MiTabListener(tab2frag, this));
} ...
// Añadimos las pestañas a la action bar barra.addTab(tab1); barra.addTab(tab2);
En el código anterior podemos ver que hemos obtenido una referencia a la barra de acción mediante el método getActionBar() y establecemos el método de navegación a NAVIGATION_MODE_TABS para que se muestren las pestañas. El método setNavigationMode() puede tomar los siguientes valores que establecen el tipo de navegación en la barra de acción: - NAVIGATION_MODE_STANDARD: muestra la barra con un título y un icono clásicos. - NAVIGATION_MODE_LIST: las pestañas se integran en el título de la barra mostrando un listado desplegable en éste. - NAVIGATION_MODE_TABS: las pestañas se integran en la barra. 210
A continuación, creamos las pestañas con el método newTab() de la barra y, en la misma sentencia, determinamos su texto con setText(). Después, instanciamos los dos fragmentos y los asociamos a cada pestaña utilizando el listener setTabListener(). Para acabar, añadimos las pestañas a la barra de acción mediante el método addTab().
Nota: hemos incluido en el código fuente el método setDisplayShowTitleEnabled()de la barra de acción que permite dinámicamente mostrar u ocultar el título
de ésta.
Si ejecutamos la aplicación en el emulador, veremos lo siguiente (depende del tamaño de pantalla de tu AVD):
U2 Multimedia y Gráficos en Android
Como podemos observar, Android ha colocado las pestañas debajo de la barra de acción porque no hay suficiente espacio disponible. Si cambiamos el emulador a la orientación horizontal [Ctrl+F12], vemos que las pestañas ya aparecen integradas en la barra:
Fíjate en el espacio que queda libre para definir el resto de la interfaz optimizando al usuario la comodidad y usabilidad de la aplicación final. Si ejecutas el proyecto en Eclipse ADT y cambias varias veces de pestaña verás que se cambia de fragmento y se muestran los mensajes correspondientes.
6. Nuevas Vistas: GridView, Interruptor (Switch) y Navigation Drawer Vamos a finalizar esta Unidad estudiando tres nuevos elementos: GridView (Vista en rejilla), Switch (Interruptor) y Navigation Drawer (Menú lateral deslizante) que el programador puede incluir en la interfaz del usuario mejorando la usabilidad y aspecto de sus aplicaciones.
6.1 Grid View En el curso de Iniciación hemos estudiado los controles de selección más utilizados por el programador en la interfaz de usuario, como son las Listas desplegables (Spinner) y las listas normales (ListView) con sus respectivos Adaptadores personalizados GridView es una Vista del tipo ViewGroup, que muestra opciones en dos dimensiones en una matriz desplazable. Las opciones del esta Vista se introducen mediante su Adaptador correspondiente.
211
Aula Mentor
Veamos un esquema visual de la distribución de este tipo de Vista:
212
Como puedes ver, el conjunto de opciones seleccionables están distribuidas de forma tabular o, dicho de otra forma, divididas en filas y columnas como una matriz. Dada la naturaleza de la Vista, sus propiedades más importantes son las típicas de cualquier listado: - android:numColumns: indica el número de columnas de la tabla. También podemos indicar auto_fit si deseamos que el propio sistema operativo las establezca a partir de ciertas propiedades. - android:columnWidth: marca el ancho de las columnas de la tabla. - android:horizontalSpacing: establece el espacio horizontal entre las celdas. - android:verticalSpacing: indica el espacio vertical entre las celdas. - android:stretchMode: marca qué hacer con el espacio horizontal sobrante. Si se establece el valor columnWidth, este espacio será dividido a partes iguales por las columnas de la tabla. Por el contrario, si se establece a spacingWidth será dividido a partes iguales por los espacios entre las celdas. Veamos cómo definiríamos un GridView en la aplicación del Ejemplo 7 del curso en el archivo
activity_main.xml:
Una vez está definida la interfaz de usuario, la forma de asignarle sus opciones es exactamente la misma que en otro tipo de listados. Por motivos pedagógicos, vamos a utilizar una matriz simple como adaptador utilizando la clase ArrayAdapter y un layout genérico (simple_list_item_1, compuesto por un simple TextView). Asociamos el adaptador al control GridView mediante su método ya conocido setAdapter(). Si abrimos la clase principal MainActivity de la aplicación del Ejemplo 7 veremos: public class MainActivity extends Activity {
private TextView labelMensaje; private GridView gridOpciones; private String[] datos; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Obtenemos la matriz datos del archivo arrays.xml datos = this.getResources().getStringArray(R.array.datos); // Creamos un adaptador sencillo ArrayAdapter adaptador = new ArrayAdapter(this, android.R.layout.simple_list_item_1, datos); // Obtenemos las referencias a las Vistas labelMensaje = (TextView)findViewById(R.id.labelMensaje); gridOpciones = (GridView)findViewById(R.id.gridOpciones);
}
// Creamos el evento onClic en una de las opciones gridOpciones.setOnItemClickListener( new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView parent, android.view.View v, int position, long id) { labelMensaje.setText(“Mes pulsado: “ + datos[position]); } }); // Establecemos el adaptador gridOpciones.setAdapter(adaptador); }
Por defecto, las opciones de la matriz se añaden al GridView ordenados por filas y, si no caben todas en la pantalla, se activa el desplazamiento (scroll) sobre la lista. En cuanto a los eventos disponibles, el más frecuente es el lanzado cuando un usuario selecciona una opción determinada de la lista: onItemClick. Este evento se captura de igual forma que con las lista Spinner y ListView.
213
Aula Mentor
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 7 (Android GridView) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado la Vista GridView.
Si ejecutas en Eclipse ADT este Ejemplo 7, verás que se muestra la siguiente lista con los meses del año:
214
Como has visto, hemos mostrado el uso básico del listado GridView. El alumno o alumna puede aplicar por su cuenta, de forma prácticamente directa, todo lo comentado sobre listas en el curso de Iniciación de Android de Mentor. Entre los conocimientos que ya debe tener podemos citar la personalización de las opciones para presentar datos complejos, la creación de un adaptador personalizado y las distintas optimizaciones para mejorar el rendimiento de la aplicación, como la reutilización de las Vistas de las opciones. Animamos al alumno a que pruebe con todas estas opciones realizando una aplicación propia.
U2 Multimedia y Gráficos en Android
6.2 Interruptores (Switches) Switch es una Vista del tipo botón compuesto que muestra un interruptor que permite al
usuario indicar si una opción está activa o inactiva. Como una imagen vale más que mil palabras, veamos el aspecto de este tipo de Vista activado y desactivado:
Como puedes ver, se trata del clásico interruptor donde el usuario puede activar o desactivar una opción con un desplazamiento sobre éste. Teniendo en cuenta la naturaleza de esta Vista, sus propiedades más importantes son: - android:text: indica el texto que debe mostrar la opción del interruptor en el lado izquierdo de éste. - android:checked: establece si el interruptor está activado o no por defecto. - android:textoOn: establece el texto interno del interruptor cuando está activado. - android:textOff: establece el texto interno del interruptor cuando está desactivado. Veamos cómo utilizamos varios Switch en la aplicación del Ejemplo 8 del curso en el archivo activity_main.xml: Si abrimos la clase principal MainActivity de la aplicación del Ejemplo 8 observamos: public class MainActivity extends Activity {
216
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Buscamos el switch gestionado para definir el evento // onCheckedChange Switch s = (Switch) findViewById(R.id.switch_gestionado); if (s != null) { s.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override // Evento que ocurre cuando el usuario enciende o // apaga un interruptor public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // Mostramos un sencillo mensaje Toast.makeText(MainActivity.this, “¡El interruptor está “ + (isChecked ? “ENCENDIDO!” : “APAGADO!”), Toast.LENGTH_SHORT).show(); } }); } //end if not null } // end onCreate }
En el código fuente anterior puedes observar que es fácil definir el evento onCheckedChanged que se lanza cuando el usuario activa o desactiva un interruptor. Es sencillo utilizar el método setCheck() para indicar si el interruptor está o no activado. Además, entre otros, también dispone del método isCheck() para conocer el estado actual del interruptor. En la ayuda oficial de Android puedes encontrar otros métodos interesantes.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 8 (Interruptores en Android) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado la Vista Switch.
Si ejecutas en Eclipse ADT este Ejemplo 8, verás que se muestra la siguiente aplicación con interruptores:
U2 Multimedia y Gráficos en Android
7. Navigation Drawer (Menú lateral deslizante) El elemento Navigation Drawer es un menú lateral deslizante donde el usuario puede seleccionar diferentes opciones. También podemos denominarlo menú de navegación. Este tipo de menús de navegación aparece en muchas aplicaciones, si bien los desarrolladores los incluían gracias a bibliotecas externas que implementaban este componente. A partir de la versión 4.3 de Android, Google pone a disposición del programador la implementación nativa de este componente y define su uso en su guía de diseño. Veamos un esquema visual de este tipo de menú:
El navigation drawer está disponible como parte de la biblioteca de compatibilidad androidsupport; así, es posible incluir este elemento visual desde la versión Android 1.6 (API 4).
217
Aula Mentor
Atención: la versión de la biblioteca android-support debe ser de la revisión 18 (Android 4.3) o superior. Si se usa una versión anterior, no funcionará el código siguiente.
La clase principal se denomina DrawerLayout y se sirve de contenedor del menú lateral deslizante. Para añadir el navigation drawer a una aplicación hay que indicar que el elemento raíz del layout XML sea del tipo android.support.v4.widget.DrawerLayout. Dentro de este elemento colocaremos únicamente dos componentes, en este orden: - FrameLayout: es el contenedor de la interfaz real de la actividad, que completamos mediante fragmentos. - ListView: es el contenedor de las distintas opciones del menú lateral.
Vamos a partir del Ejemplo 5 de esta Unidad, para desarrollar este ejemplo. Para mejorarlo lo hemos modificado utilizando la biblioteca de compatibilidad y, así, poder ejecutarlo desde la versión 2.2 de Android.
218
Veamos cómo empleamos este elemento visual en la aplicación del Ejemplo 9 del curso en el archivo activity_main.xml: Para el FrameLayout hemos ajustado con match_parent el ancho y el alto, para que ocupe todo el espacio disponible en la pantalla. En este caso del ListView el alto ocupa todo el espacio y el ancho es de 240dp; así,
U2 Multimedia y Gráficos en Android
cuando el menú esté abierto, no ocultará totalmente el contenido principal de la pantalla. Si abrimos la clase principal MainActivity de la aplicación del Ejemplo 9 encontramos el código siguiente: public class MainActivity extends ActionBarActivity // Clase que contiene el layout private DrawerLayout drawerLayout; // Opciones del layout anterior private ListView drawerList; // Listener del menú lateral private ActionBarDrawerToggle drawerToggle;
{
private CharSequence tituloMenuLateral; private CharSequence tituloAplicacion; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Buscamos las Vistas de la Interfaz usuario drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); // Listado con las opciones del menú lateral drawerList = (ListView) findViewById(R.id.menu_lateral); // Definimos las opciones del menú lateral final String[] opcionesMenu = new String[] {“Opción 1”, “Opción 2”};
// Definimos el adaptador del listado con las opciones del // menú lateral drawerList.setAdapter(new ArrayAdapter( getSupportActionBar().getThemedContext(), android.R.layout.simple_list_item_1, opcionesMenu)); // Definimos el evento onClick en el menú lateral drawerList.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { // Variable con el fragmento que vamos a crear Fragment fragmento = null; //Creamos los fragmentos según la opción // seleccionada switch (position) { case 0: fragmento = new Tab1Fragmento(); break; case 1: fragmento = new Tab2Fragmento(); break; } // end case
219
Aula Mentor
220
// Hacemos los cambios de fragmento en la interfaz // del usuario FragmentManager fragmentManager = getSupportFragmentManager(); fragmentManager.beginTransaction() .replace(R.id.frame_contenido, fragmento) .commit(); drawerList.setItemChecked(position, true); // Obtenemos el título de la opción seleccionada y // lo ponemos como título de la aplicación tituloMenuLateral = opcionesMenu[position];
getSupportActionBar().setTitle(tituloMenuLateral); // Cerramos el menú lateral drawerLayout.closeDrawer(drawerList); } }); // end onClick // Inicializamos las variables de títulos con el título // inicial de la aplicación tituloMenuLateral = getTitle(); tituloAplicacion = getTitle(); // Definimos el icono de apertura del menú lateral drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.drawable.ic_navigation_drawer, R.string.drawer_open, R.string.drawer_close) { // Evento que ocurre cuando se cierra el menú // lateral public void onDrawerClosed(View view) { // Cambiamos el título de la cabecera a // la opción elegida del menú getSupportActionBar().setTitle( tituloMenuLateral); // Ejecutamos el evento onPrepareOptionsMenu() //para actualizar la barra acción ActivityCompat.invalidateOptionsMenu( MainActivity.this); } // Evento que ocurre cuando se abre el menú // lateral public void onDrawerOpened(View drawerView) { // Cambiamos el título de la cabecera al // de la aplicación getSupportActionBar().setTitle( tituloAplicacion); // Ejecutamos el evento onPrepareOptionsMenu() // para actualizar la barra acción ActivityCompat.invalidateOptionsMenu( MainActivity.this); } };
U2 Multimedia y Gráficos en Android
// Definimos el listener del menú lateral drawerLayout.setDrawerListener(drawerToggle); // Indicamos que el icono de la aplicación es activo getSupportActionBar().setDisplayHomeAsUpEnabled(true); } // end onCreate() @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflamos el menú -> Infla la barra de acción. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } @Override // En función de la opción seleccionada mostramos un Toast en la // aplicación public boolean onOptionsItemSelected(MenuItem item) { // Si el usuario hace clic en el icono de la barra // mostramos el menú lateral if (drawerToggle.onOptionsItemSelected(item)) { return true; } else // Si no, mostramos el onClick de la opción del menú // correspondiente switch (item.getItemId()) { case R.id.menu_nuevo: Toast.makeText(this, “Has pulsado en acción Nuevo”, Toast.LENGTH_SHORT).show(); return true; case R.id.menu_guardar: Toast.makeText(this, “Has pulsado en acción Guardar”, Toast.LENGTH_SHORT).show(); return true; case R.id.menu_buscar: Toast.makeText(this, “Has pulsado en acción Buscar”, Toast.LENGTH_SHORT).show(); return true; default: return super.onOptionsItemSelected(item); } } // end onOptionsItemSelected // Evento que se lanza antes de mostrar el menú @Override public boolean onPrepareOptionsMenu(Menu menu) { // Vemos si el menú lateral está abierto boolean menuAbierto = drawerLayout.isDrawerOpen( drawerList); // Si está abierto, ocultamos la opción del menú buscar if(menuAbierto) menu.findItem(R.id.menu_buscar).setVisible(false); else // Si no, la mostramos
221
Aula Mentor
menu.findItem(R.id.menu_buscar).setVisible(true); return super.onPrepareOptionsMenu(menu); } // Evento que ocurre después de crear la Actividad @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sincronizamos el listener del menú lateral drawerToggle.syncState(); } // Evento que ocurre si cambia la configuración de la Actividad @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Notificamos el cambio al listener del menú lateral drawerToggle.onConfigurationChanged(newConfig); } } // end clase
222
En el código anterior, dentro del método onCreate() de la actividad, buscamos las Vistas de la Interfaz de usuario y definimos las opciones del menú lateral en un matriz de tipo String. Además, creamos el adaptador del listado con las opciones del menú lateral pasando como parámetro a este adaptador el contexto obtenido invocando el método getThemedContext() de la action bar. Hemos utilizando getSupportActionBar() para acceder a la barra de acción en lugar del habitual getActionBar() ya que estamos utilizando la biblioteca de compatibilidad. Como el layout de los elementos de la lista hemos establecido el estándar android.R.layout.simple_list_item_1. De esta forma, el listado será compatible con la mayoría de versiones de Android. Sin embargo, este estilo hace que la opción seleccionada no esté resaltada en el menú cada vez que se cierre y se vuelva a abrir. Para hacer esto, en Android 4, podemos aplicar el layout android.R.layout.simple_list_item_activated_1, si bien aparecerá un error si se ejecuta la aplicación en versiones anteriores de Android. Si deseamos evitar este problema de compatibilidad tenemos varias opciones: Aplicar un layout diferente dependiendo de la versión de Android en la que se ejecuta la aplicación: drawerList.setAdapter( new ArrayAdapter(getSupportActionBar().getThemedContext(), (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) ? android.R.layout.simple_list_item_activated_1 : android.R.layout.simple_list_item_1, opcionesMenu));
Es decir, se mantendrá la opción seleccionada en Android 4, pero no en versiones anteriores. Aplicar un layout distinto que mantenga la opción marcada, aunque no se el de Android 4. Por ejemplo, si utilizamos simple_list_item_checked de Android 2.x, la opción se mantendrá resaltada con una marca de tipo check a la derecha de su nombre. Veamos su aspecto: drawerList.setAdapter( new ArrayAdapter(getSupportActionBar().getThemedContext(), (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) ? android.R.layout.simple_list_item_activated_1 : android.R.layout.simple_list_item_checked, opcionesMenu));
U2 Multimedia y Gráficos en Android
Desarrollar un adaptador personalizado que establezca manualmente el estilo de la opción seleccionada del listado, tal y como hemos estudiado en el curso de Iniciación de Android de Mentor. A continuación, detallamos el evento onClick del menú lateral donde vamos a crear los fragmentos que mostraremos al seleccionar una de las opciones. En este caso hemos utilizado los mismos fragmentos que el Ejemplo 5 de esta Unidad, por lo tanto, no explicaremos su detalle. En este caso en concreto, utilizamos el Fragment Manager con getSupportFragmentManager(), haciendo uso de la biblioteca de compatibilidad, para sustituir el contenido del FrameLayout que definimos en el layout por el nuevo fragmento creado. Después, marcamos la opción pulsada del listado mediante el método setItemChecked() y actualizamos el título de la barra de acción con el título de la opción seleccionada. A continuación, cerramos el menú lateral invocando la orden closeDrawer(). Según las recomendaciones de la guía de diseño de este componente, es aconsejable mostrar un indicador en la barra de acción que informa al usuario de que la aplicación dispone de un menú lateral. Además si el usuario hace clic sobre el icono de la aplicación, debería desplegarse el menú. También podemos actualizar el título de la barra de acción y ocultar aquellas opciones del menú principal cuando esté abierto el menú lateral. Para todo esto vamos a utilizar la clase ActionBarDrawerToggle. En el código anterior hemos creado un objeto del tipo ActionBarDrawerToggle y lo hemos asociado al navigation drawer mediante el método setDrawerListener() para responder a los eventos de apertura y cierre del menú sobrescribiendo sus métodos onDrawerOpened() y onDrawerClosed(), que actualizan el título de la barra de acción mostrando el título de la aplicación, cuando el menú está abierto, o el título de la opción seleccionada actualmente, cuando el menú está cerrado. El constructor de la clase ActionBarDrawerToggle recibe cinco parámetros: el contexto actual, una referencia al navigation drawer, el ID del icono a utilizar como indicador del navigation drawer y los ID de dos cadenas de caracteres que se utilizar a efectos de accesibilidad de la aplicación (literales “Menú Abierto” y “Menú Cerrado”). Para aplicar el icono apropiado para el indicador del navigation drawer puedes usar la utilidad Navigation Drawer Indicator Generator de la página Android Asset Studio, que permite generar y descargar el icono correspondiente y copiarlo a las carpetas /res/ drawable-xxx del proyecto. Al final de cada método onDrawerOpened() y onDrawerClosed() llamamos a la orden invalidateOptionsMenu() para que se ejecute el evento onPrepareOptionsMenu() de la Actividad y, así, ocultar las acciones de la barra de acción que no se apliquen cuando este menú lateral esté abierto. De nuevo, hemos hecho uso de la orden alternativa incluida en la biblioteca de compatibilidad de la clase ActivityCompat ya que el método invalidateOptionsMenu() apareció en la versión Android 3.0 (API 11). También habilitamos que el usuario pueda abrir el menú lateral pulsando sobre el icono de la aplicación de la barra de acción llamando al método setDisplayHomeAsUpEnabled(). Si te fijas en el evento onOptionsItemSelected(), encargado de gestionar los clics del usuario sobre la barra de acción, hemos añadido una llamada inicial al método onOptionsItemSelected() del objeto ActionBarDrawerToggle creado anteriormente, ya que así podemos gestionar esta pulsación sobre el icono de la aplicación y salir directamente de este método.
223
Aula Mentor
Para acabar, las recomendaciones de la guía de diseño de ActionBarDrawerToggle indican que debemos: - Implementar el evento onPostCreate() de la Actividad que ocurre después de crearla, llamando al método syncState() del objeto ActionBarDrawerToggle para sincronizar el listener del menú lateral. - Implementar el evento onConfigurationChanged() de la Actividad que ocurre si cambia la configuración del dispositivo (por ejemplo, su orientación), invocando a su método homólogo del objeto ActionBarDrawerToggle. Para poder utilizar la biblioteca de compatibilidad es necesario importarla. Para ello, hacemos clic en la opción “File->Import” de Eclipse ADT y buscamos el directorio adt-bundlewindows-x86\sdk\extras\android\support\v7\appcompat del SDK de Android:
224
A continuación pulsamos aceptar y veremos que ya aparece en el entorno de desarrollo esta biblioteca:
U2 Multimedia y Gráficos en Android
A continuación, importamos la biblioteca en este Ejemplo 9 abriendo sus propiedades y añadiendo la biblioteca en la pestaña “Android” (en la teoría de la Unidad 4 sobre Bibliotecas puedes encontrar más información sobre este proceso):
225
Si pulsamos el botón “OK”, verás que la biblioteca queda cargada en el proyecto:
Aula Mentor
Para acabar, volvemos a pulsar el botón “OK”. Para que el tema visual de la aplicación sea compatible en todas las versiones de Android, debemos sustituirlo o extenderlo aplicando un tema definido específicamente en la biblioteca de compatibilidad: Theme.AppCompat Theme.AppCompat.Light Theme.AppCompat.Light.DarkActionBar
En este ejemplo vamos a aplicar el último de ellos modificando el archivo fichero AndroidManifest.xml e indicando el nuevo tema en el atributo android:theme del elemento :
Si no hacemos esto, aparecerá el siguiente error de compilación en Eclipse ADT: No resource found that matches the given name (at ‘theme’ with value ‘@style/Theme. AppCompat.Light.DarkActionBar’).
226
Hay que tener también en cuenta que, en versiones anteriores a Android 3.0, no existen los atributos de los menús utilizados por la funcionalidad de la barra de acción como, por ejemplo, el atributo android:showAsAction y, por lo tanto, no podemos utilizarlos directamente. Para solucionar este problema, vamos a utilizar dichos atributos indicando un espacio de nombres personalizado que definimos en el elemento y usamos en los elementos . Veamos el aspecto del archivo unidad2.eje9.navigation_drawer\res\menu\activity_main.xml:
Hemos incluido un nuevo espacio de nombres (namespace) llamado aplicacion en el elemento . Después, en las opciones del menú, indicamos el nuevo espacio de nombres en los atributos específicos de la barra de acción.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 9 (Navigation Drawer) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado un Menú deslizable lateral.
Si ejecutas en Eclipse ADT este Ejemplo 9, verás que se muestra la siguiente aplicación:
227
Si ejecutas en un AVD de Android 2.x puedes ver que la aplicación funciona correctamente y el aspecto es muy similar:
Aula Mentor
228
Si al importar el proyecto en Eclipse ADT aparece la siguiente ventana de error:
Debes importar la biblioteca de compatibilidad siguiendo los pasos anteriormente descritos.
U2 Multimedia y Gráficos en Android
8. Resumen Hay que saber al final de esta unidad: - Hoy en día, es necesario que una aplicación tenga un aspecto visual atractivo con el diseño apropiado. - En inglés, se denomina “look and feel” al aspecto visual de una aplicación. - Los temas visuales de Android (themes) son un medio rico y complejo que permiten definir todos los atributos del aspecto de Vistas de Android. - Un atributo es una propiedad de una Vista que permite modificar su aspecto visual. - Los Estilos (Styles) agrupan varios atributos visuales de una Vista en un único bloque que podemos aplicar al aspecto de ésta. - Para utilizar temas conjuntamente con estilos debemos dar los siguientes pasos: • Definir el nombre del tema y sus atributos (características de aspecto de las Vistas). • Especificar los atributos anteriores mediante referencia a otro atributo (format=”reference”). • Diseñar el aspecto de cada atributo con las propiedades visuales de una Vista Android. • Aplicar el estilo que corresponda a las vistas que definen la interfaz del usuario mediante su propiedad style. - Un recurso de color permite definir un color común a toda la aplicación. - Un recurso de tipo Dimensión permite definir una longitud común a toda la aplicación. - Un Gradient Drawable permite al programador dibujar un gradiente de colores que consiste en mostrar un degradado de dos colores en el fondo de cualquier Vista. - Un Selector Drawable es un tipo de elemento que permite realizar cambios automáticos basados en el aspecto de una Vista teniendo en cuenta el estado actual de ésta. - Un Nine-patch Drawable es un tipo especial de imagen que escala o crece tanto a lo largo como a lo ancho y que mantiene su relación de aspecto visual. - Un Widget es una aplicación reducida o programa de tamaño pequeño que permite al usuario visualizar información de forma rápida en la pantalla y/o acceder a funciones utilizadas frecuentemente. - Debemos saber que los Widgets: • Se pueden arrastrar y cambiar de posición. • El usuario puede eliminarlos o modificar su tamaño. • El usuario puede interactuar con ellos como si fueran una Actividad más. • Actualizan información de forma periódica. • Se puede crear un mismo Widget tantas veces como sea necesario. • Se puede configurar un Widget al crearlo. - Para crear la interfaz visual de usuario de los Widgets debemos utilizar la clase RemoteViews.
229
Aula Mentor
230
- Las Vistas ListView, GridView, StackView y AdapterViewFlipper se pueden utilizar en el interior de un Widget a partir de la versión Android 3.0 empleando las Vistas de Colecciones (Collections). - Desde la versión 4.2 de Android es posible incluir Widgets en la pantalla de bloqueo (Lock Screen) del dispositivo. - Los Live Wallpapers son fondos de pantalla animados e interactivos de Android similares a las aplicaciones de Android. - La clase SurfaceHolder de Android es una Interfaz abstracta que permite controlar el tamaño, el formato, editar los píxeles y gestionar los cambios de una superficie, en este caso, es la superficie del fondo de pantalla. - Un Fragmento (Fragment) es un pedazo de la interfaz de usuario que se puede añadir o eliminar de la interfaz global de usuario de forma independiente al resto de elementos de la Actividad. - Android permite utilizar Fragmentos en versiones anteriores a la 3.0 si incluimos la librería de compatibilidad android-support en el proyecto. - Fragment Manager es el componente del sistema operativo encargado de gestionar los fragmentos de una aplicación. - El ciclo de vida de un Fragmento está muy relacionado con el ciclo de vida de la Actividad que lo contiene. - Las transacciones permiten llevar a cabo otras operaciones dinámicas con Fragmentos en tiempo de ejecución que modifican la interfaz del usuario. - Android también dispone de una pila de ejecución de Fragmentos llamada back. - Un fragmento está directamente ligado a la Actividad que lo contiene y, por lo tanto, es posible una comunicación entre ambos. - Desde el punto de vista de programación es importante evitar que las Actividades y Fragmentos sean muy interdependientes entre sí ya que esto reduce las posibilidades de poder reusar el fragmento. - A partir de la versión 3.0 de Android, las clásicas ventanas de diálogo (Dialog) están en desuso en favor de los fragmentos. - La Barra de Acción de Android (o Action Bar) es, como su propio nombre indica, una barra que aparece en la parte superior de una aplicación que puede incluir un icono, el título de la Actividad y varios botones de acción o un menú extendido desplegable. - Android permite que el programador pueda añadir pestañas a la Barra de Acción de forma sencilla. - GridView es una Vista de tipo matriz que muestra opciones en dos dimensiones en una matriz desplazable. - Switch es una Vista del tipo botón compuesto que muestra un interruptor que permite al usuario indicar si una opción está activa o inactiva. - Un Navigation Drawer es un menú lateral deslizante donde el usuario puede seleccionar diferentes opciones.
U3 Sensores y dispositivos de Android
Unidad 3. Sensores y dispositivos de Android
1. Introducción En esta Unidad vamos a explicar cómo se utilizan los sensores y dispositivos de Android en una aplicación. Primero, haremos una introducción a los sensores; después, veremos cómo se usan conjuntamente con el Simulador de sensores. Además, mostraremos mediante ejemplos la integración de los dispositivos WIFI, Bluetooth, GPS y Cámara de fotos en una aplicación Android. Finalmente, veremos cómo aplicar sensores a un juego sencillo.
2. Introducción a los sensores y dispositivos La mayoría de los dispositivos de Android incorpora sensores que miden su movimiento, orientación y otras varias magnitudes físicas. Estos sensores proporcionan datos de magnitudes físicas con alta precisión y exactitud y son útiles, por ejemplo, para controlar la posición o el movimiento del dispositivo en tres dimensiones o su localización mediante GPS. Así, un juego puede monitorizar el estado del sensor de gravedad de un dispositivo para reconocer movimientos complejos de usuario como su inclinación, su movimiento, su rotación o su giro. Del mismo modo, una aplicación de predicción del tiempo podría utilizar un sensor de temperatura y de humedad del dispositivo. Igualmente, una aplicación de viajes podría usar el sensor de campo magnético y acelerómetro para mostrar el rumbo de la brújula. Podemos dividir los sensores de Android en tres categorías: - Sensores de Movimiento Miden las fuerzas de aceleración y giro de un dispositivo en sus tres ejes. Dentro de esta categoría podemos incluir acelerómetros, sensores de gravedad, giroscopios y sensores de rotación. - Sensores del Medioambiente Miden magnitudes medioambientales, como la temperatura, la presión, la iluminación y la humedad. Dentro de esta categoría tenemos: barómetros, fotómetros y termómetros. - Sensores de Posición Miden la posición física del dispositivo. Entre ellos, encontramos: los sensores de orientación y los magnetómetros. En los dispositivos Android existe otro tipo de elementos que, si bien no pueden considerarse estrictamente sensores en realidad, son componentes de hardware que contienen sensores que se usan con un propósito muy definido. Por ejemplo, un dispositivo WIFI puede usarse para conectase a una red WIFI (o para medir la potencia de ésta). Entre ellos, encontramos: dispositivos GPS, WIFI, Bluetooth y Cámara de fotos. Este tipo de componentes los estudiaremos de forma separada un poco más adelante en esta Unidad.
231
Aula Mentor
Para acceder a los sensores disponibles en un dispositivo y obtener sus datos, Android proporciona varias clases e interfaces que realizan una amplia variedad de tareas relacionados con los sensores. Por ejemplo, se pueden utilizar para: - Obtener los sensores disponibles en el dispositivo. - Conseguir las capacidades e información de un sensor en particular, como el rango de medida, el fabricante, la potencia y resolución de la medida. - Recibir los datos de las magnitudes medidas de los sensores definiendo un intervalo de actualización de las medidas. - Registrar y quitar los listeners asociados a la monitorización y medida que realizan los sensores. En este apartado vamos a estudiar cómo se usan los sensores de Android. Todos los sensores se manipulan de forma homogénea y con ellos podremos implementar mejorar la interacción del dispositivo con el usuario.
No todos los dispositivos disponen de los mismos sensores. Cada modelo y fabricante incluye los que considera apropiados. Además, para gestionar estos sensores el dispositivo emplea drivers que el fabricante no suele hacer públicos.
2.1 Gestión de Sensores de Android 232
Android permite acceder a los sensores internos del dispositivo a través de las clases del paquete android.hardware siguientes: - Sensor: clase que representa a un sensor con todas sus propiedades. - SensorEvent: clase que se usa para pasar los datos medidos por el sensor a la aplicación. - SensorManager: gestor que permite acceder a los sensores de un dispositivo. Para obtener una instancia del mismos debemos invocar el método Context.getSystemService() con el argumento SENSOR_SERVICE. - SensorEventListener: interfaz utilizada para recibir las notificaciones del SensorManager cuando se comunican nuevas medidas de los sensores. Desde el punto de vista del desarrollador, las clases Java que debemos usar son pocas y sencillas. Esto es así porque, para que el dispositivo Android gestione sensores, debe utilizar drivers cuyo código fuente no suele hacer público su fabricante. Por esto, Android tiene tanto éxito entre los fabricantes de dispositivos, ya que éstos no tienen que publicar el código fuente de los drivers que, en realidad, muestra cómo funciona su hardware. La clase Sensor contiene la información y propiedades completas de un sensor. Para obtener el tipo de sensor que contiene un objeto de esta clase, debemos usar su método getType() que devuelve 11 tipos de sensores mediante alguna de las siguientes constantes:
U3 Sensores y dispositivos de Android
Tipo / CONSTANTE
Descripción
Dimensiones
Desde API
acelerómetro
Mide aceleraciones por gravedad y cambios de movimiento
3
3
Brújula, detecta campo magnéticos
3
3
Detectar giros
3
3
Indica dirección a la que apunta el dispositivo. (Obsoleto desde API 8)
3
3
Se usa para ajustar iluminación pantalla.
1
3
Detecta un objeto a menos de 5 cm para, por ejemplo, apagar la pantalla al hablar por teléfono.
lógico
3
Altímetro y barómetro
1
3
Evita sobrecalentamientos del dispositivo. (Obsoleto desde API 14)
1
3
Mide la aceleración debida a la gravedad.
3
9
Mide aceleraciones sin tener en cuenta la gravedad.
3
9
Detecta giros
3
9
Mide la temperatura del aire.
1
14
Mide la humedad absoluta y relativa.
1
14
TYPE_ACCELEROMETER
campo magnético TYPE_MAGNETIC_FIELD
giroscopio TYPE_GYROSCOPE
orientación TYPE_ORIENTATION
luz ambiental TYPE_LIGHT
proximidad TYPE_PROXIMITY
presión atmosférica TYPE_PRESSURE
temperatura interna TYPE_TEMPERATURE
gravedad TYPE_GRAVITY
acelerómetro lineal TYPE_LINEAR_ACCELERATION
vector de rotación TYPE_ROTATION_VECTOR
temperatura ambiental TYPE_AMBIENT_TEMPERATURE
humedad relativa TYPE_RELATIVE_HUMIDITY
Esta lista anterior se va ampliando con las nuevas versiones de Android, si bien los sensores disponibles varían mucho en función del dispositivo utilizado. Además, podemos dividir los sensores en dos categorías más: real y virtual. “Real” se refiere a que el sensor indica una medida de una magnitud física. “Virtual” significa que son medidas obtenidas a partir de la combinación de las medidas de otros sensores o son medidas relativas.
233
Aula Mentor
234
Puedes observar que los sensores TEMPERATURE y ORIENTATION son obsoletos. El primero se ha sustituido por el sensor AMBIENT_TEMP. El segundo se quitará de Android porque normalmente no hay un sensor de orientación en los dispositivos. Para conocer esta orientación, el dispositivo combina los datos de los sensores acelerómetro y del campo magnético. Para obtener la orientación del dispositivo puedes utilizar el método android.view.Display. getRotation() de Android. Para acceder a los sensores de un dispositivo Android proporciona el gestor SensorManager. Para obtener el listado completo de los sensores del dispositivo, debemos usar su método getSensorList(tipo) que devuelve una lista de tipo Sensor. En el parámetro tipo hay que marcar el tipo de sensores que queremos obtener mediante uno de los tipos del listado anterior o Sensor.TYPE_ALL para indicar todos los sensores.
Atención: para desarrollar aplicaciones que utilizan sensores, es necesario disponer de un dispositivo físico, ya que el emulador de Android no permite depurar todos los tipos de sensores correctamente.
Sin embargo, en el apartado siguiente veremos cómo instalar un software de emulación que permitirá realizar pruebas sobre algunos sensores simulados del emulador.
2.1.1 Cómo se utilizan los Sensores Como los sensores están muy relacionados con el mundo real, la mejor forma de entender su funcionamiento es mediante un ejemplo práctico. Vamos a comenzar con el Ejemplo 1 de este Unidad donde vamos a obtener todos los sensores de un dispositivo y mostraremos los datos medidos por éstos. Si abrimos su layout XML activity_main.xml vemos el siguiente contenido típico del layout de una Actividad con etiquetas:
U3 Sensores y dispositivos de Android
235
Aula Mentor
En el fichero anterior puedes ver que hemos definido la Vista ScrollView con el atributo android:fadeScrollbars=”false” para que no se oculte la barra de desplazamiento vertical. Si ahora abres el archivo Java que define la Actividad, verás el siguiente contenido: public class MainActivity extends Activity implements SensorEventListener { private TextView sensores, datos;
236
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sensores = (TextView) findViewById(R.id.sensores); datos = (TextView) findViewById(R.id.datos); // Conectamos con el gestor de sensores SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); // Obtenemos el listado con todos los sensores List listaSensores = sensorManager.getSensorList(Sensor.TYPE_ALL); // Mostramos en pantalla el listado de sensores for(Sensor sensor: listaSensores) { log(sensor.getName(), sensores); } // end for // A continuación, vamos a buscar los sensores de varios tipos en // concreto y le asignamos al primero que encontremos a un // listener para leer sus medidas listaSensores = sensorManager.getSensorList(Sensor.TYPE_ROTATION_VECTOR); if (!listaSensores.isEmpty()) { Sensor orientationSensor = listaSensores.get(0); // Registramos el lister para este sensor e indicamos que // se realice una medida cada SENSOR_DELAY_UI=1000 miliseg. sensorManager.registerListener(this, orientationSensor, SensorManager.SENSOR_DELAY_UI); } listaSensores = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); if (!listaSensores.isEmpty()) { Sensor acelerometerSensor = listaSensores.get(0); sensorManager.registerListener(this, acelerometerSensor, SensorManager.SENSOR_DELAY_UI); } listaSensores = sensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD); if (!listaSensores.isEmpty()) { Sensor magneticSensor = listaSensores.get(0); sensorManager.registerListener(this, magneticSensor,
U3 Sensores y dispositivos de Android
SensorManager.SENSOR_DELAY_UI); } listaSensores = sensorManager.getSensorList(Sensor.TYPE_AMBIENT_TEMPERATURE); if (!listaSensores.isEmpty()) { Sensor temperatureSensor = listaSensores.get(0); sensorManager.registerListener(this, temperatureSensor, SensorManager.SENSOR_DELAY_UI); } } // end onCreate // Método que añade a la vista un texto private void log(String string, TextView vista) { vista.append(string + “\n”); }
// Invocada cuando cambia se exactitud del sensor. // Por ejemplo, cuando la detección del GPS pasa de hacerse // de la red de telefonía móvil al sensor GPS del dispositivo (más // preciso) @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } // Evento que se lanza cada vez que se modifican los datos del // sensor. Es decir, es una nueva medida. @Override public void onSensorChanged(SensorEvent evento) { String sensorStr=”Desconocido”; // Como estamos monitorizando todos los sensores, es necesario // controlar el acceso a las Vistas internas de la Actividad. // Es decir, cada sensor puede provocar que un thread principal // invoque a la vez este evento. Si sincronizamos esas // sentencias, entonces indicamos mediante Java que se deben // ejecutar todas ellas antes de que se pueda volver a ejecutar // de nuevo este bloque de código. synchronized (this) { switch(evento.sensor.getType()) { case Sensor.TYPE_ROTATION_VECTOR: sensorStr=”Rotación”; break; case Sensor.TYPE_ACCELEROMETER: sensorStr=”Acelerómetro”; break; case Sensor.TYPE_MAGNETIC_FIELD: sensorStr=”Campo magnético”; break; case Sensor.TYPE_AMBIENT_TEMPERATURE: sensorStr=”Temperatura”; break; default: } // end case for (int i=0 ; iadb devices * daemon not running. starting it now on port 5037 * * daemon started successfully * List of devices attached emulator-5554 device C:\cursosMentor\adt\sdk\platform-tools>
A continuación, instalamos en el AVD anterior la aplicación SensorSimulatorSettings-2.0-
243
Aula Mentor
rc1.apk, que servirá de puente con el simulador de sensores: adb -s install sensorsimulator-2.0-rc1/bin/ SensorSimulatorSettings-2.0-rc1.apk
Debemos reemplazar la etiqueta por el dispositivo mostrado en el paso anterior: C:\cursosMentor\...\platform-tools>adb -s emulator-5554 install ../ sensorsimulator-2.0-rc1/bin/SensorSimulatorSettings-2.0-rc1.apk 699 KB/s (49681 bytes in 0.069s) pkg: /data/local/tmp/SensorSimulatorSettings-2.0-rc1.apk
Success
Si no instalamos esta aplicación en el dispositivo no podremos usar el simulador de sensores en un AVD ya que su cometido es servir de conector con el SensorSimulator.
Finalmente, en el AVD buscamos la aplicación Sensor Simulator Settings instalada en el paso anterior y la ejecutamos:
244
U3 Sensores y dispositivos de Android
Debemos dejar los campos IP y Socket como están. Esta IP debe coincidir con la IP que aparece en el lado izquierdo (parte de abajo) en el SensorSimulator:
Cambiamos en la aplicación del AVD a la pestaña Testing y hacemos clic en el botón Connect:
245
Aula Mentor
En la ventana anterior podemos ver los datos de los sensores que tenemos habilitados (color azul más oscuro) en el Simulador de sensores:
246 Si movemos la imagen del teléfono arrastrando el ratón sobre ésta en la aplicación SensorSimulator, vemos cómo se cambian los valores en el emulador.
U3 Sensores y dispositivos de Android
Podemos balancear (yaw), girar (roll) y mover (move) el dispositivo arrastrando el ratón sobre el esquema del dispositivo virtual. La aplicación Sensor Simulator Settings se usa de puente entre el AVD y el SensorSimulator. De esta forma, se comunica la información del cambio de los valores de los sensores que luego usará nuestra aplicación Android.
Para añadir o quitar sensores en el dispositivo debemos usar sucesivamente los botones Disconnect y Connect de la aplicación Sensor Simulator Settings.
3.2.1 Ejemplo de desarrollo de aplicación con el Simulador de Sensores Una vez hemos instalado y comprobado que funciona el simulador de sensores, veamos cómo podemos utilizarlo en el desarrollo de una aplicación. Vamos a partir del Ejemplo 1 de esta Unidad y modificaremos su código fuente para que sea compatible con este simulador. Lo primero que debemos hacer es crear el directorio libs dentro del directorio raíz del proyecto y copiar dentro el archivo sensorsimulator-2.0-rc1/lib/sensorsimulator-lib-x.x.x.jar. Después, desde Eclipse ADT hacemos clic con el botón derecho del ratón sobre este archivo y seleccionamos la opción “Build Path” y “Add to Build Path”: 247
Una vez hecho esto, debemos abrir las propiedades del proyecto y en la opciones “Java Build Path” en la pestaña “Order and Export” hay que subir la librería a la primera posición (para
que las clases de esta librería tengan prioridad frente a las de Android) y marcarla como exportable, tal y como vemos en la siguiente pantalla:
Aula Mentor
248
A continuación, vamos a modificar el código fuente del Ejemplo 1 de esta Unidad copiándolo previamente como Ejemplo 2. A continuación, abrimos el Ejemplo 2 para ver cómo queda el código fuente una vez modificado. Empezamos comentando los imports originales y agregando los de las clases del paquete org.openintents.sensorsimulator: /*import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager;*/ import import import import
org.openintents.sensorsimulator.hardware.Sensor; org.openintents.sensorsimulator.hardware.SensorEvent; org.openintents.sensorsimulator.hardware.SensorEventListener; org.openintents.sensorsimulator.hardware.SensorManagerSimulator;
En el método onCreate() de la Actividad reemplazamos el código: sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
por la sentencia: sensorManager = SensorManagerSimulator.getSystemService(this, SENSOR_ SERVICE);
Además, incluimos el método para conectar la aplicación al simulador de sensores: sensorManager.connectSimulator(); Cuando ejecutemos la aplicación en el AVD, veremos en la Vista Logcat de Eclipse ADT los siguientes
mensajes debidos a la ejecución de la sentencia anterior:
U3 Sensores y dispositivos de Android
06-05 06-05 06-05 06-05 06-05 06-05 06-05 06-05
12:35:38.516: 12:35:38.806: 12:35:38.846: 12:35:38.846: 12:35:38.856: 12:35:38.946: 12:35:38.957: 12:35:38.986:
I/Hardware(2964): I/Hardware(2964): I/Hardware(2964): I/Hardware(2964): I/Hardware(2964): D/Hardware(2964): D/Hardware(2964): D/Hardware(2964):
Starting connection... Connecting to 10.0.2.2 : 8010 Read line... Received: SensorSimulator Connected Sensor pressure not supported Sensor barcode reader not supported Sensor gyroscope not supported
Así, podemos verificar que nuestra aplicación se ha conectado correctamente al Simulador de sensores. Normalmente, debemos hacer el registro y desregistro de los listeners en los eventos onResume() y onStop() respectivamente. Para hacerlo, hemos escrito: @Override protected void onResume() { super.onResume(); // Registramos todos los listener de los sensores sensorManager.registerListener(eventListenerAcelerometro, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerAcceleracionLineal, sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerGravedad, sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerCampoMagnetico, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerOrientacion, sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerTemperatura, sensorManager.getDefaultSensor(Sensor.TYPE_TEMPERATURE), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerLuz, sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerPresion, sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerBarcode, sensorManager.getDefaultSensor(Sensor.TYPE_BARCODE_READER), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerRotacion, sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR), SensorManagerSimulator.SENSOR_DELAY_FASTEST); sensorManager.registerListener(eventListenerGiroscopio, sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManagerSimulator.SENSOR_DELAY_FASTEST); }
249
Aula Mentor
@Override protected void onStop() { // Desregistramos todos los listeners de los sensores sensorManager.unregisterListener(eventListenerAcelerometro); sensorManager.unregisterListener(eventListenerAcceleracionLineal); sensorManager.unregisterListener(eventListenerGravedad); sensorManager.unregisterListener(eventListenerCampoMagnetico); sensorManager.unregisterListener(eventListenerOrientacion); sensorManager.unregisterListener(eventListenerTemperatura); sensorManager.unregisterListener(eventListenerLuz); sensorManager.unregisterListener(eventListenerPresion); sensorManager.unregisterListener(eventListenerRotacion); sensorManager.unregisterListener(eventListenerGiroscopio); sensorManager.unregisterListener(eventListenerBarcode); super.onStop(); }
Finalmente, implementamos los SensorEventListener, uno para cada tipo de sensor:
250
// Puedes observar que debemos implementar los mismos eventos: // onSensorChanged y onAccuracyChanged eventListenerAcelerometro = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // Obtenemos los valores devueltos por el evento y los // mostramos en la Vista correspondiente float[] values = event.values; textViewAcelerometro.setText(“Accelerómetro: “ + values[0] + “, “ + values[1] + “, “ + values[2]); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} };
Además, para que esta aplicación pueda comunicarse con el Simulador de sensores, hay que agregar al archivo Manifest el siguiente permiso:
El permiso de INTERNET es necesario para conectar la aplicación al SensorSimulator. La clase SensorManagerSimulator deriva de la clase base SensorManager de Android e implementa casi sus mismas funciones y listeners (clase SensorEventListener).
Una vez hayamos terminado y depurado la aplicación hay que volver a modificar los imports y usar la clases de sensores nativas de Android, ya que, si no lo hacemos, la aplicación fallará en un dispositivo real.
U3 Sensores y dispositivos de Android
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 2 (Listado Sensores- SensorSimulator) de la Unidad 3. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado un Simulador de Sensores.
Si ejecutas en Eclipse ADT este Ejemplo 2, verás que se muestra la siguiente aplicación:
251 En esta Unidad puedes encontrar el vídeo “Cómo utilizar SensorSimulator”, que muestra de manera visual los pasos seguidos en las explicaciones anteriores. Te recomendamos que lo muestres y leas las explicaciones que aparecen.
3.2.2 Grabación de escenario de simulación con un dispositivo real Como hemos comentado anteriormente, es posible utilizar un dispositivo real para guardar los datos de sus sensores y, después, poder hacer una simulación con ellos. Así, el programador puede automatizar esta tarea y repetirla tantas veces como sea necesaria cuando depuremos una aplicación de Android. Para hacerlo, debes hacer lo siguiente: - Instalar en el dispositivo real la aplicación SensorRecordFromDevice-x.x-x.apk, que puedes encontrar en la carpeta bin del SensorSimulator y que guardará la información de los sensores: adb -s install sensorsimulator-2.0-rc1/bin/SensorRecordFromDevice2.0-rc1
Debemos reemplazar la etiqueta por el dispositivo real. - Ejecutar en el dispositivo real la aplicación anteriormente instalada, seleccionando los sensores que deseamos monitorizar y pulsando en el botón “Record”:
Aula Mentor
252
Debes asegurarte de que el dispositivo Android se encuentra en la misma red que la aplicación Java de tu ordenador (SensorSimulator). Lo más sencillo es utilizar una red wifi. La IP que escribas debe corresponder a la IP de tu ordenador. A veces, es conveniente también deshabilitar el firewall del ordenador. Cuando hayamos, acabado debemos pulsar el botón “Stop”. - En la aplicación externa del SensorSimulator seleccionar la pestaña “Scenario Simulator”. En esta ventana veremos cómo se van cargando los valores de los sensores seleccionados:
U3 Sensores y dispositivos de Android
Animamos al alumno a que pruebe a utilizar su propio dispositivo para grabar escenarios de prueba de simulaciones.
4. Dispositivos de Android En este apartado vamos a mostrar los fundamentos de utilización de los dispositivos o conectores más importantes de Android: el módulo WIFI, el módulo Bluetooth, la Cámara de fotos y el módulo GPS. Mediante un ejemplo práctico expondremos, para cada tipo de módulo, una explicación detallada de las funciones propias del SDK. Así, podrás realizar cualquier aplicación que haga uso de ellos.
4.1 Módulo WIFI El SDK de Android dispone de clases que permiten hacer uso del módulo WIFI, siempre y cuando el dispositivo disponga de este módulo. Wi-Fi es una abreviatura de Wireless Fidelity (Fidelidad inalámbrica) y es un mecanismo de conexión de dispositivos electrónicos de forma inalámbrica. La clase más importante de la API WIFI de Android se denomina WifiManager y permite al desarrollador realizar todo tipo de operaciones sobre este módulo: buscar redes WIFI, obtener información de dichas redes y conectarse a ellas. Podemos realizar una búsqueda de redes con la clase WifiManager a través de su método startScan(), que devuelve una lista de objetos de la clase ScanResult que representan cada una de las redes WIFI encontradas. La información que podemos obtener de estas redes WIFI encontradas es la que se muestra a continuación: - SSID: el SSID (Service Set IDentifier) denomina el nombre de una red inalámbrica que aparece en tu dispositivo cuando quieres conectarte a una red WIFI. Mediante la constante SSID de la clase ScanResult podemos acceder a este nombre. - Seguridad: la seguridad que implementa una red inalámbrica WIFI puede ser: WEP (Wired Equivalent Privacy), WPA (WIFI Protected Access) o WPA2. Podemos acceder a ella a través del segundo parámetro del resultado de la búsqueda de redes, tal y como veremos en el ejemplo que se explica a continuación. - Potencia de señal: la potencia de la señal que emite la red WIFI se mide en dB (decibelios). Para obtener este valor debemos acceder el atributo público level de la clase ScanResult. - BSSID: el BSSID (Basic Service Set Identifier) de una red WIFI es un nombre de identificación único de todos los paquetes que se trasmiten por dicha red. Este identificador BSSID es la dirección física MAC (Media Access Control) del router WIFI de la red. Para obtener este valor debemos acceder el atributo público BSSID de la clase ScanResult. - Canal de frecuencia: el canal de frecuencia indica en qué banda emite la red WIFI. Para obtener este valor debemos acceder el atributo público frecuency de la clase ScanResult. En el Ejemplo 3 de este Unidad vamos a mostrar cómo desarrollar una sencilla aplicación en Android que utiliza la clase WifiManager para buscar las redes WIFI y mostrar su nombre en un listado. Además, si el usuario pulsa sobre una red de la lista, aparece la descripción detallada de esta red ofreciéndole sus datos: BSSID de la red, el canal de frecuencia, la potencia de la señal emitida que le llega al dispositivo Android y el tipo de seguridad que implementa. Es recomendable abrir el Ejemplo 3 de esta Unidad para entender la explicación siguiente.
253
Aula Mentor
Para empezar hemos definido la clase Red que sirve de modelo de datos de una red WIFI. En un objeto de este tipo guardamos los datos de una red WIFI: el SSID, la seguridad, la potencia de señal, el BSSID y el canal de frecuencia de cada una. Veamos el sencillo aspecto de la clase: // Clase que define el modelo de datos que almacena la información // de una red WIFI public class Red {
private private private private private
String String String String String
ssid; seguridad; bssid; frecuencia; potencia;
public Red(String ssid, String seg, String bssid, String frec, String potencia){ this.ssid=ssid; this.seguridad=seg; this.bssid=bssid; this.frecuencia=frec; this.potencia=potencia; } public String getSSID(){ return this.ssid; }
254
public String getSeguridad(){ return this.seguridad; } public String getBSSID(){ return this.bssid; } public String getFrecuencia(){ return this.frecuencia; } public String getPotencia(){ return this.potencia; } }
Como puedes observar en el código anterior, esta clase contiene todos los atributos y métodos necesarios para obtener la información mencionada de una red inalámbrica. La interfaz de usuario de la aplicación se compone de dos Actividades: la Actividad principal contiene un botón que, al pulsarlo, realiza una búsqueda de redes WIFI y muestra el resultado en un listado ListView en la parte inferior de esta Actividad. Si el usuario hace clic en una red del listado, puede visualizar la información detallada de ésta al abrir una segunda Actividad. En código del layout activity_main.xml se incluye el diseño de la Actividad principal:
Una vez expuesto el sencillo diseño de la Actividad, veamos la lógica de ésta en el fichero MainActivity.java: public class MainActivity extends Activity implements OnClickListener, OnItemClickListener { // Matriz del tipo Red donde vamos a guardar las redes detectadas public static ArrayList redes = new ArrayList(); private Button boton; // Lista de redes y su adaptador public static ListView listaredes; public static AdapterElements adaptador; // En la creación de la Actividad buscamos el ListView // y le asignamos su adaptador correspondiente @Override public void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activity_main); this.boton = (Button)findViewById(R. id.boton); this.boton.setOnClickListener(this); adaptador = new AdapterElements(this); listaredes = (ListView)findViewById (R.id.listView) ; listaredes.setAdapter(adaptador); listaredes.setOnItemClickListener(this); } // Si el usuario hace clic en el botón entonces // arrancamos una tarea asíncrona para buscar las // redes WIFI @Override public void onClick(View v) { switch (v.getId()){ case (R.id.boton):
255
Aula Mentor
@SuppressWarnings(“unused”) AsyncTask task = new ProgressTask(this).execute(); break; } }
256
// Evento onClick del listado @Override public void onItemClick(AdapterView arg0, View arg1, int index, long arg3) { // Obtenemos la información de la red Red red = (Red)MainActivity.redes.get(index); String ssid = red.getSSID(); String bssid = red.getBSSID(); String seguridad = red.getSeguridad(); String frecuencia = red.getFrecuencia(); String potencia = red.getPotencia(); // Creamos un Intent nuevo en el que pasamos como parámetro // la información de la red y arranca la Actividad InfoRed Intent infoRedIntent = new Intent(this,InfoRed.class); infoRedIntent.putExtra(“SSID”, ssid); infoRedIntent.putExtra(“SEGURIDAD”, seguridad); infoRedIntent.putExtra(“BSSID”, bssid); infoRedIntent.putExtra(“FRECUENCIA”, frecuencia); infoRedIntent.putExtra(“POTENCIA”, potencia); startActivity(infoRedIntent); } // Clase que define el adaptador del listado con los métodos // habituales class AdapterElements extends ArrayAdapter { Activity context; public AdapterElements(Activity context) { super(context, R.layout.elementoitems, MainActivity.redes); this.context = context; }
public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = context.getLayoutInflater(); View item = inflater.inflate(R. layout.elementoitems, null); TextView lblTitle = (TextView)item.findViewById(R.id.ssidstr); lblTitle.setText (MainActivity.redes.get(position).getSSID()); return(item); } } }
Como se puede observar en el código anterior, el evento onClick del botón invoca a la clase AsyncTask. Como sabes, esta clase se encarga de realizar tareas asíncronas en Android Ya se
estudió en el curso de Iniciación de Mentor. Hemos empleado una tarea asíncrona porque el buscador de redes WIFI puede tardar en responder un tiempo y no es recomendable bloquear el hilo principal de ejecución de la aplicación. En esta Actividad hemos definido el atributo redes con visibilidad static de forma que pueda ser accedido por cualquier Actividad de la aplicación. Este atributo es un ArrayList
U3 Sensores y dispositivos de Android
de objetos de clase Red que permite almacenar todas las redes inalámbricas encontradas en el escáner de redes WIFI. A continuación, se muestra cómo se implementa el escáner de redes WIFI mediante la clase ProgressTask: // Clase que inicia una tarea asíncrona para buscar las redes WIFI public class ProgressTask extends AsyncTask { // Diálogo de progreso private ProgressDialog dialog; private Context context; // Gestor de Android de redes WIFI private WifiManager manWifi; // Lista donde guardamos las redes encontradas private List wifiList; // Constructor de la clase public ProgressTask(Context c){ this.context = c; dialog = new ProgressDialog(context); } // Antes de ejecutar la tarea verificamos si la interfaz WIFI está // activa protected void onPreExecute() { // Buscamos el servicio WIFI this.manWifi = (WifiManager)this.context.getSystemService (Context.WIFI_SERVICE); // Si no esta activo mostramos un mensaje y no seguimos adelante if (!this.manWifi.isWifiEnabled()){ Toast.makeText(context, “ERROR: el dispositivo no dispone de interfaz WIFI o ésta no se encuentra activada.”, Toast.LENGTH_LONG).show(); } else // Si no, mostramos el diálogo de progreso { this.dialog.setMessage(“Escaneando redes WiFi...”); this.dialog.show(); } } // Al acabar ocultamos el diálogo de progreso y actualizamos el // adaptador de la Actividad principal @Override protected void onPostExecute ( final Boolean success) { if (dialog.isShowing()) { dialog.dismiss(); } if (success) { MainActivity.adaptador.notifyDataSetChanged(); } } // Sentencias que se ejecutan en segundo plano protected Boolean doInBackground (final String... args) {
257
Aula Mentor
// Si no está activa la interfaz WIFI hemos acabado // incorrectamente if (!this.manWifi.isWifiEnabled()) return false; // Buscamos las redes WIFI this.manWifi.startScan(); // Obtenemos los resultado de la búsqueda this.wifiList = this.manWifi.getScanResults(); // Limpiamos la matriz de resultados de la Actividad principal MainActivity.redes.clear(); // Recorremos todos los resultados de la búsqueda de redes y los // vamos pasando a la matriz del tipo Red for(int i = 0; i < wifiList. size () ; i++) { ScanResult scan_res = wifiList.get(i) ; String SSID = scan_res.SSID; String BSSID = scan_res.BSSID; String frec = Integer.valueOf(scan_res.frequency).toString(); String pot = Integer.valueOf(scan_res.level).toString(); String seg = scan_res.toString().split(“,”)[1].split(“:”)[1]; MainActivity.redes.add(new Red(SSID, seg, BSSID, frec, pot)); } // Indicamos que la tarea ha acabado correctamente return true; } } // end clase
258
En el código anterior se pueden observar los tres estados por los que pasa la clase ProgressTask: onPreExecute, doInBackground y onPostExecute. La búsqueda de redes se realiza en segundo plano mediante el método dolnBackground y, mientras dura la ejecución de dicho escáner, se muestra al usuario una alerta del tipo Dialog que le informa de que se está realizando una búsqueda de redes WIFI. La búsqueda de redes se realiza mediante la clase WifiManager usando su método startScan(). Posteriormente, utilizamos el método getScanResult() para obtener una lista de objetos del tipo ScanResult que contiene la información de cada red WIFI. Por último, recorremos dicha lista procesando cada objeto ScanResult y obteniendo los datos necesarios para el presente ejemplo. Una vez terminada la lógica del método doInBackground, se finaliza la ejecución de la clase ProgressTask mediante el método onPostExecute() liberando el Dialog y refrescando los datos del adaptador de la Actividad principal. Esta clase principal implementa la interface OnItemClickListener que indica las sentencias que se deben ejecutar cuando el usuario hace clic en una opción de la lista. En este bloque de código obtenemos la información de la red WIFI pulsada y la enviamos a otra Actividad que se encarga de mostrar este detalle. Como sabes, el envío de datos o parámetros entre Actividades se realiza a través del método putExtra() de la clase Intent. El diseño layout de la Actividad del fichero activity_info_red.xml muestra los detalles de la red WIFI seleccionada. Es el siguiente:
U3 Sensores y dispositivos de Android
El diseño de esta Actividad consta de cinco Vistas del tipo TextView que se encargan de mostrar la información enviada por la Actividad principal de la aplicación. Veamos la lógica de esta Actividad secundaria llamada InfoRed: public class InfoRed extends Activity { private TextView tv_ssid; private TextView tv_seguridad; private TextView tv_bssid; private TextView tv_frecuencia; private TextView tv_potencia; // OnCreate de la Actividad public void onCreate(Bundle savedlnstanceState){ super.onCreate(savedlnstanceState); // Asignamos el layout y buscamos sus Vistas setContentView(R.layout.activity_info_red); this.tv_ssid =(TextView)findViewById(R.id.SSIDTV); this.tv_seguridad = (TextView)findViewById(R.id.SeguridadTV) ; this.tv_bssid = (TextView)findViewById(R.id.BSSIDTV) ;
259
Aula Mentor
this.tv_frecuencia = (TextView)findViewById(R.id.FrecuenciaTV) ; this.tv_potencia = (TextView)findViewById(R.id.PotenciaTV) ;
// Obtenemos los parámetros del Intent y los mostramos en las // Vistas
Bundle extras = getIntent().getExtras(); if (extras != null){ String ssid = extras.getString(“SSID”); String security = extras.getString(“SEGURIDAD”); String bssid = extras.getString(“BSSID”); String frec = extras.getString(“FRECUENCIA”); String power = extras.getString(“POTENCIA”); this.tv_ssid.append(ssid); this.tv_seguridad.append(security); this.tv_bssid.append(bssid); this.tv_frecuencia.append(frec); this.tv_potencia.append(power); } } // end OnCreate }
Como puedes observar en el código anterior, la información enviada es recogida mediante el método getExtras() de la clase Intent. La información se recoge utilizando el valor de la clave y el tipo de dato enviado. Una vez obtenida esta información, se añade esta información a la Vista correspondiente del tipo TextView mediante el método append(). 260
Finalmente, para poder ejecutar esta aplicación es necesario que tenga permisos de acceso a la interfaz WIFI del dispositivo. Para ello, hay que incluir en el fichero AndroidManifest.xml las siguientes etiquetas:
Además, para poder depurar la aplicación en un dispositivo real de Android es necesario indicarlo en el archivo manifest en la etiqueta mediante el atributo android:debuggable=”true”.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 3 (WIFI) de la Unidad 3. Estudia el código fuente y ejecútalo en un dispositivo Android para ver el resultado del programa anterior, en el que hemos utilizado su interfaz WIFI.
Desgraciadamente, el AVD del SDK de Android no incluye la interfaz WIFI, ni siquiera mediante el Simulador de Sensores. Por lo tanto, es necesario probar esta aplicación en un dispositivo real de Android.
U3 Sensores y dispositivos de Android
Si ejecutas en Eclipse ADT este Ejemplo 3 en un dispositivo real, verás que se muestra la siguiente aplicación:
261
Para poder usar un dispositivo real desde Eclipse ADT es necesario conectar este dispositivo mediante un cable al ordenador y modificar sus Ajustes en las opciones siguientes: En “Opciones del desarrollador”, marcar “Depuración de USB”. En “Seguridad”, señalar “Fuentes desconocidas”.
4.2 Módulo Bluetooth El SDK de Android también ofrece soporte para la tecnología Bluetooth que permite a un dispositivo intercambiar datos de forma inalámbrica con otros dispositivos cercanos. Esta funcionalidad permite realizar lo siguiente: - Búsqueda de otros dispositivos Bluetooth. - Consultar los dispositivos Bluetooth emparejados con el nuestro. - Establecer canales de comunicación RFCOMM (Radio Frecuency Communication) utilizando, para simular, comunicaciones propias de puertos serie como los modem de datos. - Conectar con otros dispositivos a través del descubrimiento de servicios y transferir datos entre ellos. - Administrar conexiones múltiples.
Aula Mentor
Las clases más importantes (no incluimos todas) de la API Bluetooth de Android son: - BluetoothAdapter: permite realizar todo tipo de operaciones sobre este módulo, como buscar dispositivos bluetooth con el método startDiscovery() y obtener información de éstos. - BluetoothDevice: representa un dispositivo bluetooth remoto con sus atributos y propiedades correspondientes. - BluetoothSocket: permite la vinculación entre dispositivos y la trasmisión de datos. La diferencia principal con el módulo anterior WIFI está en que en este módulo Bluetooth es necesario definir un receptor de mensajes del tipo BroadcastReceiver para recibir asíncronamente los dispositivos encontrados mediante mensajes del BluetoothAdapter. En este apartado, se pretende mostrar al alumno de forma práctica cómo buscar dispositivos Bluetooth en el sistema operativo Android. Es recomendable abrir el Ejemplo 4 de esta Unidad para seguir la explicación siguiente. Hemos desarrollado este Ejemplo 4 con la misma estructura que el anterior Ejemplo 3. Por esto, no mostraremos el código fuente completo del mismo ya que se repiten algunas partes del código. Sólo estudiaremos la parte nueva. Podemos encontrar las diferencias en la clase principal de la aplicación MainActivity, que tiene este contenido:
262
public class MainActivity extends Activity implements OnClickListener, OnItemClickListener { private ProgressDialog dialog; // Matriz del tipo Red donde vamos a guardar los dispositivos // detectados public static ArrayList dispositivos = new ArrayList(); // Adaptador de Bluetooth private BluetoothAdapter mBtAdapter; // Lista de dispositivos y su adaptador public static ListView listadispositivos; public static AdapterElements adaptador; // BroadcastReceiver que sirve de método callback para // recibir los eventos del BluetoothAdapter private final BroadcastReceiver blueReceiver = new BroadcastReceiver() { @Override public void onReceive(Context arg0, Intent argl) { // Obtenemos la ACTION String action = argl.getAction(); // Si hemos encontrado un dispositivo leemos sus datos y los // añadimos a la matriz de datos if (BluetoothDevice.ACTION_FOUND.equals(action)) { // Obtenemos los datos del dispositivo BluetoothDevice device = argl.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // Si el dispositivo no está vinculado obtenemos sus datos if (device.getBondState() != BluetoothDevice.BOND_BONDED){ dispositivos.add(new BTooth(device.getName(), device.getAddress(), getBTMajorDeviceClass(device.getBluetoothClass().
U3 Sensores y dispositivos de Android
getMajorDeviceClass()), device.getBondState() != BluetoothDevice.BOND_BONDED ? “DISPONIBLE” : “VINCULADO”)); } } else // Si hemos acabado la búsqueda ocultamos el diálogo y // refrescamos el adaptador del ListView if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { if (dialog.isShowing()) { dialog.dismiss(); } adaptador.notifyDataSetChanged(); } } // end onReceive // Método que traduce el tipo de dispositivo Bluetooth private String getBTMajorDeviceClass(int major){ switch(major){ case BluetoothClass.Device.Major.AUDIO_VIDEO: return “AUDIO_VIDEO”; case BluetoothClass.Device.Major.COMPUTER: return “ORDENADOR”; case BluetoothClass.Device.Major.HEALTH: return “SALUD”; case BluetoothClass.Device.Major.IMAGING: return “FOTOGRAFÍA”; case BluetoothClass.Device.Major.MISC: return “VARIOS”; case BluetoothClass.Device.Major.NETWORKING: return “REDES”; case BluetoothClass.Device.Major.PERIPHERAL: return “PERIFÉRICO”; case BluetoothClass.Device.Major.PHONE: return “TELÉFONO”; case BluetoothClass.Device.Major.TOY: return “JUGUETE”; case BluetoothClass.Device.Major.UNCATEGORIZED: return “SIN CATEGORÍA”; case BluetoothClass.Device.Major.WEARABLE: return “PORTABLE”; default: return “DESCONOCIDO”; } } }; //fin de la creación de blueReceiver // En la creación de la Actividad buscamos el ListView // y le asignamos su adaptador correspondiente @Override public void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); // La orientación es siempre vertical this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_POR TRAIT); setContentView(R.layout.activity_main);
263
Aula Mentor
Button boton = (Button)findViewById(R.id.boton); boton.setOnClickListener(this); dialog = new ProgressDialog(this); adaptador = new AdapterElements(this); listadispositivos = (ListView)findViewById(R.id.listView) ; listadispositivos.setAdapter(adaptador); listadispositivos.setOnItemClickListener(this); } // Si el usuario hace clic en el botón entonces // arrancamos las sentencias de búsqueda de // dispositivos Bluetooth
264
@Override public void onClick(View v) { switch (v.getId()) { case (R.id.boton): // Buscamos el adaptador Bluetooth mBtAdapter = BluetoothAdapter.getDefaultAdapter(); // Si no existe mostramos un mensaje de error if (mBtAdapter == null) { Toast.makeText(this, “ERROR: dispositivo Bluetooth no está disponible.”, Toast.LENGTH_LONG).show(); return; } // Si existe el dispositivo pero no está activo solicitamos // al sistema mediante un Intent que lo active if (!mBtAdapter.isEnabled()) { Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) ; this.startActivity(enableIntent); // Nota: podemos ejecutar el Intent anterior mediante // la orden startActivityForResult y esperar a la // vuelta de éste para seguir con el descubrimiento // de los dispositivos. return; } // Mostramos una ventana de progreso this.dialog.setMessage(“Buscando dispositivos Bluetooth...”); this.dialog.show(); // Limpiamos la matriz de resultados dispositivos.clear(); // Registramos 2 Intent con un filtro para recepción de // eventos cuando se encuentra Intent un dispositivo // Bluetooth o se acaba la búsqueda IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); // Indicamos el método callback bluereceiver que se // ejecutará cada vez que se encuentre un dispositivo this.registerReceiver(blueReceiver, filter); filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); // Indicamos el método callback bluereceiver que se // ejecutará una vez acabe la búsqueda de dispositivos this.registerReceiver(blueReceiver, filter);
U3 Sensores y dispositivos de Android
mBtAdapter.startDiscovery(); break; } } // Evento onClick del listado @Override public void onItemClick(AdapterView arg0, View view, int index, long arg3){ // Obtenemos la información del dispositivo BTooth bluetooth = MainActivity.dispositivos.get(index); String nombre = bluetooth.getNombre(); String address = bluetooth.getAddress(); String tipo = bluetooth.getTipo(); String vinculado = bluetooth.getVinculado(); // Creamos un Intent nuevo en el que pasamos como parámetros // la información del dispositivo y arranca la Actividad //InfoBluetooth Intent i_btinfo = new Intent(this,InfoBluetooth.class); i_btinfo.putExtra(“NOMBRE”, nombre); i_btinfo.putExtra(“ADDRESS”, address); i_btinfo.putExtra(“TIPO”, tipo); i_btinfo.putExtra(“VINCULADO”, vinculado); startActivity(i_btinfo); } // Clase que define el adaptador del listado con los métodos // habituales class AdapterElements extends ArrayAdapter { Activity context; public AdapterElements(Activity context) { super(context, R.layout.elementoitems, MainActivity.dispositivos); this.context = context; }
public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = context.getLayoutInflater(); View item = inflater.inflate(R. layout.elementoitems, null); TextView lblTitle = (TextView)item.findViewById(R.id.nombrestr); lblTitle.setText(MainActivity.dispositivos.get(position). getNombre()); return(item); } } }
Como podemos ver en el código anterior, para realizar una búsqueda de dispositivos Bluetooth debemos comprobar que el módulo Bluetooth está presente en el dispositivo en cuestión. El módulo Bluetooth es instanciado a través del método getDefaultAdapter() de la clase BluetoothAdapter.
Si dicha instancia devuelve un valor null, significa que el módulo Bluetooth no está disponible en el dispositivo Android. Sin embargo, si la instancia devuelve un valor distinto de
265
Aula Mentor
266
null, pero su método isEnabled() devuelve un valor falso, revela que el módulo Bluetooth está apagado. En el caso de que el módulo Bluetooth se encuentre apagado, es posible encenderlo automáticamente creando un Intent del sistema operativo mediante la constante BluetoothAdapter.ACTION_REQUESTENABLE. Una vez inicializado el módulo Bluetooth, la aplicación debe registrar en el sistema que desea recibir mensajes de los eventos ACTIONFOUND y ACTION_DISCOVERY_FINISHED del sistema operativo. Estos mensajes indican que se ha encontrado un dispositivo Bluetooth durante la búsqueda y que ha finalizado la búsqueda respectivamente. Para poder recibir dichos mensajes hay que definir un receptor de mensaje del sistema de la clase BroadcastReceiver que se encarga de esta tarea. Además, debemos llamar al método startDiscovery() de la clase BluetoothAdapter para iniciar la búsqueda de dispositivos. Todas estas operaciones anteriores se han implementado en el evento onClick del botón de la aplicación. Mediante un objeto de tipo BroadcastReceiver llamado blueReceiver se filtran los mensajes recibidos a través del parámetro arg1 que es un Intent. Mediante el método getAction() de este Intent podemos obtener el tipo de mensaje (ACTION) recogido y ejecutar el bloque de sentencias que corresponda. Por ejemplo, en el caso de recibir el mensaje de que se ha encontrado un dispositivo Bluetooth usamos el método getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) de este Intent para obtener sus características. Finalmente, para poder ejecutar esta aplicación es necesario que tenga permisos de acceso a la interfaz WIFI del dispositivo. Para ello, hay que incluir en el fichero AndroidManifest. xml las siguientes etiquetas:
Además, para poder depurar la aplicación en un dispositivo real de Android, es necesario indicarlo en el archivo manifest en la etiqueta mediante el atributo android:debuggable=”true”. Es sencillo mejorar la aplicación para incluir funcionalidades nuevas, como la vinculación entre dispositivos y la trasmisión de datos mediante la clase BluetoothSocket. En la Guía oficial del desarrollador Bluetooth de Android puedes encontrar más información, así como ejemplos descriptivos.
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 4 (Bluetooth) de la Unidad 3. Estudia el código fuente y ejecútalo en un dispositivo Android para ver el resultado del programa anterior, en el que hemos utilizado su módulo Bluetooth.
Desgraciadamente, el AVD del SDK de Android no incluye el módulo Bluetooth, ni siquiera mediante el Simulador de Sensores. Por lo tanto, es necesario probar esta aplicación en un dispositivo real de Android.
U3 Sensores y dispositivos de Android
Si ejecutas en Eclipse ADT este Ejemplo 4 en un dispositivo real, verás que se muestra la siguiente aplicación:
267 Si ejecutas esta aplicación en tu dispositivo físico Android, verás que en el listado únicamente aparecen los dispositivos que no están vinculados a él.
Para poder usar un dispositivo real desde Eclipse ADT es necesario conectar este dispositivo mediante un cable al ordenador y modificar sus Ajustes en las opciones siguientes: En “Opciones del desarrollador”, marcar “Depuración de USB”. En “Seguridad”, señalar “Fuentes desconocidas”.
4.3 Cámara de fotos Casi todos los dispositivos Android integran una cámara de fotos, incluso, algunos disponen de una cámara frontal y otra en la parte posterior. El SDK de Android incluye las clases Java necesarias para gestionar una o varias cámaras de fotos. Existen dos formas de gestionar la cámara de fotos: - Utilizando un Intent que gestiona por nosotros la cámara de fotos y devuelve la imagen a la aplicación que lo ha iniciado. - Integrando directamente la cámara en la aplicación utilizando las librerías (API) de Android.
Aula Mentor
4.3.1 Ejemplo de cámara mediante un Intent Como hemos comentado, es muy sencillo utilizar un Intent de Android para lanzar la aplicación de cámara de fotos de un dispositivo y, posteriormente, obtener la foto tomada. Fíjate en el código fuente del siguiente ejemplo: public class ImagePickActivity extends Activity { // Constante utilizada en el Intent private static final int REQUEST_CODE = 1; // Bitmap donde almacenamos la imagen private Bitmap bitmap; // Vistas de tipo Imagen de la interfaz usuario private ImageView imageView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Recuperamos el ImageView imageView = (ImageView) findViewById(R.id.resultado); }
268
// En el layout de la aplicación hemos definido un botón y asociado // este método como onClick public void capturaImage(View View) { // Definimos el Intent que lanza la aplicación de cámara de fotos Intent intent = new Intent(); intent.setType(“image/*”); intent.setAction(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(intent, REQUEST_CODE); } // Método @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) try { // Si el bitmap ya existe tenemos que vaciarlo de datos if (bitmap != null) { bitmap.recycle(); } // Leemos los datos devuelto por el Intent en un stream InputStream stream = getContentResolver().openInputStream(data.getData()); // Pasamos los datos al bitmap bitmap = BitmapFactory.decodeStream(stream); // Liberamos el stream stream.close(); // Asignamos la imagen al ImageView imageView.setImageBitmap(bitmap); } catch (FileNotFoundException e) { e.printStackTrace();
U3 Sensores y dispositivos de Android
}
}
} catch (IOException e) { e.printStackTrace(); } super.onActivityResult(requestCode, resultCode, data);
Como puedes ver en el código anterior, basta con utilizar una Intención con estas características: Tipo: image/* Acción: ACTION_GET_CONTENT Categoría: CATEGORY_OPENABLE
De esta forma podemos ejecutar la aplicación interna del dispositivo Android y, posteriormente, esperar el evento onActivityResult(), para capturar el resultado, que es una imagen. Puedes incluir este código en tu propia aplicación cuando desees integrar la captura de fotos en tus aplicaciones.
4.3.2 Ejemplo de cámara mediante API de Android El SDK de Android incluye el soporte nativo, mediante una librería, que permite utilizar las cámaras de los dispositivos para sacar fotos y grabar vídeos. Existe gran cantidad de aplicaciones que hacen uso de esta cámara de fotos mejorando la experiencia del usuario y abriendo un nuevo campo de integración entre el mundo real y el virtual. Veamos las clases de la API de la cámara de Android: - Camera: ésta es la clase básica de la API para controlar la cámara, tomar fotos y grabar vídeos. Como un dispositivo Android puede tener varias cámaras, para distinguirlas, esta clase utiliza la variable de tipo entero cameraId. - CameraInfo: clase que contiene la información de la cámara con el identificativo cameraId. - SurfaceView: esta clase se usa para mostrar al usuario la previsualización de la cámara de fotos. - MediaRecorder: esta clase se emplea para grabar vídeos. En este apartado se pretende mostrar al alumno, de forma práctica, cómo utilizar la cámara de fotos. Es recomendable abrir el Ejemplo 5 de esta Unidad para seguir la explicación que se ofrece. La aplicación que desarrollamos permitirá capturar una foto con la cámara o seleccionar una imagen de la galería del dispositivo y mostrarla en la interfaz de usuario. En código del layout activity_main.xml se incluye el diseño de la Actividad principal:
269
Aula Mentor
270
En el diseño anterior hemos incluido el elemento FrameLayout, que sirve como contenedor de la previsualización de la cámara de fotos o de la imagen seleccionada por el usuario en la galería. Si ahora abrimos la clase principal de la aplicación MainActivity, vemos el siguiente contenido: public class MainActivity extends Activity { // Constantes que se usan en la aplicación public final static String DEBUG_TAG = “CAMARA_FOTOS”; private static int SELECT_PICTURE = 1; // Botón que se usa de disparador de la cámara public Button boton_disparador; // Objeto que sive de interfaz con la cámara public static Camera camara; // Objeto de previsualización de la cámara de fotos Preview preview; // Id de la cámara seleccionada del dispositivo public static int cameraId = 0; // Evento onCreate de la Actividad @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Buscamos el botón disparador y asociamos su evento onClick boton_disparador = (Button)findViewById(R. id.boton_disparador); boton_disparador.setOnClickListener(new OnClickListener() {
U3 Sensores y dispositivos de Android
@Override public void onClick(View v) { // Si la aplicación está visualizando una imagen entonces... if (boton_disparador.getText().equals(“Ver previsualización”)) { // Volvemos a modo previsualización de la cámara boton_disparador.setText(“Disparador”); // Intercambiamos en el frame el objeto de previsualización ((FrameLayout)findViewById(R.id.preview)).removeAllViews(); ((FrameLayout)findViewById(R.id.preview)).addView(preview); return; } // Si no se encuentra la cámara entonces no podemos sacar // fotos if (cameraId < 0) { Toast.makeText(getBaseContext(), “ERROR: no se encuentra la cámara de fotos en este dispositivo.”, Toast.LENGTH_LONG).show(); } else { // Si encontramos la cámara entonces paramos la // previsualización, tomamos la foto y volvemos a // previsualizar camara.stopPreview(); // Tomamos la foto indicando el método callback // PictureCallback en la clase FotoHandler para que // trate la foto una vez se ha sacado. camara.takePicture(null, null, new FotoHandler(getApplicationContext())); camara.startPreview(); } } }); // end onClick // Botón galería Button boton_galeria = (Button)findViewById(R. id.boton_galeria); // Evento onClick del botón galería boton_galeria.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Definimos un Intent del tipo siguiente Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media. INTERNAL_CONTENT_URI); // Iniciamos la Actividad con este Intent indicando que // queremos seleccionar una imagen startActivityForResult(intent, SELECT_PICTURE); } }); // A continuación, vamos a comprobar si el dispositivo tiene cámara // de fotos if (!getPackageManager() .hasSystemFeature(PackageManager.FEATURE_CAMERA)) { Toast.makeText(this, “ERROR: este dispositivo no tiene cámara de fotos.”, Toast.LENGTH_LONG).show(); } else {
271
Aula Mentor
// Si tiene cámara de fotos, obtenemos el ID de la misma cameraId = encuentraCamaraPrincipal(); if (cameraId < 0) { Toast.makeText(this, “ERROR: no se encuentra la cámara de fotos en este dispositivo.”, Toast.LENGTH_LONG).show(); } else // Si obtenemos el ID de la cámara entonces creamos el objeto // de previsualización y éste último lo asignamos al frame camara = Camera.open(cameraId); preview = new Preview(this); ((FrameLayout) findViewById(R.id.preview)).addView(preview);
}
}
272
// Función que se ejecuta cuando concluye el intent en el // que se solicita una imagen de la galería @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Si el resultado es OK if (requestCode == SELECT_PICTURE && resultCode == RESULT_OK){ // Recibimos el URI de la imagen y construimos un Bitmap a // partir de un stream de Bytes Uri selectedImage = data.getData(); InputStream is; try { // Obtenemos la imagen seleccionada is = getContentResolver(). openInputStream(selectedImage); // Leemos la imagen BufferedInputStream bis = new BufferedInputStream(is); Bitmap bitmap = BitmapFactory.decodeStream(bis); // Definimos una Vista de tipo imagen y la asignamos al // frame ImageView imagen = new ImageView(this); imagen.setImageBitmap(bitmap); // Paramos la previsualización de la cámara camara.stopPreview(); // Asignamos al frame la Vista ImagenView ((FrameLayout)findViewById(R.id.preview)).removeAllViews(); ((FrameLayout) findViewById(R.id.preview)).addView(imagen); boton_disparador.setText(“Ver previsualización”); } catch (FileNotFoundException e) {} } } // Se invoca cuando la Actividad vuelve a estar Activa @Override public void onResume() { // La primera vez no podemos reiniciar la previsualización // porque no hemos detectado la cámara. Sólo mostramos la // previsualización si no estamos mostrando una foto if (cameraId>0 && boton_disparador.getText().equals(“Disparador”)) { camara.startPreview(); } super.onResume();
U3 Sensores y dispositivos de Android
} // Método que busca la cámara principal private int encuentraCamaraPrincipal(){ cameraId=0; // Obtenemos el nº total de cámaras del dispositivo int numberOfCameras = Camera.getNumberOfCameras(); // Recorremos todas las cámaras for (int i = 0; i < numberOfCameras; i++) { // Obtenemos la información de la cámara i CameraInfo info = new CameraInfo(); Camera.getCameraInfo(i, info); // Si es la cámara de atrás indicamos que hemos acabado if (info.facing == CameraInfo.CAMERA_FACING_BACK) { Log.d(DEBUG_TAG, “¡Cámara encontrada!”); cameraId = i; break; } } return cameraId; } // Evento que ocurre cuando la Actividad pasa a segundo plano @Override protected void onPause() { // Es muy importante parar la previsualización para // no consumir recursos del sistema como CPU if (camara != null) { camara.stopPreview(); } super.onPause(); } // Evento que se lanza cuando se destruye la Actividad @Override protected void onDestroy(){ // Paramos la previsualización camara.setPreviewCallback(null); preview = null; // Liberamos el objeto cámara camara.release(); camara = null; super.onDestroy(); } } // end clase
En el código anterior puedes ver que fijamos una constante para identificar la acción realizada (seleccionar imagen de la galería). Además, definimos las variables de la clase Camera de Android para gestionar la cámara y Preview de previsualización de la cámara definida por esta aplicación y que estudiaremos más adelante. En el evento onCreate() de esta Actividad hemos incluido las sentencias necesarias para asignar a los dos botones las operaciones que deben realizar cuando un usuario haga clic sobre ellos. En el caso del botón “Disparador” se aplica la siguiente lógica: si el FrameLayout no está mostrando la previsualización, la incluimos para que el usuario pueda sacar una foto. Si no ocurre esto anterior y existe una cámara, entonces tomamos una fotografía mediante el método
273
Aula Mentor
takePicture() clase Camera donde pasamos como parámetro un método callback del tipo
PictureCallback que hemos implementado en la clase FotoHandler de esta aplicación, para que almacene la foto una vez se ha tomado. Para la funcionalidad del botón “Ver Galería” hemos utilizado un Intent del tipo MediaStore.ACTION_IMAGE_CAPTURE y lo hemos ejecutado mediante la orden startActivityForResult() para recibir la imagen seleccionada por el usuario en el método onActivityResult(). En este último método recibimos el URI de la imagen y construimos un Bitmap a partir de un stream de Bytes para cargarlo en una Vista de tipo imagen y la asignamos al frame para mostrarla al usuario parando la previsualización de la cámara de fotos con la sentencia camara.stopPreview(). Además de definir las órdenes que deben ejecutar los botones, hemos insertado la sentencia siguiente que valida si el dispositivo tiene cámara de fotos: getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
Si existe una cámara, entonces buscamos identificarla mediante el cameraId para su uso posterior mediante el método local encuentraCamaraPrincipal(). Este método obtiene el número total de cámaras del dispositivo con el método Camera.getNumberOfCameras() y, a través de un bucle, la clase CameraInfo extrae las características de cada cámara buscando la cámara principal trasera que se define con la constante CAMERA_FACING_BACK.
274
Al final del código de esta clase MainActivity hemos definido los métodos siguientes del ciclo de vida de la actividad: - onPause(): cuando la Actividad pasa al segundo plano, debemos parar la previsualización de la cámara con la orden stopPreview(), ya que consume recursos de sistema operativo. - onResume(): en el caso de que sea necesario, se debe volver a iniciar la previsualización de la cámara con startPreview() cuando la Actividad vuelva al primer plano. - onDestroy(): si la Actividad se destruye, debemos eliminar el método callback de la previsualización con camara.setPreviewCallback(null) y, a continuación, liberar la cámara con camara.release().
Es muy importante sobrescribir (override) los métodos anteriores de la Actividad, ya que la cámara de fotos consume muchos recursos del sistema, como CPU y batería, por lo que sólo debe utilizarse cuando sea necesario.
Veamos ahora el contenido de la clase Preview de la aplicación que implementa la previsualización de la cámara: class Preview extends SurfaceView implements SurfaceHolder.Callback { // Superficie que muestra la previsualización SurfaceHolder mHolder; Preview(Context context) { super(context); // Obtenemos el contenedor del SurfaceView y le asignamos // un método para detectar cuándo se crea y se destruye esta // superficie mHolder = getHolder();
U3 Sensores y dispositivos de Android
}
mHolder.addCallback(this);
// Evento onCreate de la superficie public void surfaceCreated(SurfaceHolder holder) { try { // Indicamos a la cámara que la Vista de previsualización // es esta superficie MainActivity.camara.setPreviewDisplay(holder); // A modo de control, definimos el método PreviewCallback() // que ocurre cuando se lanza la previsualización de la // cámara. Realmente no es necesario. MainActivity.camara.setPreviewCallback(new PreviewCallback() { public void onPreviewFrame(byte[] data, Camera arg1) { try { Log.d(MainActivity.DEBUG_TAG, “onPreviewFrame - “ + data.length); } finally {} } }); // end PreviewCallback } catch (IOException e) { e.printStackTrace(); } } // end on surfaceCreated public void surfaceDestroyed(SurfaceHolder holder) { } // Evento que se lanza cuando cambia el tamaño de la superficie public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { // Si cambia el tamaño de la superficie debemos cambiar // el parámetro de la previsualización de acuerdo con éste. if (MainActivity.camara==null) return; Camera.Parameters parameters = MainActivity.camara.getParameters(); parameters.setPreviewSize(w, h); MainActivity.camara.setParameters(parameters); MainActivity.camara.startPreview(); } // Evento que lanza la primera vez que dibujamos la superficie. // Mostramos un texto en color rojo @Override public void draw(Canvas canvas) { super.draw(canvas); Paint p= new Paint(Color.RED); Log.d(MainActivity.DEBUG_TAG,”Dibujo de previsualización”); canvas.drawText(“PREVISUALIZACIÓN”, canvas.getWidth()/2, canvas.getHeight()/2, p ); } } // end Preview
En el código anterior hemos extendido la clase Preview de la clase superficie SurfaceView que hemos estudiado en la Unidad 1. Ésta se encargará de mostrar la previsualización de la cámara de fotos e implementar la interfaz SurfaceHolder.Callback, que ejecutará el código
275
Aula Mentor
cuando haya cambios de esta superficie. Lo primero que hemos hecho es obtener el contenedor del SurfaceView y le asignamos los métodos callback para detectar cuándo se crea, cambia o se destruye esta superficie. A continuación, definimos los métodos de esta interfaz SurfaceHolder.Callback: - surfaceCreated(): este evento ocurre cuando el sistema crea la superficie. En él, indicamos a la cámara que la Vista de previsualización es esta superficie y, para poder depurarla, definimos el método PreviewCallback(), que ocurre cuando el sistema operativo actualiza la previsualización de la cámara. Realmente no es necesario definirlo. - surfaceDestroyed(): este evento ocurre cuando se destruye la superficie. En este caso hemos delegado en la Actividad principal la destrucción de este objeto Preview. También, podríamos haber destruido las variables creadas. - surfaceChanged(): evento que sucede cuando el sistema operativo detecta que se ha producido un cambio en la superficie. En este caso, establecemos los parámetros del tamaño de previsualización de la cámara. - draw(): evento que se lanza la primera vez que se dibuja la superficie. Mostramos un texto en color rojo. Veamos ahora el contenido de la clase FotoHandler de la aplicación que guarda la imagen tomada por la cámara de fotos:
276
// Clase que implementa el método callback PictureCallback para que // trate la foto una vez se ha sacado. public class FotoHandler implements PictureCallback { // Guardamos el contexto para poder hacer un Toast private final Context contexto; public FotoHandler(Context context) { this.contexto = context; } // Debemos definir el evento onPictureTaken @Override public void onPictureTaken(byte[] data, Camera camera) { // Obtenemos el directorio donde vamos a guardar la foto File pictureFileDir = getPicDir(); // Si no existe el directorio y no lo podemos crear // entonces mostramos un error if (!pictureFileDir.exists() && !pictureFileDir.mkdirs()) { Log.d(MainActivity.DEBUG_TAG, “ERROR: no se puede crear el directorio para guardar las fotos.”); Toast.makeText(contexto, “ERROR: no se puede crear el directorio para guardar las fotos.”, Toast.LENGTH_LONG).show(); return; } // Definimos el formato del nombre del archivo SimpleDateFormat dateFormat = new SimpleDateFormat(“dd-mm-yyyy hh mm ss”, Locale.ROOT); String date = dateFormat.format(new Date()); String fotoFile = “Foto_” + date + “.jpg”; String filename = pictureFileDir.getPath() + File.separator + fotoFile; // Usamos un fichero para guardar la foto File pictureFile = new File(filename);
U3 Sensores y dispositivos de Android
// Guardamos la foto try { FileOutputStream fos = new FileOutputStream(pictureFile); fos.write(data); fos.close(); Toast.makeText(contexto, “Se ha guardado la imagen:” + fotoFile, Toast.LENGTH_LONG).show(); } catch (Exception error) { Log.d(MainActivity.DEBUG_TAG, “File” + filename + “no guardada: “ + error.getMessage()); Toast.makeText(contexto, “ERROR: no se puede guardar la imagen.”, Toast.LENGTH_LONG).show(); } // Iniciamos la previsualización de la cámara camera.startPreview(); } // Método que obtiene el directorio PICTURES del sistema private File getPicDir() { File sdDir = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES); return new File(sdDir, “CursoMentor_Unidad3_Ejemplo5”); } } // end clase
Cuando tomamos una fotografía, hay que invocar el método takePicture() de la clase Camera, donde debemos indicar como parámetro un método callback del tipo PictureCallback que hemos implementado en la clase anterior FotoHandler. Este método se encarga de guardar la foto una vez se ha tomado. Puedes observar que la clase implementa el método onPictureTaken, que es el que se encarga de almacenar la imagen en la tarjeta SD del dispositivo Android. Como parámetro de este método usamos la matriz de bytes data que contiene la información de la foto tomada. En este bloque de código seguimos los pasos habituales para almacenar un fichero de tipo binario en la tarjeta SD del dispositivo; en concreto, lo guardamos en el directorio de fotos Environment.DIRECTORY_PICTURES del sistema operativo. Finalmente, para poder ejecutar esta aplicación, es necesario que tenga permisos de acceso a la cámara y tarjeta SD del dispositivo. Para ello, hay que incluir en el fichero AndroidManifest.xml las siguientes etiquetas:
Además, recordamos que para poder depurar la aplicación en un dispositivo real de Android, es necesario indicarlo en el archivo manifest en la etiqueta mediante el atributo android:debuggable=”true”. Es sencillo mejorar esta aplicación incluyendo funcionalidades nuevas, como la grabación de vídeo. En la Guía oficial de Cámara de Android puedes encontrar más información, así como ejemplos descriptivos.
277
Aula Mentor
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 5 (Cámara de fotos) de la Unidad 3. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado la Cámara de fotos.
En este ejemplo, sí podemos utilizar el AVD del SDK de Android configurándolo correctamente. Para ello, debemos editar el dispositivo virtual y elegir en la cámara trasera (“Back Camera”) la opción “Emulated” para que simule una cámara (como hemos hecho en las capturas que aparecen después) o “Webcam0” para utilizar la Webcam de tu ordenador como cámara de fotos:
278
U3 Sensores y dispositivos de Android
Si ejecutas en Eclipse ADT este Ejemplo 5 en el AVD, verás que se muestra la siguiente aplicación:
Si tomas una foto y, a continuación, pulsas el botón “Ver Galería”, verás que no aparece ninguna foto en ésta: 279
Esto se debe a que el AVD, por motivos de rendimiento, no busca automáticamente nuevos contenidos multimedia en la tarjeta SD. Para forzar esta búsqueda debemos hacer clic en la aplicación “Dev Tools” que aparece en el escritorio de aplicaciones de Android del AVD:
Aula Mentor
Después, debemos seleccionar la opción “Media Provider” y hacer clic en el botón “Scan SD card” para que el sistema busque nuevos contenidos multimedia en la tarjeta SD:
Si ahora haces clic en el botón “Galería”, verás que ya puedes seleccionar esa imagen correctamente: 280
En lugar de tener que lanzar manualmente el escaneo de nuevos archivos multimedia, es posible implementar en esta aplicación un mensaje de tipo Broadcast con el Intent correspondiente que actualice la librería multimedia del sistema operativo. En el Ejemplo 1 de la Unidad 1 lo hemos hecho en el método addRecordingToMediaLibrary().
U3 Sensores y dispositivos de Android
4.4 Módulo GPS En la actualidad, existen muchas aplicaciones que hacen uso de la localización del usuario para ofrecer servicios avanzados. Google Maps es, posiblemente, la aplicación que más aprovecha la funcionalidad del GPS para ofrecer al usuario servicios teniendo en cuenta su localización, como, por ejemplo, una guía de rutas o la localización basada en mapas. Sin embargo, los desarrolladores reinventan continuamente el uso de la geolocalización y crean proyectos y aplicaciones que ofrecen al usuario servicios en función de su localización, como, por ejemplo, la posibilidad de obtener recomendaciones de puntos de interés cercanos o compartir información y conversaciones con personas de la misma zona. Existen varios métodos de obtener la localización de un dispositivo móvil, si bien la más conocida es la localización mediante el módulo GPS. También es posible conseguir la posición geográfica de un dispositivo utilizando las antenas de telefonía móvil o mediante los puntos de acceso WI-FI cercanos. Cada uno de estos métodos tiene una precisión, velocidad y consumo de recursos del sistema distintos. Por otra parte, la aplicación al código fuente del modo de funcionamiento de cada uno de estos métodos no es directa ni intuitiva. En este apartado, se pretende mostrar el alumno, de forma práctica, cómo utilizar la funcionalidad de geolocalización que ofrece el SDK de Android a través del módulo GPS que, hoy en día, tiene la mayoría de los dispositivos. Es recomendable abrir el Ejemplo 6 de esta Unidad para seguir la explicación que se ofrece a continuación. La aplicación que desarrollamos muestra los módulos disponibles de localización del dispositivo Android, selecciona el mejor módulo en función de los parámetros indicados por el programador y muestra la posición geográfica del dispositivo y el estado del módulo escogido. En el código del layout activity_main.xml se incluye el diseño de la Actividad principal:
281
Aula Mentor
282
U3 Sensores y dispositivos de Android
Como puedes ver, la interfaz del usuario es sencilla. Se incluyen dos botones que permiten Activar/Actualizar y Desactivar la localización, etiquetas para mostrar los datos al usuario y un listado de tipo ListView para mostrar los módulos de localización disponibles. Si ahora abrimos la clase principal de la aplicación MainActivity vemos el siguiente contenido: public class MainActivity extends Activity { // Definimos el gestor de localizaciones private LocationManager locManager; //Listener para recibir cambios de localización private LocationListener locListener; // Matriz del tipo Red donde vamos a guardar los LocationProvider // detectados public static ArrayList locProviderList = new ArrayList(); // Lista para mostrar los providers de localización y su adaptador public static ListView listaProviders; public static AdapterElements adaptador; // Etiquetas y botones de la aplicación private TextView lblMejorProvider; private Button btnActivar; private Button btnDesactivar; private TextView lblLatitud; private TextView lblLongitud; private TextView lblPrecision; private TextView lblEstado; // Evento onCreate de la Actividad public void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activity_main); // Buscamos las Vistas de la actividad lblMejorProvider = (TextView)findViewById(R.id.lblMejorProvider); btnActivar = (Button)findViewById(R.id.botonActivar); btnDesactivar = (Button)findViewById(R.id.botonDesactivar); lblLatitud = (TextView)findViewById(R.id.lblPosLatitud); lblLongitud = (TextView)findViewById(R.id.lblPosLongitud); lblPrecision = (TextView)findViewById(R.id.lblPosPrecision);
283
Aula Mentor
lblEstado = (TextView)findViewById(R.id.lblEstado); btnDesactivar.setEnabled(false); // Definimos el adaptador del listado de LocationProviders adaptador = new AdapterElements(this); listaProviders = (ListView)findViewById(R.id.listView) ; listaProviders.setAdapter(adaptador); // Mostramos los datos en blanco muestraPosicion(null); // Definimos lo que se debe hacer si el usuario hace clic en los // botones btnActivar.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Activamos el botón Desactivar btnDesactivar.setEnabled(true); // Si ya conocemos el LocationProvider mostramos la // posición última conocida if (btnActivar.getText().equals(“Actualizar datos”)) muestraPosicion(locManager.getLastKnownLocation( LocationManager.GPS_PROVIDER)); // Si no, buscamos el LocationProvider con este método else obtenerGPS(); btnActivar.setText(“Actualizar datos”); } });
284
// Evento que desactiva el LocationManager btnDesactivar.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { btnActivar.setText(“Activar localización”); btnDesactivar.setEnabled(false); // Quitamos el listener para que ya no lleguen // actualizaciones locManager.removeUpdates(locListener); // Mostramos los datos en blanco muestraPosicion(null); // Limpiamos el listado de LocationProviders locProviderList.clear(); adaptador.notifyDataSetChanged(); } }); } // end onCreate // Método que lista todos los LocationProvider disponibles // y muestra la posición actual private void obtenerGPS(){ // Buscamos el LocationManager del sistema operativo locManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE); // Obtenemos la lista de todos los LocationProviders List listaProviders = locManager.getAllProviders(); // Limpiamos el listado local locProviderList.clear(); // Recorremos todos los LocationProviders que hemos obtenido
U3 Sensores y dispositivos de Android
// anteriormente y los pasamos a un matriz global de la Actividad for (int i=0; i-1) { if (m.getData().getInt(“sonido”)==R.raw.rebote) JuegoActivity.soundPool.play(JuegoActivity.idRebote, 1, 1, 0, 0, 1); else if (m.getData().getInt(“sonido”)==R.raw.gameover) JuegoActivity.soundPool.play(JuegoActivity.idGameOver, 1, 1, 1, 0, 1); else if (m.getData().getInt(“sonido”)==R.raw.win) JuegoActivity.soundPool.play(JuegoActivity.idWin, 1, 1, 1, 0, 1); } }; // end Handler } // end constructor // Libera todos los recursos public void liberarTodo() { // Paramos el Hilo y limpiamos this.hilo.setRunning(false); this.hilo.liberarTodo(); // Quitamos el método Callback de esta clase this.removeCallbacks(hilo); hilo = null; // Quitamos el evento onTouch this.setOnTouchListener(null); // Liberamos el Listener del sensor sensorListener = null; // Quitamos el método Callback de la superficie SurfaceHolder holder = getHolder(); holder.removeCallback(this); } // Método que establece el Hilo a la Vista de Juego public void setHilo(JuegoThread elHilo) { // Guardamos el hilo hilo = elHilo; // Delegamos el evento onTouch al Hilo setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { if(hilo!=null) { return hilo.onTouch(event);
U3 Sensores y dispositivos de Android
} else return false; } }); // end onTouch
// Definimos el listener del sensor y lo delegamos también al // Hilo this.sensorListener = new SensorEventListener() { public void onAccuracyChanged(Sensor arg0, int arg1) { // no es necesario } public void onSensorChanged(SensorEvent event) { if(hilo!=null) { if (hilo.isAlive()) { hilo.onSensorChanged(event); } } } }; // end listener sensor
// Indicamos que el usuario puede hacer clic en la // vista y que ésta toma el foco setClickable(true); setFocusable(true); } // Devuelve el Hilo de esta Vista de juego public JuegoThread getHilo() { return hilo; } // Métodos que devuelven o establecen las Vistas internas del juego public TextView getEstadoView() { return mEstadoView; }
public void setEstadoView(TextView mEstadoView) { this.mEstadoView = mEstadoView; }
public TextView getPuntuacionView() { return mPuntuacionView; }
public void setPuntuacionView(TextView mPuntuacionView) { this.mPuntuacionView = mPuntuacionView; }
public Handler getmHandler() { return mHandler; } public void setmHandler(Handler mHandler) { JuegoView.mHandler = mHandler; }
301
Aula Mentor
/* * FUNCIONES DE PANTALLA */ // Si la superficie pierde el foco, entonces pausamos // el juego. Esto ocurre, por ejemplo, si el usuario // pulsa la tecla menú de su dispositivo @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if(hilo!=null) { if (!hasWindowFocus) hilo.pausarJuego(); } }
302
// Al crear la superficie iniciamos el hilo de ésta public void surfaceCreated(SurfaceHolder holder) { if(hilo!=null) { hilo.setRunning(true); // Si el estado del hilo es nuevo entonces lo arrancamos if(hilo.getState() == Thread.State.NEW){ hilo.start(); } else { // Si el hilo se ha terminado, lo reiniamos con el // estado del anterior if(hilo.getState() == Thread.State.TERMINATED){ hilo = new JuegoThread(this, hilo); hilo.setRunning(true); hilo.start(); } } } } // end surfaceCreated // Se lanza cuando cambia la superficie: indicamos al hilo el tamaño // de ésta public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if(hilo!=null) { // Indicamos el nuevo tamaño de la superficie al hilo hilo.setSurfaceSize(width, height); } } // Si la superficie se destruye entonces hay que parar el hilo public void surfaceDestroyed(SurfaceHolder arg0) { if(hilo!=null) { // Paramos el hilo hilo.setRunning(false); } // Unimos el Hilo de actualización con el Hilo principal // de la aplicación ejecutando la orden join // hasta que lo conseguimos. Hay que tener en cuenta que // normalmente los juegos tienen varios hilos funcionando a la // vez por lo que es necesario unirlos para destruirlos. En este
U3 Sensores y dispositivos de Android
// ejemplo hemos utilizado un único hilo. boolean reintentar = true; while (reintentar) { try { if(hilo!=null) { hilo.join(); } reintentar = false; } catch (InterruptedException e) { } } } // end surfaceDestroyed // Métodos que conecta y desconecta el tipo de sensor // TYPE_ORIENTATION con el SensorManager y el listener // correspondiente @SuppressWarnings(“deprecation”) public void conectaSensor(SensorManager sm) { // Es importante marcar con SENSOR_DELAY_GAME que el // dispositivo debe indicar con el menor tiempo posible los // cambios de orientación para que el jugador note fluidez en // las respuestas de sus movimientos. sm.registerListener(this.sensorListener, sm.getDefaultSensor(Sensor.TYPE_ORIENTATION), SensorManager.SENSOR_DELAY_GAME); } public void desconectaSensor(SensorManager sm) { sm.unregisterListener(this.sensorListener); this.sensorListener = null; } } // end class
Veamos el código anterior que implementa la superficie que define el área del juego. Tal y como hemos estudiado en la Unidad 1, para utilizar una superficie debemos crear una subclase que se extienda de la clase SurfaceView e implemente la interfaz SurfaceHolder.Callback para poder gestionar los métodos relacionados con esta superficie: En el constructor de la clase del código anterior puedes observar que se invoca al método addCallback(this) para gestionar los eventos del SurfaceView y poder acceder a la citada superficie. Para actualizar las Vistas de esta superficie (hilo principal) desde el Hilo del juego es recomendable utilizar un Handler de Java. Este mecanismo permite trasmitir información entre hilos de una aplicación. Para ello, en el constructor de esta superficie definimos un Handler que comunica información desde el Hilo hacia la Vista del juego que recibe mensajes de éste, como actualizar la puntuación o cambiar el estado del juego. Para ello, implementamos el método handleMessage() que gestiona los mensajes recibidos del hilo y actualiza las Vistas de la superficie. En esta aplicación hemos implementado algunos de los eventos que lanza una superficie. Estudiemos las sentencias más importantes de éstos. En el método surfaceCreated() del SurfaceView ejecutamos un hilo que hemos creado en el constructor de la clase JuegoActivity y que se ha recibido en la superficie mediante el método setHilo() desde la Actividad principal. Después, establecemos el estado del hilo al modo ejecución y lo arrancamos con la orden start(). La función de este hilo es actualizar la
303
Aula Mentor
interfaz de usuario dibujándola cuando sea necesario. El método SurfaceDestroyed() se lanza cada vez que la aplicación pasa a segundo plano y el SurfaceView se va a destruir junto con todo su contenido. Por lo tanto, es necesario parar el hilo. Para ello, unimos el Hilo del juego con el Hilo principal de la de la aplicación invocando la orden join hasta que lo conseguimos. Si hubiera más de dos hilos, sería necesario unir todos estos hilos para destruirlos correctamente. El evento onWindowFocusChanged() se invoca si la superficie pierde el foco y debemos parar el juego. Esto ocurre, por ejemplo, si el usuario pulsa la tecla menú de su dispositivo. Como hemos comentado anteriormente, es imprescindible que la interacción del usuario con la aplicación sea suave y los gráficos se muestren sin parpadeos. Para conseguirlo, tenemos que ejecutar dos hilos en la aplicación: el hilo principal, que se encarga de gestionar la Actividad de la aplicación, y el segundo hilo, que se encarga de dibujar la superficie, de gestionar la interacción del usuario con ésta y los cambios del sensor. La razón principal de esta división de tareas es que, como estudiamos en el curso de Iniciación a Android, no debemos bloquear nunca el hilo principal de una aplicación. Por ejemplo, si el usuario presiona la pantalla táctil puede crear paradas en la ejecución del código.
Es muy importante separar siempre el código en dos o más hilos: el hilo principal de la aplicación y un segundo o tercer hilo que se encargan de dibujar las superficies, de gestionar la interacción con éstas y de obtener datos de los sensores, para que el usuario obtenga una sensación de juego óptima. 304
Hemos implementado el Hilo JuegoThread para actualizar el dibujo del objeto SurfaceView, gestionar los toques sobre éste y obtener datos de los sensores. En el constructor de este hilo pasamos como parámetro la referencia al objeto JuegoView, para poder modificar el contenido de ésta. Por esto, en el método surfaceChanged() del JuegoView obtenemos el tamaño de la pantalla del dispositivo Android y se lo pasamos al hilo JuegoThread. No hemos definimos el método onDraw() de la superficie ya que será el hilo el encargado de dibujarla. En el método setHilo() de esta clase hemos delegado el método onTouchEvent() al Hilo mediante la sentencia setOnTouchListener(). También definimos, en este método, el listener del sensor y lo delegamos nuevamente al Hilo. Este listener lo utilizamos en el método conectaSensor(), donde mediante la orden registerListener() del ServiceManager lo registramos indicando con SENSOR_DELAY_ GAME que el dispositivo debe comunicar con el menor tiempo posible los cambios de orientación (TYPE_ORIENTATION), para que el jugador note fluidez en las respuestas a sus movimientos. Aunque el tipo de sensor TYPE_ORIENTATION es obsoleto (deprecated), lo hemos utilizado por sencillez en este Ejemplo. Si no queremos usar un sensor obsoleto, debemos utilizar una combinación de los sensores TYPE_ACCELEROMETER y TYPE_MAGNETIC_FIELD. A continuación, se muestra cómo se implementa este hilo en el archivo JuegoThread.java: public class JuegoThread extends Thread{ // Estados del juego public static final int ESTADO_PERDER = 1; public static final int ESTADO_PAUSA = 2; public static final int ESTADO_PREPARADO = 3;
U3 Sensores y dispositivos de Android
public static final int ESTADO_EJECUCION = 4; public static final int ESTADO_GANAR = 5; // Variable que almacena uno de los estados del juego anteriores private int mEstado = 1; // Indica si se debe ejecutar el Hilo private boolean mRun = false; // Superficie donde dibuja este hilo private SurfaceHolder mSurfaceHolder; // Handler para comunicar mensajes del Hilo a la Vista del Juego private Handler mHandler; // Contexto de la aplicación Android private Context mContexto; // Vista del Juego (Superficie) public JuegoView juegoView; // Tamaño del canvas de la superficie protected int canvasWidth = 1; protected int canvasHeight = 1; // Tiempo de ejecución desde la última actualización de la // superficie protected long tiempoEjec = 0; // Imagen del fondo de pantalla protected Bitmap mBackgroundImage; // Puntuación de la partida private long puntuacion = 50; // Imagen de la pelota private Bitmap pelota; // Posición actual de la pelota private float pelotaX = -1; private float pelotaY = -1; // Dirección de la pelota private float pelotaVelocidadX = 0; private float pelotaVelocidadY = 0; // Constructor del Hilo public JuegoThread(JuegoView eljuegoView) { juegoView = eljuegoView; // Obtenemos la superficie del juego, su Handler y el Contexto mSurfaceHolder = eljuegoView.getHolder(); mHandler = eljuegoView.getmHandler(); mContexto = eljuegoView.getContext(); // Cargamos las imágenes de fondo del juego de la pelota mBackgroundImage = BitmapFactory.decodeResource (eljuegoView.getContext().getResources(), R.drawable.background); pelota = BitmapFactory.decodeResource
305
Aula Mentor
(eljuegoView.getContext().getResources(), R.drawable.pelota);
} // Constructor para cuando se desea jugar otra vez public JuegoThread(JuegoView gameView, JuegoThread oldThread) { // Recuperamos las variables anteriores juegoView = gameView; mSurfaceHolder = gameView.getHolder(); mHandler = gameView.getmHandler(); mContexto = gameView.getContext(); // Transferimos los valores del juego anterior mEstado = oldThread.mEstado; mRun = oldThread.mRun; canvasWidth = oldThread.canvasWidth; canvasHeight = oldThread.canvasHeight; tiempoEjec = oldThread.tiempoEjec; mBackgroundImage = oldThread.mBackgroundImage; puntuacion = oldThread.puntuacion;
306
}
pelota = oldThread.pelota; pelotaX = oldThread.pelotaX; pelotaY = oldThread.pelotaY; pelotaVelocidadX = oldThread.pelotaVelocidadX; pelotaVelocidadY = oldThread.pelotaVelocidadY;
// Liberamos todas las variables public void liberarTodo() { this.mContexto = null; this.juegoView = null; this.mHandler = null; this.mSurfaceHolder = null; } // Método que inicia el juego public void empezarJuego() { // Sincronizamos estos cambios sobre la superficie synchronized(mSurfaceHolder) { // La pelota no se mueve (no tiene dirección) pelotaVelocidadX = 0; pelotaVelocidadY = 0; // La centramos en la pantalla pelotaX = canvasWidth / 2; pelotaY = canvasHeight / 2; // Tiempo de ejecución tiempoEjec = System.currentTimeMillis() + 100; // Pasamos al estado de Ejecución setEstado(ESTADO_EJECUCION); // La puntuación es 0 setPuntuacion(50); } } // end empezarJuego // Métodos que guardan y recuperan el estado del Hilo
U3 Sensores y dispositivos de Android
public Bundle saveState(Bundle bundle) { // Sincronizamos la superficie para ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { // A continuación guardamos la puntuación, estado // y posición/velocidad de la pelota para poder recuperar // luego en el restore todos los datos y poder reiniciar // el juego if (bundle != null) { bundle.putLong(“puntuacion”, puntuacion); bundle.putInt(“modoJuego”, mEstado); bundle.putFloat(“mPelotaX”, pelotaX); bundle.putFloat(“mPelotaY”, pelotaY); bundle.putFloat(“mPelotaDX”, pelotaVelocidadX); bundle.putFloat(“mPelotaDY”, pelotaVelocidadY);
} } return bundle; } // end saveState
public synchronized void restoreState(Bundle savedState) { // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { // Pausamos el juego hasta que sepamos en qué estado se // quedó el juego setEstado(ESTADO_PAUSA); // Leemos las variables guardadas setPuntuacion(savedState.getLong(“puntuacion”)); pelotaX = savedState.getFloat(“mPelotaX”); pelotaY = savedState.getFloat(“mPelotaY”); pelotaVelocidadX = savedState.getFloat(“mPelotaDX”); pelotaVelocidadY = savedState.getFloat(“mPelotaDY”); Integer modoJuego = savedState.getInt(“modoJuego”); // Si el modo de juego era PERDER o GANAR cambiamos al // modo final. Si no, lo dejamos en PAUSA para que el // usuario haga clic en la pantalla y pueda continuar. if (modoJuego == JuegoThread.ESTADO_PERDER | modoJuego == JuegoThread.ESTADO_GANAR) { setEstado(modoJuego); } } } // end restoreState // Método principal del hilo @Override public void run() { Canvas canvas; // Mientras el hilo se ejecuta... while (mRun) { canvas = null; try { // Obtenemos el canvas de la superficie canvas = mSurfaceHolder.lockCanvas(null); // Sincronizamos la superficie para ningún proceso
307
Aula Mentor
}
308
// pueda acceder a ella synchronized (mSurfaceHolder) { // Si estamos jugando movemos la pelota if (mEstado == ESTADO_EJECUCION) { muevePelota(); } // Dibujamos la superficie doDraw(canvas); }
}
} finally { // Acabamos y desbloqueamos la superficie if (canvas != null) { if(mSurfaceHolder != null) mSurfaceHolder.unlockCanvasAndPost(canvas); } }
// Se usa para establecer el nuevo tamaño de la superficie public void setSurfaceSize(int width, int height) { // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { // Guardamos el nuevo tamaño canvasWidth = width; canvasHeight = height; // Reescalamos el fondo de la superficie mBackgroundImage = Bitmap.createScaledBitmap(mBackgroundImage, width, height, true); } } // Método que dibuja la superficie de juego protected void doDraw(Canvas canvas) { // Si no existe canvas no hacemos nada if(canvas == null) return; // Si tenemos la imagen de fondo, la añadimos al fondo de la // superficie if(mBackgroundImage != null) canvas.drawBitmap(mBackgroundImage, 0, 0, null);
}
// Colocamos la pelota la primera vez que se dibuja la // superficie if (pelotaX 0) ) { // Cambia la dirección de la pelota en el eje X pelotaVelocidadX = -pelotaVelocidadX; actualizaPuntuacion(-1); // Reproducimos sonido rebote sonidoRebote(); } else // Si la pelota se encuentra en la parte de arriba de la // pantalla if(pelotaY = canvasWidth / 2 - 64 && pelotaX = canvasHeight - pelota.getHeight()) setEstado(JuegoThread.ESTADO_PERDER); // Guardamos el último tiempo de ejecución tiempoEjec = tAhora; } // end doDraw
// Si el usuario toca la pantalla public boolean onTouch(MotionEvent e) { // Sólo queremos detectar un toque en la pantalla. Por lo tanto, // si no es un toque, no hacemos nada.
309
Aula Mentor
if(e.getAction() != MotionEvent.ACTION_DOWN) return false; // Si estamos en un estado de final creamos un nuevo juego if(mEstado == ESTADO_PREPARADO || mEstado == ESTADO_PERDER || mEstado == ESTADO_GANAR) { empezarJuego(); return true; } else // Si estamos en el estado PAUSA seguimos con el juego if(mEstado == ESTADO_PAUSA) { continuarJuego(); return true; } // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { this.actionOnTouch(e); } return false; } // end onTouch
310
private void actionOnTouch(MotionEvent e) { } // Evento que se lanza cuando hay un cambio en una medida del // sensor. Aunque el tipo de sensor TYPE_ORIENTATION es obsoleto //(deprecated), lo hemos utilizado por sencillez en el proyecto. // Si no queremos usar un sensor obsoleto debemos utilizar una // combinación de los sensores TYPE_ACCELEROMETER y // TYPE_MAGNETIC_FIELD. @SuppressWarnings(“deprecation”) public void onSensorChanged(SensorEvent event) { // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { // La dirección de la pelota la da la posición del sensor // orientación if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { // Inclinación alrededor del eje x (-90 a 90) pelotaVelocidadX -= 1.5f*event.values[2]; // Rotación sobre el eje Y (-180 a 180) pelotaVelocidadY -= 1.5f*event.values[1]; } } } // end onSensorChanged // Cambios de estado del juego public void pausarJuego() { synchronized (mSurfaceHolder) { if (mEstado == ESTADO_EJECUCION) setEstado(ESTADO_PAUSA); } } public void continuarJuego() { // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { // Actualizamos el tiempo de ejecución para que no // se produzca un salto tiempoEjec = System.currentTimeMillis();
U3 Sensores y dispositivos de Android
}
} setEstado(ESTADO_EJECUCION);
// Método que establece el estado del juego tanto desde este Hilo // como desde la Actividad juego public void setEstado(int estado) { // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { // Guardamos el nuevo estado mEstado = estado; // Si el estado cambia a ejecución creamos un nuevo // mensaje y lo enviamos al JuegoView para que actualice // las Vistas en función del estado del juego if (mEstado == ESTADO_EJECUCION) { Message msg = mHandler.obtainMessage(); Bundle b = new Bundle(); // Ocultamos la etiqueta del estado del juego b.putString(“texto”, “”); b.putInt(“visible”, View.INVISIBLE); // Enviamos el mensaje al JuegoView msg.setData(b); mHandler.sendMessage(msg); } // Si no estamos jugando debemos mostrar el mensaje // correspondiente else { Message msg = mHandler.obtainMessage(); Bundle b = new Bundle(); Resources res = mContexto.getResources(); CharSequence str = “”; if (mEstado == ESTADO_PREPARADO) str = res.getText(R.string.modo_preparado); else if (mEstado == ESTADO_PAUSA) str = res.getText(R.string.modo_pausa)+ “\n” + res.getText(R.string.mensaje_parado); else if (mEstado == ESTADO_PERDER) { str = res.getText(R.string.modo_perder); // Reproducimos sonido perder b.putInt(“sonido”, R.raw.gameover); } else if (mEstado == ESTADO_GANAR) { str = res.getText(R.string.modo_ganar); // Sonido de ganar b.putInt(“sonido”, R.raw.win); } b.putString(“texto”, str.toString()); // Mostramos la etiqueta del estado del juego b.putInt(“viz”, View.VISIBLE); // Enviamos el mensaje al JuegoView
311
Aula Mentor
msg.setData(b); mHandler.sendMessage(msg);
} } } // end detestado // Método que envía un mensaje al hilo principal // para que reproduzca sonido de rebote public void sonidoRebote() { Message msg = mHandler.obtainMessage(); Bundle b = new Bundle(); b.putInt(“sonido”, R.raw.rebote); msg.setData(b); // Enviamos el mensaje del sonido mHandler.sendMessage(msg); } // end sonidoRebote
312
// Método que establece la superficie public void setSurfaceHolder(SurfaceHolder sh) { mSurfaceHolder = sh; } // Indica si estamos ejecutando el hilo public boolean isRunning() { return mRun; } // Cambia el hilo a modo ejecución public void setRunning(boolean running) { mRun = running; } // Obtiene el estado del juego public int getEstado() { return mEstado; } // Método que envía mediante un Handler la puntuación al JuegoView public void setPuntuacion(long puntuacion) { this.puntuacion = puntuacion; // Sincronizamos la superficie para que ningún proceso pueda // acceder a ella synchronized (mSurfaceHolder) { Message msg = mHandler.obtainMessage(); Bundle b = new Bundle(); // Creamos los datos del mensaje b.putBoolean(“puntuacion”, true); // Es necesario hacer un typecasting del campo puntuación b.putString(“texto”, Long.toString(Math.round(this.puntuacion)).toString()); // Enviamos el mensaje al JuegoView msg.setData(b); mHandler.sendMessage(msg); } } // end setPuntuacion // Actualiza la puntuación actual sumando la actual a los nuevos // puntos obtenidos public void actualizaPuntuacion(long puntuacion) { this.setPuntuacion(this.puntuacion + puntuacion); } } // end clase
U3 Sensores y dispositivos de Android
Empecemos por comentar que, en el código anterior, hemos definido las constantes que empiezan por ESTADO_ para controlar el estado del juego y actuar en consecuencia. Puedes ver también que esta clase es un hilo típico de Java con su correspondiente método run(). En éste puedes ver que bloqueamos el canvas de la superficie para dibujarlo con la orden mSurfaceHolder.lockCanvas() y, cuando es necesario, sincronizamos los métodos internos que mueven y dibujan la pelota. Si el usuario está jugando, entonces llamamos al método muevePelota(), que desplaza la pelota en la zona de juego. Habitualmente los desarrolladores de juegos a este método lo denominan physics() ya que simula la física (movimiento) del juego. Además, en este bloque run() dibujamos la superficie con la orden doDraw(). Una vez hemos ejecutado este bloque de código, liberamos el canvas de la superficie desbloqueándolo con la orden unlockCanvasAndPost(canvas). Si te fijas en el método doDraw() verás que únicamente pinta la superficie de juego con la pelota en la posición que corresponda. Sin embargo, muevePelota() calcula la posición siguiente de la pelota en función del tiempo pasado (así simulamos el movimiento) y la dirección de la pelota. También cambia el estado del juego si la pelota acaba en ciertas posiciones de la pantalla. El método onTouch() sirve para interactuar con la pulsación del usuario en la pantalla. Este método tiene como parámetro la clase MotionEvent que permite conocer la acción del usuario mediante la orden getAction(). Así, sólo utilizamos la acción MotionEvent.ACTION_ DOWN cuando toca la pantalla para continuar o empezar el juego. Para que el jugador controle el movimiento de la pelota, hemos utilizando el sensor de orientación y desarrollado el método onSensorChanged() que nos proporciona en su parámetro SensorEvent la inclinación alrededor del eje x (-90 a 90) y la rotación sobre el eje Y (-180 a 180), que empleamos para obtener la nueva velocidad y sentido de la pelota. Es importante notar que esta clase tiene dos constructores. El tradicional JuegoThread(JuegoView eljuegoView) que obtiene la superficie del juego, su Handler y el Contexto de la aplicación. Y un segundo método JuegoThread(JuegoView gameView, JuegoThread oldThread) que utilizamos en el método surfaceCreated() de JuegoView para comprobar con la constante Thread.State.NEW si el hilo se ha ejecutado alguna vez. En la documentación de Android se indica que es obsoleto parar y reiniciar hilos. Ten en cuenta que un usuario puede decidir abrir otra aplicación mientras está jugando y esto pausa el juego destruyendo la superficie, por lo que volver a recrearla. Por lo tanto, una forma de recrear un hilo nuevo es definir un segundo método JuegoThread() donde copiamos todos los atributos del hilo existente y, así, el nuevo hilo comienza en el estado en el que se quedó anteriormente garantizando la experiencia del usuario al continuar el juego en el estado donde se quedó. Te recomendamos que le eches un vistazo a los métodos setPuntuacion() y setEstado() que establecen en la superficie JuegoView la puntuación y el estado del juego, respectivamente, desde este hilo utilizando un objeto de la clase Bundle. En esta clase vamos a introducir la información que deseamos pasar al hilo principal, a añadirla al objeto Message con setData(bundle) y a enviar este mensaje con la orden sendMessage(mensaje) de Handler. Como no podemos bloquear el hilo del juego, hemos aprovechado estos mensajes para notificar al hilo principal que debe reproducir sonidos con la clase asíncrona SoundPool en función del estado del juego o cuando la pelota rebota en los muelles. Finalmente, hemos definido algunos métodos auxiliares, como empezarJuego() que inicia el juego, saveState() y restoreState() que guardan y recuperan el estado de ejecución del Hilo, respectivamente, y varios métodos más del tipo set…() y get…() necesarios en el hilo.
313
Aula Mentor
Cuando se utilizan hilos es imprescindible usar la orden synchronized en el objeto dinámico correspondiente para evitar accesos simultáneamente a éste que lo dejan en un estado incompleto, de forma que el usuario tendrá la sensación de que la aplicación se mueve a trompicones. En este caso, lo hacemos siempre que vamos a modificar la superficie.
Aunque es posible utilizar el SensorSimulator para depurar la aplicación, es lento y poco práctico; por lo tanto, recomendamos probar esta aplicación en un dispositivo real de Android.
Para poder usar un dispositivo real desde Eclipse ADT es necesario conectar este dispositivo mediante un cable al ordenador y modificar sus Ajustes en las opciones siguientes: En “Opciones del desarrollador”, marcar “Depuración de USB”. En “Seguridad”, señalar “Fuentes desconocidas”.
314
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 7 (Juego) de la Unidad 3. Estudia el código fuente y ejecútalo en un dispositivo Android para ver el resultado del programa anterior, en el que hemos utilizado un elemento SurfaceView y un Sensor para desarrollar un juego.
Si ejecutas en Eclipse ADT este Ejemplo 7 en el AVD, verás que se muestra la siguiente aplicación:
Puedes probar a mover el dispositivo para constatar cómo cambia el sentido de la pelota y oír los efectos de sonido. ¡Esperamos que el juego te resulte entretenido!
U3 Sensores y dispositivos de Android
6. Resumen Hay que saber al final de esta unidad: - La mayoría de los dispositivos de Android incorporan sensores que miden su movimiento, orientación y otras varias magnitudes físicas. - Los tipos de sensores de Android son: • Sensores de Movimiento • Sensores del Medioambiente • Sensores de Posición - Android permite acceder a los sensores del dispositivo a través de las clases: • Sensor: clase que representa a un sensor con todas sus propiedades. • SensorEvent: clase que almacena los datos medidos por el sensor. • SensorManager: gestor que permite acceder a los sensores del dispositivo. • SensorEventListener: interfaz utilizada para recibir las notificaciones del SensorManager. - Se pueden dividir los sensores en dos categorías más: real y virtual. Real señala que el sensor indica una medida de una magnitud física. Virtual significa que son medidas obtenidas a partir de otros sensores o son medidas relativas. - Para desarrollar aplicaciones que utilizan sensores, es necesario disponer de un dispositivo físico ya que el emulador de Android no permite depurarlas correctamente. - Por defecto, en el emulador de dispositivos virtuales (AVD) del SDK de Android no se pueden utilizar sensores para depurar aplicaciones. - SensorSimulator es un simulador de sensores que permite depurar una aplicación Android que los use. - El SDK de Android dispone de la clase WifiManager que permite hacer uso del módulo WIFI, siempre y cuando el dispositivo disponga de él. - El SDK de Android también ofrece soporte para la tecnología Bluetooth que permite a un dispositivo intercambiar datos de forma inalámbrica con otros dispositivos cercanos. - Las clases más importantes del SDK Bluetooth de Android son: • BluetoothAdapter: permite realizar todo tipo de operaciones sobre este módulo bluetooth. • BluetoothDevice: representa un dispositivo bluetooth remoto con sus atributos y propiedades correspondientes. • BluetoothSocket: permite la vinculación entre dispositivos y la trasmisión de datos. - Para utilizar este módulo Bluetooth es necesario definir un receptor de mensajes del tipo BroadcastReceiver para recibir los eventos del BluetoothAdapter cada vez que encuentre un dispositivo. - El AVD de Android no incluye los módulos WIFI ni de Bluetooth; por lo tanto, es necesario depurar aplicaciones que los usen en un dispositivo real de Android. - Prácticamente todos los dispositivos Android integran una o varias cámaras de fotos. El SDK de Android incluye las clases Java necesarias para gestionar cámaras de fotos.
315
Aula Mentor
- Existen dos formas de gestionar la cámara de fotos: • Utilizando un Intent que gestiona la cámara de fotos y devuelve la imagen a la aplicación que lo ha iniciado. • Integrando directamente la cámara en la aplicación utilizando las librerías (API) de Android. - Las clases más importantes de la API de la cámara de Android son: • Camera: clase básica de la API para gestionar la cámara, tomar fotos y grabar vídeos. • CameraInfo: clase que contiene la información de la cámara. • SurfaceView: clase que se usa para mostrar al usuario la previsualización de la cámara de fotos. • MediaRecorder: clase utilizada para grabar vídeos. - Es muy importante gestionar los métodos del ciclo de vida de la Actividad que contiene la cámara de fotos, ya que ésta consume muchos recursos del sistema, como CPU y batería, por lo que sólo se deben utilizar cuando sea necesario. - Muchos dispositivos Android integran el módulo GPS de localización geográfica mundial. El SDK de Android incluye las clases Java necesarias para gestionarlo. - La clase LocationManager de Android permite gestionar el módulo GPS. - Existen varios métodos de obtener la localización de un dispositivo móvil: mediante el módulo GPS, utilizando las antenas de telefonía móvil o mediante los puntos de acceso WI-FI cercanos. 316
U4 Bibliotecas, APIs y Servicios de Android
Unidad 4. Bibliotecas, APIs y Servicios de Android
1. Introducción En esta Unidad vamos a explicar cómo se utilizan bibliotecas de una aplicación para ampliar su funcionalidad. Además, usaremos la API de Telefonía de Android para gestionar llamadas de teléfono y mensajes cortos (SMS) desde un dispositivo. Después, utilizaremos el Calendario de Android para administrar las citas y eventos de éste. También veremos cómo aprovechar la funcionalidad del gestor de descargas (Download manager) de Android. Finalmente, estudiaremos cómo enviar un correo electrónico e implementaremos Servicios avanzados en Android.
2. Uso de Bibliotecas en Android Una biblioteca (del inglés library) es un conjunto de funciones y utilidades que tienen una interfaz bien definida para el comportamiento que se invoca. A diferencia de un programa ejecutable, una biblioteca no se puede ejecutar de forma autónoma, pues su fin es ser utilizada por otros programas, a veces, de forma simultánea. Según el tipo de sistema operativo, estas bibliotecas pueden tener distinta extensión: .SO en Unix, .DLL en Windows, etcétera. Android, como sistema operativo completo, también dispone de este tipo de biblioteca que ayuda a ampliar de forma sencilla las aplicaciones utilizando bibliotecas externas a éstas, si bien, la biblioteca de Android posee una serie de peculiaridades especiales que la diferencian de las típicas bibliotecas. La estudiaremos a continuación. El SDK de Android dispone de proyectos de tipo biblioteca (del inglés, library projects). Un proyecto de tipo biblioteca es un conjunto de código Java y recursos que parece un proyecto normal Android, pero que nunca acaba en un archivo de tipo .apk en sí mismo. Sin embargo, cuando incluimos este código y recursos en un proyecto Android, pasa a formar parte de éste y se compilan en un archivo único de tipo .apk. Es decir, las bibliotecas en Android son funciones y recursos que podemos compilar conjuntamente en un proyecto Android. Veamos las características de estas bibliotecas: - Pueden tener su propio nombre de paquete Java. - Se compila siempre como en un proyecto de Android que lo utiliza para ampliar su funcionalidad. - Puede utilizar otros archivos JAR. - No se puede convertir en un archivo JAR en sí mismo, si bien Google lo va a permitir en versiones futuras de Android.
317
Aula Mentor
- A excepción de los archivos de Java, el resto de los archivos que pertenecen a una biblioteca, por ejemplo, recursos de tipo imagen, se mantiene en el proyecto de tipo biblioteca. - Si utilizamos una biblioteca en un proyecto Android, es necesario incluirla en éste como dependencia del proyecto. - A partir del SDK 15 de Android los IDs de recursos de la biblioteca no son finales (final). - Tanto el proyecto de tipo biblioteca como el proyecto principal pueden acceder a los recursos internos de la biblioteca a través de sus respectivas referencias a R.java. - Es posible definir IDs de recursos repetidos en el proyecto principal y en la biblioteca. Los IDs del proyecto principal tendrán prioridad sobre los de la biblioteca. Por esto, es recomendable distinguir los IDs de recursos entre los dos proyectos utilizando diferentes prefijos de recursos, por ejemplo, anteponiendo el prefijo “lib_” para los recursos de la biblioteca. - Un proyecto principal puede hacer referencia a varias bibliotecas al mismo tiempo. Es posible establecer la prioridad de las bibliotecas para marcar qué recursos son más importantes. - Si queremos utilizar una Actividad (Activity) o un Servicio (Service) desarrollados en una biblioteca, deben incluirse en el archivo manifest.xml del proyecto principal. Además, es necesario indicar el nombre del paquete completo de la biblioteca al incluir la Actividad o el Servicio. - Un proyecto de tipo biblioteca no puede hacer referencia a otra biblioteca, si bien en futuras versiones del SDK de Android se podrá hacer.
318
Es importante saber integrar bibliotecas en proyectos Android ya que existen muchas bibliotecas de código libre disponibles en Internet que pueden simplificar la programación de un proyecto.
Para crear un proyecto de tipo biblioteca, debemos crear un proyecto normal de Android y luego marcar la opción biblioteca (library). Veamos de forma práctica cómo definir y utilizar una biblioteca en Android.
2.1 Ejemplo de Biblioteca de Android A continuación, vamos a estudiar cómo crear e integrar una biblioteca en una aplicación Android. En el Ejemplo 1 de esta Unidad hemos desarrollado una aplicación sencilla que utiliza una biblioteca. Lo primero que vamos a hacer es crear la biblioteca. Para ello, pulsamos en el menú principal de Eclipse ADT en la opción siguiente:
U4 Bibliotecas, APIs y Servicios de Android
Aparecerá esta ventana que debemos rellenar de la siguiente manera:
319
A continuación, pulsamos el botón “Next” y aparecerá esta ventana:
Aula Mentor
En la ventana anterior es muy importante marcar la opción “Mark this Project as a library”. Después, hacemos clic de nuevo en el botón “Next” donde podríamos cambiar el icono de la biblioteca:
320 Volvemos a pulsar el botón “Next” para pasar a la siguiente pantalla:
U4 Bibliotecas, APIs y Servicios de Android
En esta ventana seleccionamos “Blank Activity” y pulsamos el botón “Next”:
321
En la ventana anterior rellenamos los campos tal y como aparecen en la captura y para acabar la creación de la biblioteca hacemos clic en el botón “Finish”. Si abres el explorador de archivos en la carpeta bin del proyecto anterior (unidad4. eje1.biblioteca) verás que Eclipse ADT ya ha compilado el proyecto en el formato .jar de biblioteca Java:
Esta biblioteca consta de una Actividad secundaria que invocaremos desde la aplicación principal y de una imagen (robot.png) que usaremos en ésta:
Aula Mentor
Si abres el archivo activity_actividad_secundaria.xml, verás que el diseño es muy sencillo y contiene una etiqueta y dos botones:
322
U4 Bibliotecas, APIs y Servicios de Android
La lógica de la Actividad se define en el archivo ActividadSecundaria.java, que tiene este aspecto: public class ActividadSecundaria extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_actividad_secundaria); // Recuperamos el parámetro y mostramos un mensaje Bundle extras = getIntent().getExtras(); String parametro = extras.getString(“parametro”); Toast.makeText(this, “Parámetro recibido: “+parametro, Toast.LENGTH_LONG).show(); } // Evento que se lanza cuando el usuario hace clic en un botón public void botonClick(View v) { Intent returnIntent = new Intent(); // Según el botón que sea devolvemos un resultado distinto if (v.getId()==R.id.botonOK) setResult(RESULT_OK,returnIntent); else setResult(RESULT_CANCELED,returnIntent); finish(); } } // end clase
Se trata de una sencilla Actividad que devuelve RESULT_OK o RESULT_CANCELED según el botón donde pulse el usuario. Ahora vamos a crear una aplicación Android como viene siendo habitual. No se incluye el proceso ya que el alumno o alumna debe saber crear el proyecto. Después, añadimos la biblioteca creada anteriormente haciendo clic con el botón derecho del ratón y seleccionando la opción del menú desplegable “Properties”. Accediendo a la opción “Android”, pulsamos el botón “Add” para añadir la biblioteca creada anteriormente:
323
Aula Mentor
Una vez importada la librería biblioteca, verás que aparecen dos archivos de recursos en la carpeta gen: uno de la aplicación que contiene la biblioteca (es.mentor.unidad4.eje1. biblioteca_aplicacion) y el otro de la biblioteca (es.mentor.unidad4.eje1.biblioteca) propiamente dicho:
Estudiemos ahora cómo utilizar los recursos (imagen robot.png) y clase (ActividadSecundaria) que define esta biblioteca.
324
En el archivo layout activity_main.xml definimos la interfaz de usuario que consta de un botón que lanza la Actividad de la biblioteca y de una imagen que obtenemos de ésta:
Lo primero que observamos en el fichero anterior es que la imagen de la biblioteca está disponible como recurso del proyecto y basta escribir @drawable/robot para acceder a ella. En el archivo MainActivity.java desarrollamos la lógica de la aplicación: public class MainActivity extends Activity {
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
// Método onClick del botón que lanza la Actividad secundaria public void lanzaActSec(View v) { // Añadimos un parámetro a la invocación Intent intent = new Intent(this,ActividadSecundaria.class); intent.putExtra(“parametro”, “Texto del parámetro”); // Lanzamos el Intent esperando su respuesta startActivityForResult(intent, 1); } // Evento que se lanza cuando la Actividad secundaria acaba @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Si respondemos a la actividad ejecutada if (requestCode == 1) { // Mostramos un mensaje en función del resultado de la // Actividad if (resultCode == RESULT_OK) { Toast.makeText(this, “Actividad secundaria OK”, Toast.LENGTH_LONG).show(); } else if (resultCode == RESULT_CANCELED) { Toast.makeText(this, “Actividad secundaria CANCELADA”, Toast.LENGTH_LONG).show(); } } } // end onActivityResult } // end clase
En las sentencias anteriores hemos utiliza la clase ActividadSecundaria definida en la biblioteca como una clase del proyecto invocándola cuando el usuario hace clic en el botón correspondiente. Para que la ActividadSecundaria esté disponible en la aplicación, debemos incluir en el archivo manifest.xml del proyecto lo siguiente:
Para que la ActividadSecundaria esté disponible en el proyecto es muy importante incluir el nombre completo del paquete de la librería donde se define.
326
Desde Eclipse ADT puedes abrir el proyecto Ejemplo 1 (Biblioteca) de la Unidad 4. Estudia el código fuente y ejecútalo en el AVD para ver el resultado del programa anterior, en el que hemos utilizado una Biblioteca de Android.
Si ejecutas en Eclipse ADT este Ejemplo 1 en el AVD, verás que se muestra la siguiente aplicación:
U4 Bibliotecas, APIs y Servicios de Android
Si pulsas en el botón “Lanza actividad secundaria” aparecerá la Actividad de la segunda captura de la derecha. Como puedes observar, utilizar bibliotecas es muy sencillo, bastante práctico desde el punto de vista del programador y permite ampliar de forma rápida la funcionalidad de una aplicación Android.
3. APIs del teléfono: llamadas y SMS Antes de estar disponible Android en otros tipos de dispositivos, este sistema operativo inició su carrera en teléfonos móviles. Por lo tanto, dispone de su propia API de telefonía para gestionar llamadas de teléfono y mensajes cortos (SMS). En este apartado, mediante un ejemplo práctico, se hace una explicación detallada de las funciones propias de esta API. Conocerlas te permitirá realizar cualquier aplicación que haga uso de sus funcionalidades disponibles en el paquete android.telephony. Esta API proporciona acceso a la información sobre los servicios de telefonía disponibles en el dispositivo y obtiene información sobre su estado. Además, accede a algunos tipos de información del usuario: IMEI del dispositivo, número de teléfono, operador, potencia de la red, etcétera. Asimismo, las aplicaciones pueden registrar un listener para recibir notificaciones de cambios del estado del teléfono. Si una aplicación desea tener acceso a esta API, es necesario definir los permisos adecuados en el archivo manifest.xml del proyecto. El SDK de Android dispone de clases que permiten hacer uso del teléfono, siempre y cuando el dispositivo disponga de este módulo. Las clases más importantes de esta API de Android se denominan TelephonyManager y SMSManager. Veamos sus características más importantes.
3.1 TelephonyManager TelephonyManager, gestor que permite determinar la siguiente información del servicio de telefonía (se incluye un listado básico): - IMEI del dispositivo - Nº de Teléfono - Nombre del Operador en varios formatos - Tipo y Potencia de la red conectada - Datos de la tarjeta SIM - Nº del buzón de mensajes de voz - Datos de la Celda de Telefonía (antena a la que está conectado)
Y del estado del teléfono: - LIBRE (IDLE) - LLAMADA ENTRANTE (RINGING) - DESCOLGADO (OFFHOOK) Además, es posible recibir notificaciones cuando este estado cambia.
327
Aula Mentor
3.2 SMSManager SMSManager, este Gestor permite enviar mensajes cortos (SMS) y leer su formato interno PDU
que significa “Protocol Description Unit”, que es el formato estándar de los mensajes cortos SMS.
3.3 Ejemplo de utilización de la API de telefonía Estudiemos ahora, de forma práctica, cómo utilizar la API de telefonía. Es recomendable abrir el Ejemplo 2 de esta Unidad para seguir la explicación que se expone a continuación. La aplicación que desarrollamos muestra dos pestañas: “Teléfono” y “Mensajes cortos (SMS)”. La primera pestaña permite realizar llamadas de teléfono y recibir cambios en el estado del teléfono. La segunda, enviar y recibir mensajes SMS. En código del layout activity_main.xml se incluye el diseño de la Actividad principal:
328
En el diseño anterior hemos incluido el elemento FragmentTabHost, que sirve de contenedor de las pestañas TabWidget. Es decir, para establecer la interfaz del usuario hemos utilizado Fragmentos de tipo pestaña. Si, a continuación, abrimos la clase principal de la aplicación MainActivity vemos el siguiente contenido: public class MainActivity extends FragmentActivity implements TabHost.OnTabChangeListener private FragmentTabHost mTabHost;
{
U4 Bibliotecas, APIs y Servicios de Android
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Buscamos el contenedor de Tabs de la interfaz de usuario en // el archivo activity_main.xml mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost); // Configuramos el frame que contendrá el contenido de la // pestaña mTabHost.setup(this, getSupportFragmentManager(), R.id.tabFrameLayout); // Añadimos la pestaña Teléfono utilizando el fragmento // TelefonoFragmentTab mTabHost.addTab( mTabHost.newTabSpec(“tab1”).setIndicator(“Teléfono”, getResources().getDrawable(R.drawable.telefono)), TelefonoFragmentTab.class, null); // Añadimos la pestaña SMS utilizando el fragmento // SMSFragmentTab mTabHost.addTab( mTabHost.newTabSpec(“tab2”).setIndicator(“Mensajes cortos (SMS)”, getResources().getDrawable(R.drawable.sms)), SMSFragmentTab.class, null); // Establecemos el listener del cambio de pestaña mTabHost.setOnTabChangedListener(this); } // end onCreate()
// Evento que se lanza cada vez que se selecciona una pestaña nueva @Override public void onTabChanged(String tabId) { // Mostramos un mensaje al usuario Toast.makeText(this, “Has cambiado a la pestaña “ + tabId, Toast.LENGTH_SHORT).show(); } } // end clase
En el código anterior buscamos el contenedor FragmentTabHost de pestañas en la interfaz de usuario definido en el archivo activity_main.xml. Mediante su método setup() establecemos la Vista donde debe aparecer el contenido de las pestañas. Después, mediante el método addTab(TabSpec arg0, Class arg1, Bundle arg2) añadimos las dos pestañas indicando en el parámetro de tipo TabSpec el nombre, texto e imagen de la pestaña y en el arg1 la clase que define el fragmento. El parámetro arg2 se usa en el caso de que sea necesario pasar al fragmento información adicional. También hemos implementado en esta Actividad de tipo fragmento la interfaz TabHost. OnTabChangeListener que lanza un evento cada vez que el usuario selecciona una nueva pestaña. En este caso, mostramos un mensaje sencillo de tipo Toast al usuario. A continuación, se muestra cómo se implementa el fragmento de la primera pestaña que gestiona llamadas de teléfono. Si abres en Eclipse ADT el archivo telefono_fragment_layout.xml verás que contiene un diseño muy sencillo que consiste en una caja de texto y un botón para realizar llamadas y una etiqueta para mostrar los eventos de la API de telefonía:
329
Aula Mentor
330
U4 Bibliotecas, APIs y Servicios de Android
En la clase TelefonoFragmentTab se define la lógica del fragmento: public class TelefonoFragmentTab extends Fragment { // Gestor de la API de telefonía private TelephonyManager tm; // Listener para detectar cambios en el teléfono private MiPhoneStateListener miListener = null; // Vista que muestra el log de eventos al usuario private TextView log; // Almacena el nº de tfno que introduce el usuario private EditText et; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Definimos un Intent del tipo nueva llamada saliente String outgoing = “android.intent.action.NEW_OUTGOING_CALL” ; IntentFilter intentFilter = new IntentFilter(outgoing); // Registramos un receptor de mensajes para el Intent anterior getActivity().registerReceiver(OutGoingCallReceiver, intentFilter); // Indicamos que el fragmento no debe destruirse setRetainInstance(true); } // Como es un fragmento debemos buscar las Vistas en este evento y // no en onCreate() @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Obtenemos las vistas del fragmento View v = inflater.inflate(R.layout.telefono_fragment_layout, container, false); log = (TextView) v.findViewById(R.id.Log); TextView tv = (TextView) v.findViewById(R.id.datosTfno); et = (EditText) v.findViewById(R.id.nTelefono); Button boton = (Button) v.findViewById(R.id.llamarBtn); // Definimos el evento onClick de llamada tfno boton.setOnClickListener(new OnClickListener() { public void onClick(final View v) { // Limpiamos el log log.setText(“Log:”); // También podríamos definir ACTION_DIAL o // ACTION_VIEW para ver el marcador de teléfono pero // sin iniciar la llamada
331
Aula Mentor
});
}
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse(“tel:” + et.getText().toString().trim())); startActivity(intent);
tv.append(“\nTipo teléfono: “); // Obtenemos el gestor de la API del teléfono tm = (TelephonyManager)getActivity().getSystemService( Context.TELEPHONY_SERVICE); // Definimos el listener de llamadas miListener = new MiPhoneStateListener(); // Obtenemos el tipo de teléfono del dispositivo int phoneType = tm.getPhoneType(); switch(phoneType){ case TelephonyManager.PHONE_TYPE_NONE: tv.append(“No hay teléfono”); break; case TelephonyManager.PHONE_TYPE_GSM: tv.append(“GSM”); break; case TelephonyManager.PHONE_TYPE_CDMA: tv.append(“CDMS”); break;
332
case TelephonyManager.PHONE_TYPE_SIP: tv.append(“SIP”); break;
default: tv.append(“Desconocido”); }
// Mostramos más información sobre el dispositivo: // nº IMEI, Nº teléfono, País de la SIM, Nombre Operador y Nº // del buzón de voz tv.append(“\nNº IMEI: “+ tm.getDeviceId()); tv.append(“\nNº Teléfono: “+ tm.getSubscriberId()); tv.append(“\nPaís SIM: “+ tm.getSimCountryIso()); tv.append(“\nNombre Operador: “+ tm.getNetworkOperatorName()); tv.append(“\nBuzón de voz: “+ tm.getVoiceMailNumber()); return v; } // end onCreateView @Override public void onResume() { super.onResume(); Log.d(“Teléfono”, “onResume”); // Indicamos al gestor de la API del teléfono que escuche // los eventos de éste con el listener que hemos creado // anteriormente tm.listen(miListener, PhoneStateListener.LISTEN_CALL_STATE);
U4 Bibliotecas, APIs y Servicios de Android
}
// Si deseamos dejar de controlar la llamadas cuando la Actividad // pase a segundo plano deberíamos descomentar el siguiente método /*@Override public void onPause() { super.onPause(); Log.d(“Teléfono”, “onPause”); tm.listen(miListener, PhoneStateListener.LISTEN_NONE); }*/ // Al destruir el Fragmento es necesario dejar de escuchar los // eventos del teléfono @Override public void onDestroy() { super.onDestroy(); Log.d(“Teléfono”, “onDestroy”); tm.listen(miListener, PhoneStateListener.LISTEN_NONE); }
// Listener que escucha los eventos de la API del Tfno public class MiPhoneStateListener extends PhoneStateListener { private String logText = “”; // Evento que se lanza cuando cambia el estado de llamada del // teléfono @Override public void onCallStateChanged(int state, String incomingNumber) { super.onCallStateChanged(state, incomingNumber); // Almacenamos la información del nuevo estado del tfno switch(state) { case TelephonyManager.CALL_STATE_IDLE: logText = “EN ESPERA...”; break; case TelephonyManager.CALL_STATE_RINGING: logText = “SONANDO... nº llamante = “+ incomingNumber; break; case TelephonyManager.CALL_STATE_OFFHOOK: logText = “LLAMADA ACTIVA”; if (!incomingNumber.isEmpty()) logText += “ nº llamante = “+ incomingNumber; break; default: logText = “SIN ESTADO [“+state+”] nº llamante = “+ incomingNumber; break; } // Mostramos la información al usuario if (!logText.trim().equals(“”)) log.append(“\nEstado: “+logText); } } // end MiPhoneStateListener
333
Aula Mentor
// Receptor de mensajes para recibir el evento del sistema // operativo cuando se produzca una nueva llamada BroadcastReceiver OutGoingCallReceiver = new BroadcastReceiver() { // Cuando el usuario inicia una nueva llamada se lanza este // evento @Override public void onReceive(Context context, Intent intent) { // Obtenemos el nº de teléfono que nos pasa el mensaje // BroadCast String str = “LLAMADA ACTIVA nº llamante = “+ intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); // Mostramos el dato al usuario log.append(“\nEstado: “ + str); } }; } // end clase
En el código anterior puedes observar que, al tratar con un Fragmento, hemos empleado el método onCreateView() para buscar las Vistas. En este mismo método hemos definido el evento onClick() del botón “Llamar por teléfono” que crea una Intención de tipo Intent.ACTION_CALL e inicia la Actividad correspondiente. También podríamos utilizar los tipos ACTION_DIAL o ACTION_VIEW para ver el marcador de teléfono pero sin iniciar la llamada. 334
Después, hemos obtenido el gestor TelephonyManager de la API del teléfono mediante la orden getSystemService(Context.TELEPHONY_SERVICE) y utilizado sus métodos extraemos los
datos siguientes: - getPhoneType(): tipo de teléfono del dispositivo. - getDeviceId(): IMEI del dispositivo. - getSubscriberId(): nº de teléfono. - getSimCountryIso(): país de la tarjeta SIM. - getNetworkOperatorName(): nombre del operador de telefonía. - getVoiceMailNumber(): nº de buzón de voz.
En este mismo método creamos el listener que se extiende de PhoneStateListener, que recibirá los eventos del teléfono y que se lanza cuando cambia el estado de llamada del teléfono con onCallStateChanged(). Cuando esto ocurre, simplemente añadimos un texto al Log de la interfaz del usuario. En los eventos onResume() y onDestroy() del fragmento indicamos al gestor de la API del teléfono que escuche o deje de escuchar respectivamente los eventos del teléfono con el listener que hemos creado anteriormente. Para ello, utilizamos el método listen(PhoneStateListener listener, int events) del gestor TelephonyManager e indicamos en el segundo parámetro el tipo de evento que queremos escuchar: LISTEN_CALL_STATE (cambios en el estado de llamada). También es posible escuchas otro tipo de eventos, como envío de datos, cambios en la potencia de la señal, información de la celda, etcétera. Sin embargo, para detectar llamadas salientes es necesario definir en el método onCreate() del fragmento un receptor de mensajes para el Intent del tipo android.intent.action. NEW_OUTGOING_CALL mediante la orden registerReceiver(). Hemos incluido el receptor de mensajes llamado OutGoingCallReceiver del tipo BroadcastReceiver para recibir el evento del
U4 Bibliotecas, APIs y Servicios de Android
sistema operativo cuando se produzca una nueva llamada y se lance el evento onReceive(). En este caso, añadimos un nuevo mensaje de Log al usuario. Veamos ahora cómo se implementa el fragmento de la segunda pestaña que gestiona mensajes cortos (SMS). Si abres en Eclipse ADT el archivo sms_fragment_layout.xml, verás que contiene un diseño muy sencillo que consiste en dos cajas de texto (teléfono y mensaje corto) con su respectivo botón para enviar el mensaje y un listado ListView para mostrar los mensajes cortos recibidos:
336
En la clase SMSFragmentTab se define la lógica del fragmento: public class SMSFragmentTab extends Fragment implements LoaderCallbacks { // Almacena el nº de tfno y el texto sms que escribe el usuario private EditText telefono, textoSMS; // Adaptador del listado de SMS recibidos private SimpleCursorAdapter adapter; private static final Uri SMS_INBOX = Uri.parse(“content://sms/inbox”); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Definimos un Intent del tipo nueva llamada saliente String outgoing = “android.provider.Telephony.SMS_RECEIVED” ; IntentFilter intentFilter = new IntentFilter(outgoing); // Registramos un receptor de mensajes para el Intent anterior getActivity().registerReceiver(IncomingSMSReceiver, intentFilter); // Indicamos que el fragmento no debe destruirse setRetainInstance(true); // Definimos los campos de la BD de SMS que vamos a // utilizar y las Vistas donde aparecerá el texto String[] columnas = new String[] { “address”, “body” }; int[] nombres = new int[] { R.id.telefono, R.id.sms }; // Indicamos el loader que va a gestionar las actualizaciones // del listado mediante un Cursor getActivity().getLoaderManager().initLoader(0, null, this);
U4 Bibliotecas, APIs y Servicios de Android
// No definimos un Cursor para acceder a la BD ya que el // Loader se encargará de ello adapter = new SimpleCursorAdapter(getActivity(), R.layout.fila_listado, null, columnas, nombres, 0); }
// Como es un fragmento debemos buscar las Vistas en este evento y // no en onCreate() @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Buscamos las Vistas de la interfaz del usuario View v = inflater.inflate(R.layout.sms_fragment_layout, container, false); telefono = (EditText) v.findViewById(R.id.nTelefono); textoSMS = (EditText) v.findViewById(R.id.mensajeET); ListView listado = (ListView) v.findViewById(R.id.listado); Button boton = (Button) v.findViewById(R.id.enviarBoton); // Definimos el evento onClick de envio SMS boton.setOnClickListener(new OnClickListener() { public void onClick(final View v) { try { // Buscamos el gestor por defecto de SMS SmsManager smsMgr = SmsManager.getDefault(); // Enviamos un mensaje al teléfono indicado smsMgr.sendTextMessage( telefono.getText().toString(), null, textoSMS.getText().toString(), null, null); // Mostramos un mensaje al usuario indicando que // todo ha ido bien Toast.makeText(getActivity(), “SMS enviado”, Toast.LENGTH_LONG).show(); } catch (Exception e) { Toast.makeText(getActivity(), “Error al enviar el SMS”, Toast.LENGTH_LONG).show(); } } }); // end onclick botón enviar SMS
}
// Establecemos el adaptador del listado listado.setAdapter(adapter); return v;
// Receptor de mensajes para recibir el evento del sistema // operativo cuando se reciba un nuevo SMS BroadcastReceiver IncomingSMSReceiver = new BroadcastReceiver() { @Override // Evento que ocurre cuando se recibe un mensaje public void onReceive(Context context, Intent intent) { // Obtenemos el contenido del mensaje SMS Bundle bundle = intent.getExtras(); SmsMessage[] mensjs = null;
337
Aula Mentor
338
String str = “”; // Si el mensaje contiene algo if (bundle != null) { // Obtenemos la información del SMS recibido. // El campo PDU significa “Protocol Description Unit” // y es el formato estándar de los mensajes cortos // SMS Object[] pdus = (Object[]) bundle.get(“pdus”); // Pasamos todos los mensajes recibidos en formato // pdu a una matriz del tipo SmsMessage que contendrá // los mensajes en un formato interno y accesible por // Android. // Definimos el tamaño de la matriz con el nº de // mensaje recibidos mensjs = new SmsMessage[pdus.length]; // Recorremos todos los mensajes recibidos for (int i=0; i
View more...
Comments