Sesión 3 - Gráficos avanzados
Hasta este momento hemos visto como crear la interfaz de usuario de nuestra aplicación utilizando una serie de componentes predefinidos. Ahora vamos a ver cómo personalizar los componentes existentes o crear nuestros propios componentes.
Comenzaremos viendo cómo establecer el aspecto de los diferentes componentes de la interfaz de los que disponemos en Android, para así poder dar a nuestras aplicaciones un estilo gráfico propio. A continuación, veremos cómo crear componentes propios, en los que podamos tener un control absoluto sobre lo que se dibuja en pantalla. Por último, trataremos la forma de crear gráficos 3D y aplicaciones que necesiten una elevada tasa de refresco, como son los videojuegos.
Elementos drawables
Un drawable es un tipo de recurso que puede ser dibujado en pantalla. Podremos utilizarlos para especificar el aspecto que van a tener los diferentes componentes de la interfaz, o partes de éstos. Estos drawables podrán ser definidos en XML o de forma programática. Entre los diferentes tipos de drawables existentes encontramos:
Color: Rellena el lienzo de un determinado color.
Gradiente: Rellena el lienzo con un gradiente.
Forma (shape): Se pueden definir una serie de primitivas geométricas básicas como drawables.
Imágen (bitmap): Una imagen se comporta como drawable, ya que podrá ser dibujada y referenciada de la misma forma que el resto.
Nine-patch: Tipo especial de imagen PNG que al ser escalada sólo se escala su parte central, pero no su marco.
Animación: Define una animación por fotogramas, como veremos más adelante.
Capa (layer list): Es un drawable que contiene otros drawables. Cada uno especificará la posición en la que se ubica dentro de la capa.
Estados (state list): Este drawable puede mostrar diferentes contenidos (que a su vez son drawables) según el estado en el que se encuentre. Por ejemplo sirve para definir un botón, que se mostrará de forma distinta según si está normal, presionado, o inhabilitado.
Niveles (level list): Similar al anterior, pero en este caso cada item tiene asignado un valor numérico (nivel). Al establecer el nivel del drawable se mostrará el item cuyo nivel sea mayor o igual que el indicado.
Transición (transition): Nos permite mostrar una transición de un drawable a otro mediante un fundido.
Inserción (inset): Ubica un drawable dentro de otro, en la posición especificada.
Recorte (clip): Realiza un recorte de un drawable.
Escala (scale): Cambia el tamaño de un drawable.
Todos los drawables derivan de la clase Drawable
. Esta nos permite que todos ellos puedan ser utilizados de la misma forma, independientemente del tipo del que se trate. Se puede consultar la lista completa de drawables y su especificación en la siguiente dirección:
Por ejemplo, vamos a definir un drawable que muestre un rectángulo rojo con borde azul, creando un fichero XML de nombre rectangulo.xml
en el directorio /res/drawable/
. El fichero puede tener el siguiente contenido:
Podremos hacer referencia a este rectángulo desde el código mediante R.drawable.rectangulo
y mostrarlo en la interfaz asignándolo a un componente de alto nivel como por ejemplo ImageView
, o bien hacer referencia a él desde un atributo del XML mediante @drawable/rectangulo
.
Por ejemplo, podríamos especificar este drawable en el atributo android:background
de la etiqueta Button
dentro de nuestro layout, para que así el botón pase a tener como aspecto una forma rectangular de color rojo y con borde azul. De la misma forma podríamos darle al botón el aspecto de cualquier otro tipo de drawable de los vistos anteriormente. A continuación vamos a ver con más detalle los tipos de drawables más interesantes.
Imágenes
Las imágenes que introduzcamos en los directorios de recursos de tipo drawable (/res/drawable/
) podrán ser tratadas igual que cualquier otro tipo de drawable. Por ejemplo, si introducimos en dicho directorio una imagen titulo.png
, podremos hacer referencia a ella en los atributos de los XML mediante @drawable/titulo
(no se pone la extensión), o bien desde el código mediante R.drawable.titulo
.
Las imágenes se encapsulan en la clase Bitmap
. Los bitmaps pueden ser mutables o inmutables, según si se nos permite modificar el valor de sus pixels o no respectivamente.
Si el bitmap se crea a partir de un array de pixels, de un recurso con la imagen, o de otro bitmap, tendremos un bitmap inmutable.
Si creamos el bitmap vacío, simplemente especificando su altura y su anchura, entonces será mutable (en este caso no tendría sentido que fuese inmutable ya que sería imposible darle contenido). También podemos conseguir un bitmap mutable haciendo una copia de un bitmap existente mediante el método copy
, indicando que queremos que el bitmap resultante sea mutable.
Para crear un bitmap vacío, a partir de un array de pixels, o a partir de otro bitmap, tenemos una serie de métodos estáticos createBitmap
dentro de la clase Bitmap
.
Para crear un bitmap a partir de un fichero de imagen (GIF, JPEG, o PNG, siendo este último el formato recomendado) utilizaremos la clase BitmapFactory
. Dentro de ella tenemos varios métodos con prefijo decode
que nos permiten leer las imágenes de diferentes formas: de un array de bytes en memoria, de un flujo de entrada, de un fichero, de una URL, o de un recurso de la aplicación. Por ejemplo, si tenemos una imagen (titulo.png
) en el directorio de drawables podemos leerla como Bitmap
de la siguiente forma:
Al crear un bitmap a partir de otro, podremos realizar diferentes transformaciones (escalado, rotación, etc).
Una vez no se vaya a utilizar más el bitmap, es recomendable liberar la memoria que ocupa. Podemos hacer esto llamando a su método recycle
.
Imágenes nine-patch
Como hemos comentado anteriormente, utilizaremos los drawables para especificar el aspecto que queremos que tengan los componentes de la interfaz. La forma más flexible de definir este aspecto es especificar una imagen propia. Sin embargo, encontramos el problema de que los componentes normalmente no tendrán siempre el mismo tamaño, sino que Android los "estirará" según su contenido y según los parámetros de layout especificados (es decir, si deben ajustarse a su contenido o llenar todo el espacio disponible). Esto es un problema, ya que si siempre especificamos la misma imagen como aspecto para estos componentes, al estirarla veremos que ésta se deforma, dando un aspecto terrible a nuestra aplicación, como podemos ver a continuación:
Sin embargo, tenemos un tipo especial de imágenes PNG llamadas nine-patch (llevan extensión .9.png
), que nos permitirán evitar este problema. Normalmente la parte central de nuestros componentes es homogénea, por lo que no pasa nada si se estira. Sin embargo, los bordes si que contienen un mayor número de detalles, que no deberían ser deformados, especialmente las esquinas. Las imágenes nine-patch se dividen en 9 regiones: la parte central, que puede ser escalada en cualquier dirección, las esquinas, que nunca pueden escaladas, y los bordes, que sólo pueden ser escalados en su misma dirección (horizontal o vertical). A continuación vemos un ejemplo de dicha división:
Si ponemos una imagen de este tipo como drawable de fondo para un botón, veremos que siempre se mostrará con el aspecto correcto, independientemente de su contenido:
Podemos crear este tipo de imágenes con la herramienta draw9patch
que podemos encontrar en el subdirectorio tools
del SDK de Android. Lo único que necesitaremos es arrastrar el PNG que queramos tratar como nine-patch, y añadir una serie de píxeles en el marco de la imagen para marcar las regiones:
La fila de píxeles superior y la columna izquierda indican las zonas de la imagen que son flexibles y que se pueden ampliar si es necesario repitiendo su contenido. En el caso de la fila superior, indica que se pueden estirar en la horizontal, mientras que los del lateral izquierdo corresponden a la vertical.
Opcionalmente podemos especificar en la fila inferior y en la columna derecha la zona que utilizaremos como contenido. Por ejemplo, si utilizamos la imagen como marco de un botón, esta será la zona donde se ubicará el texto que pongamos en el botón. Marcando la casilla Show content veremos en el lateral derecho de la herramienta una previsualización de la zona de contenido.
Lista de estados
Siguiendo con el ejemplo del botón, encontramos ahora un nuevo problema. Los botones no deben tener siempre el mismo aspecto de fondo, normalmente cambiarán de aspecto cuando están pulsados o seleccionados, sin embargo sólo tenemos la posibilidad de especificar un único drawable como fondo. Para poder personalizar el aspecto de todos los estados en los que se encuentra el botón tenemos un tipo de drawable llamado state list drawable. Se define en XML, y nos permitirá especificar un drawable diferente para cada estado en el que se puedan encontrar los componentes de la interfaz, de forma que en cada momento el componente mostrará el aspecto correspondiente a su estado actual.
Por ejemplo, podemos especificar los estados de un botón (no seleccionado, seleccionado, y pulsado) de la siguiente forma:
Los drawables especificados para cada estado pueden ser de cualquier tipo (por ejemplo imágenes, nine-patch, o formas definidas en XML).
Un drawable similar es el de tipo level list, pero en este caso los diferentes posibles drawables a mostrar se especifican para un rango de valores numéricos. ¿Para qué tipos de componentes de la interfaz podría resultar esto de utilidad?
Animación por fotogramas
Este tipo de drawable nos permite definir una animación a partir de diferentes fotogramas, que deberemos especificar también como drawables, además del tiempo en milisegundos que durará el fotograma. Se definen en XML de la siguiente forma:
Además, la propiedad one shot nos indica si la animación se va a reproducir sólo una vez o en bucle infinito. Al ponerla como false
especificamos que se reproduzca de forma continuada.
Desde el código, podremos obtener la animación de la siguiente forma, considerando que la hemos guardado en un fichero animacion.xml
:
De forma alternativa, podríamos haberla definido de forma programática de la siguiente forma:
La diferencia entre
Bitmap
yBitmapDrawable
reside en que en el primer caso simplemente tenemos una imagen, mientras que en el segundo lo que tenemos es un drawable que encapsula una imagen, es decir, se le podrá proporcionar a cualquier componente que acepte drawables en general como entrada, y concretamente lo que dibujará será la imagen (Bitmap
) que contiene.
Para que comience la reproducción deberemos llamar al método start
de la animación:
De la misma forma, podemos detenerla con el método stop
:
El método
start
no puede ser llamado desde el métodoonCreate
de nuestra actividad, ya que en ese momento el drawable todavía no está vinculado a la vista. Si lo que queremos es que se ponga en marcha nada más cargarse la actividad, el lugar idóneo para invocarlo es el eventoonWindowFocusChanged
. Lo recomendable será llamar astart
cuando obtengamos el foco, y astop
cuando lo perdamos.
Definición programática
Vamos a suponer que tenemos un ImageView
con identificador visor
y un drawable de nombre rectangulo
. Normalmente especificaremos directamente en el XML el drawable que queremos mostrar en el ImageView
. Para ello deberemos añadir el atributo android:src = "@drawable/rectangulo"
en la definición del ImageView
.
Podremos también obtener una referencia a dicha vista y mostrar en ella nuestro rectángulo especificando el identificador del drawable de la siguiente forma:
Otra alternativa para mostrarlo es obtener primero el objeto Drawable
y posteriormente incluirlo en el ImageView
:
Estas primitivas básicas también se pueden crear directamente de forma programática. En el paquete android.graphics.drawable.shape
podemos encontrar clases que encapsulan diferentes formas geométricas. Podríamos crear el rectángulo de la siguiente forma:
Componentes propios
Si no hay ningún componente predefinido que se adapte a nuestras necesidades, podemos crear un nuevo tipo de vista (View
) en la que especificaremos exactamente qué es lo que queremos dibujar en la pantalla. El primer paso consistirá en crear una subclase de View
en la que sobrescribiremos el método onDraw
, que es el que define la forma en la que se dibuja el componente.
Lienzo y pincel
El método onDraw
recibe como parámetro el lienzo (Canvas
) en el que deberemos dibujar. En este lienzo podremos dibujar diferentes tipos de elementos, como primitivas geométricas, texto e imágenes.
No confundir el
Canvas
de Android con elCanvas
que existe en Java ME/SE. En Java ME/SE elCanvas
es un componente de la interfaz, que equivaldría aView
en Android, mientras que elCanvas
de Android es más parecido al objetoGraphics
de Java ME/SE, que encapsula el contexto gráfico (o lienzo) del área en la que vamos a dibujar.
Además, para dibujar determinados tipos de elementos deberemos especificar también el tipo de pincel a utilizar (Paint
), en el que especificaremos una serie de atributos como su color, grosor, etc.
Por ejemplo, para especificar un pincel que pinte en color rojo escribiremos lo siguiente:
Las propiedades que podemos establecer en el pincel son:
Color plano: Con
setARGB
osetColor
se puede especificar el código ARGB del color o bien utilizar constantes con colores predefinidos de la claseColor
.Gradientes y shaders: Se pueden rellenar las figuras utilizando shaders de gradiente o de bitmap. Para utilizar un shader tenemos el método
setShader
, y tenemos varios shaders disponibles, como distintos shaders de gradiente (LinearShader
,RadialShader
,SweepShader
),BitmapShader
para rellenar utilizando un mapa de bits como patrón, yComposeShader
para combinar dos shaders distintos.
Máscaras: Nos sirven para aplicar un suavizado a los
gráficos (
BlurMaskFilter
) o dar efecto de relieve(
EmbossMaskFilter
). Se aplican consetMaskFilter
.
Sombras: Podemos crear efectos de sombra con
setShadowLayer
.Filtros de color: Aplica un filtro de color a los gráficos dibujados, alterando así su color original. Se aplica con
setColorFilter
.Estilo de la figura: Se puede especificar con
setStyle
que se dibuje sólo el trazo, sólo el relleno, o ambos.
Estilo del trazo: Podemos especificar el grosor
del trazo (
setStrokeWidth
), el tipo de línea (setPathEffect
),la forma de las uniones en las polilíneas
(redondeada/
ROUND
, a inglete/MITER
,o biselada/
BEVEL
, consetStrokeJoin
),o la forma de las terminaciones (cuadrada/
SQUARE
,redonda/
ROUND
o recortada/BUTT
,con
setStrokeCap
).
Antialiasing: Podemos aplicar antialiasing con
setAntiAlias
a los gráficos para evitar el efecto sierra.Dithering: Si el dispositivo no puede mostrar los 16 millones de colores, en caso de haber un gradiente, para que el cambio de color no sea brusco, con esta opción (
setDither
) se mezclan pixels de diferentes colores para dar la sensación de que la transición entre colores es más suave.
Modo de transferencia: Con
setXferMode
podemos cambiar el modo de transferencia con el que se dibuja. Por ejemplo, podemos hacer que sólo se dibuje encima de pixels que tengan un determinado color.Estilo del texto: Podemos también especificar el tipo de fuente a utilizar y sus atributos. Lo veremos con más detalle más adelante.
Una vez establecido el tipo de pincel, podremos utilizarlo para dibujar diferentes elementos en el lienzo, utilizando métodos de la clase Canvas
.
En el lienzo podremos también establecer algunas propiedades, como el área de recorte (clipRect
), que en este caso no tiene porque ser rectangular (clipPath
), o transformaciones geométricas (translate
, scale
, rotate
, skew
, o setMatrix
). Si queremos cambiar temporalmente estas propiedades, y luego volver a dejar el lienzo como estaba originalmente, podemos utilizar los métodos save
y restore
.
Vamos a ver a continuación como utilizar los métodos del lienzo para dibujar distintos tipos de primitivas geométricas.
Primitivas geométricas
En la clase Canvas
encontramos métodos para dibujar diferentes tipos de primitivas geométricas. Estos tipos son:
Puntos: Con
drawPoint
podemos dibujar un punto en las coordenadas X, Y especificadas.Líneas: Con
drawLine
dibujamos una línea recta desde un punto de origen hasta un punto destino.Polilíneas: Podemos dibujar una polilínea mediante
drawPath
. La polilínea se especificará mediante un objeto de clasePath
, en el que iremos añadiendo los segmentos de los que se compone. Este objetoPath
representa un contorno, que podemos crear no sólo a partir de segmentos rectos, sino también de curvas cuadráticas y cúbicas.Rectángulos: Con
drawRect
podemos dibujar un rectángulo con los límites superior, inferior, izquierdo y derecho especificados.Rectángulos con bordes redondeados: Es también posible dibujar un rectángulo con esquinas redondeadas con
drawRoundRect
. En este caso deberemos especificar también el radio de las esquinas.Círculos: Con
drawCircle
podemos dibujar un círculo dando su centro y su radio.Óvalos: Los óvalos son un caso más general que el del círculo, y los crearemos con
drawOval
proporcionando el rectángulo que lo engloba.Arcos: También podemos dibujar arcos, que consisten en un segmento del contorno de un óvalo. Se crean con
drawArc
, proporcionando, además de los mismos datos que en el caso del óvalo, los ángulos que limitan el arco.Todo el lienzo: Podemos también especificar que todo el lienzo se rellene de un color determinado con
drawColor
odrawARGB
. Esto resulta útil para limpiar el fondo antes de empezar a dibujar.
A continuación mostramos un ejemplo de cómo podríamos dibujar una polilínea y un rectángulo:
Cadenas de texto
Para dibujar texto podemos utilizar el método drawText
. De forma alternativa, se puede utilizar drawPosText
para mostrar texto especificando una por una la posición de cada carácter, y drawTextOnPath
para dibujar el texto a lo largo de un contorno (Path
).
Para especificar el tipo de fuente y sus atributos, utilizaremos las propiedades del objeto Paint
. Las propiedades que podemos especificar del texto son:
Fuente: Con
setTypeface
podemos especificar la fuente, que puede ser alguna de las fuentes predefinidas (Sans Serif, Serif, Monoespaciada), o bien una fuente propia a partir de un fichero de fuente. También podemos especificar si el estilo de la fuente será normal, cursiva, negrita, o negrita cursiva.Tamaño: Podemos establecer el tamaño del texto con
setTextSize
.Anchura: Con
setTextScaleX
podemos modificar la anchura del texto sin alterar la altura.Inclinación: Con
setTextSkewX
podemos aplicar un efecto de desencajado al texto, pudiendo establecer la inclinación que tendrán los carácteres.Subrayado: Con
setUnderlineText
podemos activar o desactivar el subrayado.Tachado: Con
setStrikeThruText
podemos activar o desactivar el efecto de tachado.Negrita falsa: Con
setFakeBoldText
podemos darle al texto un efecto de negrita, aunque la fuente no sea de este tipo.Alineación: Con
setTextAlign
podemos especificar si el texto de alinea al centro, a la derecha, o a la izquierda.Subpixel: Se renderiza a nivel de subpixel. El texto se genera a una resolución mayor que la de la pantalla donde lo vamos a mostrar, y para cada pixel real se habrán generado varios pixels. Si aplicamos antialiasing, a la hora de mostrar el pixel real, se determinará un nivel de gris dependiendo de cuantos pixels ficticios estén activados. Se consigue un aspecto de texto más suavizado.
Texto lineal: Muestra el texto con sus dimensiones reales de forma lineal, sin ajustar los tamaños de los caracteres a la cuadrícula de pixels de la pantalla.
Contorno del texto: Aunque esto no es una propiedad del texto, el objeto
Paint
también nos permite obtener el contorno (Path
) de un texto dado, para así poder aplicar al texto los mismos efectos que a cualquier otro contorno que dibujemos.
Con esto hemos visto como dibujar texto en pantalla, pero para poderlo ubicar de forma correcta es importante saber el tamaño en pixels del texto a mostrar. Vamos a ver ahora cómo obtener estas métricas.
Las métricas se obtendrán a partir del objeto Paint
en el que hemos definido las propiedades de la fuente a utilizar. Mediante getFontMetrics
podemos obtener una serie de métricas de la fuente actual, que nos dan las distancias recomendadas que debemos dejar entre diferentes líneas de texto:
ascent
: Distancia que asciende la fuente desde la línea de base. Para texto con espaciado sencillo es la distancia que se recomienda dejar por encima del texto. Se trata de un valor negativo.descent
: Distancia que baja la fuente desde la línea de base. Para texto con espaciado sencillo es la distancia que se recomienda dejar por debajo del texto. Se trata de un valor positivo.leading
: Distancia que se recomienda dejar entre dos líneas consecutivas de texto.bottom
: Es la máxima distancia que puede bajar un símbolo desde la línea de base. Es un valor positivo.top
: Es la máxima distancia que puede subir un símbolo desde la línea de base. Es un valor negativo.
Los anteriores valores son métricas generales de la fuente, pero muchas veces necesitaremos saber la anchura de una determinada cadena de texto, que ya no sólo depende de la fuente sino también del texto. Tenemos una serie de métodos con los que obtener este tipo de información:
measureText
: Nos da la anchura en pixels de una cadena de texto con la fuente actual.breakText
: Método útil para cortar el texto de forma que no se salga de los márgenes de la pantalla. Se le proporciona la anchura máxima que puede tener la línea, y el método nos dice cuántos carácteres de la cadena proporcionada caben en dicha línea.getTextWidths
: Nos da la anchura individual de cada carácter del texto proporcionado.getTextBounds
: Nos devuelve un rectángulo con las dimensiones del texto, tanto anchura como altura.
Imágenes
Podemos también dibujar en nuestro lienzo imágenes que hayamos cargado como Bitmap
. Esto se hará utilizando el método drawBitmap
.
También podremos realizar transformaciones geométricas en la imagen al mostrarla en el lienzo lienzo con drawBitmap
, e incluso podemos dibujar el bitmap sobre una malla poligonal con drawBitmapMesh
Drawables
También podemos dibujar objetos de tipo drawable en nuestro lienzo, esta vez mediante el método draw
definido en la clase Drawable
. Esto nos permitirá mostrar en nuestro componente cualquiera de los tipos disponibles de drawables, tanto definidos en XML como de forma programática.
Medición del componente
Al crear un nuevo componente, además de sobreescribir el método onDraw
, es buena idea sobreescribir también el método onMeasure
. Este método será invocado por el sistema cuando vaya a ubicarlo en el layout, para asignarle un tamaño. Para cada dimensión (altura y anchura), nos pasa dos parámetros:
Tamaño: Tamaño en píxeles solicitado para la dimensión (altura o anchura).
Modo: Puede ser
EXACTLY
,AT_MOST
, oUNSPECIFIED
. En el primer caso indica que el componente debe tener exactamente el tamaño solicitado, el segundo indica que como mucho puede tener ese tamaño, y el tercero nos da libertad para decidir el tamaño.
Antes de finalizar onMeasure
, deberemos llamar obligatoriamente a setMeasuredDimension(width, height)
proporcionando el tamaño que queramos que tenga nuestro componente. Una posible implementación sería la siguiente:
Podemos ver que tenemos unas dimensiones preferidas por defecto para nuestro componente. Si nos piden unas dimensiones exactas, ponemos esas dimensiones, pero si nos piden unas dimensiones como máximo, nos quedamos con el mínimo entre nuestra dimensión preferida y la que se ha especificado como límite máximo que puede tener.
Atributos propios
Si creamos un nuevo tipo de vista, es muy probable que necesitemos parametrizarla de alguna forma. Por ejemplo, si queremos dibujar una gráfica que nos muestre un porcentaje, necesitaremos proporcionar un valor numérico como porcentaje a mostrar. Si vamos a crear la vista siempre de forma programática esto no es ningún problema, ya que basta con incluir en nuestra clase un método que establezca dicha propiedad.
Sin embargo, si queremos que nuestro componente se pueda añadir desde el XML, será necesario poder pasarle dicho valor como atributo. Para ello en primer lugar debemos declarar los atributos propios en un fichero /res/values/attrs.xml
:
En el XML donde definimos el layout, podemos especificar nuestro componente utilizando como nombre de la etiqueta el nombre completo (incluyendo el paquete) de la clase donde hemos definido la vista:
Podemos fijarnos en que para declarar el atributo propio hemos tenido que especificar el espacio de nombres en el que se encuentra. En dicho espacio de nombres deberemos especificar el paquete que hemos declarado en AndroidManifest.xml
para la aplicación (en nuestro caso es.ua.jtech.grafica
).
En primer lugar podemos ver que debemos definir todos los posibles constructores de las vistas, ya que cuando se cree desde el XML se invocará uno de los que reciben la lista de atributos especificados. Una vez recibamos dicha lista de atributos, deberemos obtener el conjunto de atributos propios mediante obtainStyledAttributes
, y posteriormente obtener los valores de cada atributo concreto dentro de dicho conjunto.
Actualización del contenido
Es posible que en un momento dado cambien los datos a mostrar y necesitemos actualizar el contenido que nuestro componente está dibujando en pantalla. Podemos forzar que se vuelva a dibujar llamando al método invalidate
de nuestra vista (View
).
Esto podemos utilizarlo también para crear animaciones. De hecho, para crear una animación simplemente deberemos cambiar el contenido del lienzo conforme pasa el tiempo. Una forma de hacer esto es simplemente cambiar mediante un hilo o temporizadores propiedades de los objetos de la escena (como sus posiciones), y forzar a que se vuelva a redibujar el contenido del lienzo cada cierto tiempo.
Sin embargo, si necesitamos contar con una elevada tasa de refresco, como por ejemplo en el caso de un videojuego, será recomendable utilizar una vista de tipo SurfaceView
como veremos a continuación.
Gráficos 3D
Para mostrar gráficos 3D en Android contamos con OpenGL ES, un subconjunto de la librería gráfica OpenGL destinado a dispositivos móviles.
Hasta ahora hemos visto que para mostrar gráficos propios podíamos usar un componente que heredase de View
. Estos componentes funcionan bien si no necesitamos realizar repintados continuos o mostrar gráficos 3D.
Sin embargo, en el caso de tener una aplicación con una gran carga gráfica, como puede ser un videojuego o una aplicación que muestre gráficos 3D, en lugar de View
deberemos utilizar SurfaceView
. Esta última clase nos proporciona una superficie en la que podemos dibujar desde un hilo en segundo plano, lo cual libera al hilo principal de la aplicación de la carga gráfica.
Vamos a ver en primer lugar cómo utilizar SurfaceView
, y las diferencias existentes con View
.
Para crear una vista con SurfaceView
tendremos que crear una nueva subclase de dicha clase (en lugar de View
). Pero en este caso no bastará con definir el método onDraw
, ahora deberemos crearnos un hilo independiente y proporcionarle la superficie en la que dibujar (SurfaceHolder
). Además, en nuestra subclase de SurfaceView
también implementaremos la interfaz SurfaceHolder.Callback
que nos permitirá estar al tanto de cuando la superficie se crea, cambia, o se destruye.
Cuando la superficie sea creada pondremos en marcha nuestro hilo de dibujado, y lo pararemos cuando la superficie sea destruida. A continuación mostramos un ejemplo de dicha clase:
Como vemos, la clase SurfaceView
simplemente se encarga de obtener la superficie y poner en marcha o parar el hilo de dibujado. En este caso la acción estará realmente en el hilo, que es donde especificaremos la forma en la que se debe dibujar el componente. Vamos a ver a continuación cómo podríamos implementar dicho hilo:
Podemos ver que en el bucle principal de nuestro hilo obtenermos el lienzo (Canvas
) a partir de la superficie (SurfaceHolder
) mediante el método lockCanvas
. Esto deja el lienzo bloqueado para nuestro uso, por ese motivo es importante asegurarnos de que siempre se desbloquee. Para tal fin hemos puesto unlockCanvasAndPost
dentro del bloque finally
. Además debemos siempre dibujar de forma sincronizada con el objeto SurfaceHolder
, para así evitar problemas de concurrencia en el acceso a su lienzo.
Para aplicaciones como videojuegos 2D sencillo un código como el anterior puede ser suficiente (la clase View
sería demasiado lenta para un videojuego). Sin embargo, lo realmente interesante es utilizar SurfaceView
junto a OpenGL, para así poder mostrar gráficos 3D, o escalados, rotaciones y otras transformaciones sobre superficies 2D de forma eficiente.
El estudio de la librería OpenGL queda fuera del ámbito de este curso. A continuación veremos un ejemplo de cómo utilizar OpenGL (concretamente OpenGL ES) vinculado a nuestra SurfaceView
.
Realmente la implementación de nuestra clase que hereda de SurfaceView
no cambiará, simplemente modificaremos nuestro hilo, que es quien realmente realiza el dibujado. Toda la inicialización de OpenGL deberá realizarse dentro de nuestro hilo (en el método run
), ya que sólo se puede acceder a las operaciones de dicha librería desde el mismo hilo en el que se inicializó. En caso de que intentásemos acceder desde otro hilo obtendríamos un error indicando que no existe ningún contexto activo de OpenGL.
En este caso nuestro hilo podría contener el siguiente código:
En primer lugar debemos inicializar la interfaz EGL, que hace de vínculo entre la plataforma nativa y la librería OpenGL:
A continuación debemos proceder a la inicialización de la interfaz de la librería OpenGL:
Una vez hecho esto, ya sólo nos queda ver cómo dibujar una malla 3D. Vamos a ver como ejemplo el dibujo de un triángulo:
Para finalizar, es importante que cuando la superficie se destruya se haga una limpieza de los recursos utilizados por OpenGL:
Podemos llamar a este método cuando el hilo se detenga (debemos asegurarnos que se haya detenido llamando a join
previamente).
A partir de Android 1.5 se incluye la clase GLSurfaceView
, que ya incluye la inicialización del contexto GL y nos evita tener que hacer esto manualmente. Esto simplificará bastante el uso de la librería. Vamos a ver a continuación un ejemplo de como trabajar con dicha clase.
En este caso ya no será necesario crear una subclase de GLSurfaceView
, ya que la inicialización y gestión del hilo de OpenGL siempre es igual. Lo único que nos interesará cambiar es lo que se muestra en la escena. Para ello deberemos crear una subclase de GLSurfaceViewRenderer
que nos obliga a definir los siguientes métodos:
Podemos observar que será el método onDrawFrame
en el que deberemos escribir el código para mostrar los gráficos. Con hacer esto será suficiente, y no tendremos que encargarnos de crear el hilo ni de inicializar ni destruir el contexto.
Para mostrar estos gráficos en la vista deberemos proporcionar nuestro renderer al objeto GLSurfaceView
:
Por último, será importante transmitir los eventos onPause
y onResume
de nuestra actividad a la vista de OpenGL, para así liberar a la aplicación de la carga gráfica cuando permanezca en segundo plano. El código completo de la actividad quedaría como se muestra a continuación:
Para utilizar esta vista deberemos tener al menos unos conocimientos básicos de OpenGL. Si lo que queremos es iniciarnos en el desarrollo de videojuegos para Android, contamos con diferentes librerías y frameworks que nos van a facilitar bastante el trabajo. Destacamos las siguientes:
Última actualización
¿Te fue útil?