Hasta ahora hemos visto la creación de aplicaciones con una interfaz gráfica creada a partir de una serie de componentes de alto nivel definidos en la API LCDUI (alertas, campos de texto, listas, formularios).
En este punto veremos como dibujar nuestros propios gráficos directamente en pantalla. Para ello Java nos proporciona acceso a bajo nivel al contexto gráfico del área donde vayamos a dibujar, permitiéndonos a través de éste modificar los pixels de este área, dibujar una serie de figuras geométricas, así como volcar imágenes en ella.
También podremos acceder a la entrada del usuario a bajo nivel, conociendo en todo momento cuándo el usuario pulsa o suelta cualquier tecla del móvil.
Este acceso a bajo nivel será necesario en aplicaciones como juegos, donde debemos tener un control absoluto sobre la entrada y sobre lo que dibujamos en pantalla en cada momento. El tener este mayor control tiene el inconveniente de que las aplicaciones serán menos portables, ya que dibujaremos los gráficos pensando en una determinada resolución de pantalla y un determinado tipo de teclado, pero cuando la queramos llevar a otro dispositivo en el que estos componentes sean distintos deberemos hacer cambios en el código.
Por esta razón para las aplicaciones que utilizan esta API a bajo nivel, como los juegos Java para móviles, encontramos distintas versiones para cada modelo de dispositivo. Dada la heterogeneidad de estos dispositivos, resulta más sencillo rehacer la aplicación para cada modelo distinto que realizar una aplicación adaptable a las características de cada modelo.
Al programar las aplicaciones deberemos facilitar en la medida de lo posible futuros cambios para adaptarla a otros modelos, permitiendo reutilizar la máxima cantidad de código posible.
La API de gráficos a bajo nivel de LCDUI es muy parecida a la existente en AWT, por lo que el aprendizaje de esta API para programadores que conozcan la de AWT va a ser casi inmediato.
Las clases que implementan la API de bajo nivel en LCDUI son Canvas
y Graphics
. Estas clases reciben el mismo nombre que las de AWT,
y se utilizan de una forma muy parecida. Tienen alguna diferencia en cuanto
a su interfaz para adaptarse a las necesidades de los dispositivos móviles.
El Canvas
es un tipo de elemento displayable correspondiente
a una pantalla vacía en la que nosotros podremos dibujar a bajo nivel
el contenido que queramos. Además este componente nos permitirá
leer los eventos de entrada del usuario a bajo nivel.
Esta pantalla del móvil tiene un contexto gráfico asociado que
nosotros podremos utilizar para dibujar en ella. Este objeto encapsula el raster
de pantalla (la matriz de pixels de los que se compone la pantalla)
y además tiene una serie de atributos con los que podremos modificar
la forma en la que se dibuja en este raster. Este contexto gráfico
se definirá en un objeto de la clase Graphics
. Este objeto
nos ofrece una serie de métodos que nos permiten dibujar distintos elementos
en pantalla. Más adelante veremos con detalle los métodos más
importantes.
Este objeto Graphics
para dibujar en la pantalla del dispositivo
nos lo deberá proporcionar el sistema en el momento en que se vaya a
dibujar, no podremos obtenerlo nosotros por nuestra cuenta de ninguna otra forma.
Esto es lo que se conoce como render pasivo, definimos la forma en
la que se dibuja pero es el sistema el que decidirá cuándo hacerlo.
Creación de un Canvas
Para definir la forma en la que se va a dibujar nuestro componente deberemos
extender la clase Canvas
redefiniendo su método paint
.
Dentro de este método es donde definiremos cómo se realiza el
dibujado de la pantalla. Esto lo haremos de la siguiente forma:
public class MiCanvas extends Canvas { public void paint(Graphics g) { // Dibujamos en la pantalla // usando el objeto g proporcionado } }
Con esto en la clase MiCanvas
hemos creado una pantalla en la
que nosotros controlamos lo que se dibuja. Este método paint
nunca debemos invocarlo nosotros, será el sistema el que se encargue
de invocarlo cuando necesite dibujar el contenido de la pantalla. En ese momento
se proporcionará como parámetro el objeto correspondiente al contexto
gráfico de la pantalla del dispositivo, que podremos utilizar para dibujar
en ella. Dentro de este método es donde definiremos cómo dibujar
en la pantalla, utilizando para ello el objeto de contexto gráfico Graphics
.
Siempre deberemos dibujar utilizando el objeto Graphics
dentro
del método paint
. Guardarnos este objeto y utilizarlo después
de haberse terminado de ejecutar paint
puede producir un comportamiento
indeterminado, y por lo tanto no debe hacerse nunca.
Propiedades del Canvas
Las pantallas de los dispositivos pueden tener distintas resoluciones. Además normalmente el área donde podemos dibujar no ocupa toda la pantalla, ya que el móvil utiliza una franja superior para mostrar información como la cobertura o el título de la aplicación, y en la franja inferior para mostrar los comandos disponibles.
Es probable que nos interese conocer desde dentro de nuestra aplicación
el tamaño real del área del Canvas
en la que podemos
dibujar. Para ello tenemos los métodos getWidth
y getHeight
de la clase Canvas
, que nos devolverán el ancho y el alto
del área de dibujo respectivamente.
Para obtener información sobre el número de colores soportados
deberemos utilizar la clase Display
tal como vimos anteriormente,
ya que el número de colores es propio de todo el visor y no sólo
del área de dibujo.
Mostrar el Canvas
Podemos mostrar este componente en la pantalla del dispositivo igual que mostramos cualquier otro displayable:
MiCanvas mc = new MiCanvas(); mi_display.setCurrent(mc);
Es posible que queramos hacer que cuando se muestre este canvas se realice alguna acción, como por ejemplo poner en marcha alguna animación que se muestre en la pantalla. De la misma forma, cuando el canvas se deje de ver deberemos detener la animación. Para hacer esto deberemos tener constancia del momento en el que el canvas se muestra y se oculta.
Podremos saber esto debido a que los métodos showNotify
y hideNotify
de la clase Canvas
serán invocados
son invocados por el sistema cuando dicho componente se muestra o se oculta
respectivamente. Nosotros podremos en nuestra subclase de Canvas
redefinir estos métodos, que por defecto están vacíos,
para definir en ellos el código que se debe ejecutar al mostrarse u ocultarse
nuestro componente. Por ejemplo, si queremos poner en marcha o detener una animación,
podemos redefinir los métodos como se muestra a continuación:
public class MiCanvas extends Canvas { public void paint(Graphics g) { // Dibujamos en la pantalla // usando el objeto g proporcionado }
public void showNotify() {
// El Canvas se muestra
comenzarAnimacion();
}
public void hideNotify() {
// El Canvas se oculta
detenerAnimacion();
} }
De esta forma podemos utilizar estos dos métodos como respuesta a los eventos de aparición y ocultación del canvas.
El objeto Graphics
nos permitirá acceder al contexto gráfico
de un determinado componente, en nuestro caso el canvas, y a través
de él dibujar en el raster de este componente. En el caso del
contexto gráfico del canvas de LCDUI este raster corresponderá
a la pantalla del dispositivo móvil. Vamos a ver ahora como dibujar utilizando
dicho objeto Graphics
. Este objeto nos permitirá dibujar
distintas primitivas geométricas, texto e imágenes.
Atributos
El contexto gráfico tendrá asociados una serie de atributos que
indicarán cómo se va a dibujar en cada momento, como por ejemplo
el color o el tipo del lápiz que usamos para dibujar. El objeto Graphics
proporciona una serie de métodos para consultar o modificar estos atributos.
Podemos encontrar los siguientes atributos en el contexto gráfico de
LCDUI:
Podemos trabajar con los colores de dos formas distintas: tratando los
componentes R, G y B por separado, o de forma conjunta. En MIDP desaparece
la clase Color
que teníamos en AWT, por lo que deberemos
asignar los colores proporcionando directamente los valores numéricos
del color.
Si preferimos tratar los componentes de forma separada, tenemos los siguientes métodos para obtener o establecer el color actual del lápiz:
g.setColor(rojo, verde, azul);
int rojo = g.getRedComponent();
int green = g.getGreenComponent();
int blue = g.getBlueComponent();
Donde g
es el objeto Graphics
del contexto donde
vamos a dibujar. Estos componentes rojo, verde y azul tomarán valores
entre 0 y 255.
Podemos tratar estos componentes de forma conjunta empaquetándolos en un único entero. En hexadecimal se codifica de la siguiente forma:
0x00RRGGBB
Podremos leer o establecer el color utilizando este formato empaquetado con los siguientes métodos:
g.setColor(rgb);
int rgb = g.getColor();
Tenemos también métodos para trabajar con valores en escala de grises. Estos métodos nos pueden resultar útiles cuando trabajemos con dispositivos monocromos.
int gris = g.getGrayScale();
g.setGrayScale(gris);
Con estos métodos podemos establecer como color actual distintos
tonos en la escala de grises. El valor de gris se moverá en el intervalo
de 0 a 255. Si utilizamos getGrayScale
teniendo establecido
un color fuera de la escala de grises, convertirá este color a escala
de grises obteniendo su brillo.
Graphics.SOLID |
Línea sólida (se dibujan todos los pixels) |
Graphics.DOTTED |
Línea punteada (se salta algunos pixels sin dibujarlos) |
Podemos establecer el tipo del lápiz o consultarlo con los siguientes métodos:
int tipo = g.getStrokeStyle();
g.setStrokeStyle(tipo);
Font
para especificar la fuente
de texto que vamos a utilizar, al igual que en AWT. Podemos obtener o establecer
la fuente con los siguientes métodos:
Font fuente = g.getFont();
g.setFont(fuente);
g.setClip(x, y, ancho, alto);
También tenemos disponible el siguiente método:
g.clipRect(x, y, ancho, alto);
Este método establece un recorte en el área de recorte anterior.
Si ya existía un rectángulo de recorte, el nuevo rectángulo
de recorte será la intersección de ambos. Si queremos eliminar
el área de recorte anterior deberemos usar el método setClip
.
Figura 12. Sistema de coordenadas del área de dibujo
Podemos trasladar el origen de coordenadas utilizando el método
translate
. También tenemos métodos para obtener la traslación del origen de coordenadas.int x = g.getTranslateX();
int y = g.getTranslateY();
g.translate(x, y);Estas coordenadas no corresponden a pixels, sino a los límites de los pixels. De esta forma, el píxel de la esquina superior izquierda de la imagen se encontrará entre las coordenadas (0,0), (0,1), (1,0) y (1,1).
![]()
Figura 13. Coordenadas de los límites de los pixels
Dibujar primitivas geométricas
Una vez establecidos estos atributos en el contexto gráfico, podremos
dibujar en él una serie de elementos utilizando una serie de métodos
de Graphics
. Vamos a ver en primer lugar cómo dibujar una
serie de primitivas geométricas. Para ello tenemos una serie de métodos
que comienzan por draw_
para dibujar el contorno de una determinada
figura, o fill_
para dibujar dicha figura con relleno.
g.drawLine(x1, y1, x2, y2);
En este caso no encontramos ningún método fill
ya que las líneas no pueden tener relleno. Al dibujar una línea
se dibujarán los píxels situados inmediatamente abajo
y a la derecha de las coordenadas indicadas. Por ejemplo, si dibujamos con
drawLine(0, 0, 0, 0)
se dibujará el pixel de
la esquina superior izquierda.
g.drawRect(x, y, ancho, alto); g.fillRect(x, y, ancho, alto);
En el caso de fillRect
, lo que hará será rellenar
con el color actual los pixels situados entre las coordenadas limítrofes.
En el caso de drawRect
, la línea inferior y derecha se
dibujarán justo debajo y a la derecha respectivamente de las coordenadas
de dichos límites, es decir, se dibuja con un pixel más
de ancho y de alto que en el caso relleno. Esto es poco intuitivo, pero se
hace así para mantener la coherencia con el comportamiento de drawLine
.
Al menos, lo que siempre se nos asegura es que cuando utilizamos las mismas dimensiones no quede ningún hueco entre el dibujo del relleno y el del contorno.
Podemos también dibujar rectángulos con las esquinas redondeadas, utilizando los métodos:
g.drawRoundRect(x, y, ancho, alto, ancho_arco, alto_arco); g.fillRoundRect(x, y, ancho, alto, ancho_arco, alto_arco)
g.drawArc(x, y, ancho, alto, angulo_inicio, angulo_arco); g.fillArc(x, y, ancho, alto, angulo_inicio, angulo_arco);
Los ángulos especificados deben estar en grados. Por ejemplo, si queremos
dibujar un círculo o una elipse en angulo_arco
pondremos
un valor de 360 grados para que se cierre el arco. En el caso del círculo
los valores de ancho
y alto
serán iguales,
y en el caso de la elipse serán diferentes.
Figura 14. Ejemplos de diferentes primitivas
Por ejemplo, el siguiente canvas aparecerá con un dibujo de un círculo rojo y un cuadrado verde:
public class MiCanvas extends Canvas { public void paint(Graphics g) { g.setColor(0x00FF0000); g.fillArc(10,10,50,50,0,360); g.setColor(0x0000FF00); g.fillRect(60,60,50,50); } }
Puntos anchor
En MIDP se introduce una características no existente en AWT que son los puntos anchor. Estos puntos nos facilitarán el posicionamiento del texto y de las imágenes en la pantalla. Con los puntos anchor, además de dar una coordenada para posicionar estos elementos, diremos qué punto del elemento vamos a posicionar en dicha posición.
Para el posicionamiento horizontal tenemos las siguientes posibilidades:
Graphics.LEFT |
En las coordenadas especificadas se posiciona la parte izquierda del texto o de la imagen. |
Graphics.HCENTER |
En las coordenadas especificadas se posiciona el centro del texto o de la imagen. |
Graphics.RIGHT |
En las coordenadas especificadas se posiciona la parte derecha del texto o de la imagen. |
Para el posicionamiento vertical tenemos:
Graphics.TOP |
En las coordenadas especificadas se posiciona la parte superior del texto o de la imagen. |
Graphics.VCENTER |
En las coordenadas especificadas se posiciona el centro de la imagen. No se aplica a texto. |
Graphics.BASELINE |
En las coordenadas especificadas se posiciona la línea de base del texto. No se aplica a imágenes. |
Graphics.BOTTOM |
En las coordenadas especificadas se posiciona la parte inferior del texto o de la imagen. |
Cadenas de texto
Podemos dibujar una cadena de texto utilizando el método drawString
.
Deberemos proporcionar la cadena de texto de dibujar y el punto anchor
donde dibujarla.
g.drawString(cadena, x, y, anchor);
Por ejemplo, si dibujamos la cadena con:
g.drawString("Texto de prueba", 0, 0, Graphics.LEFT|Graphics.BASELINE);
Este punto corresponderá al inicio de la cadena (lado izquierdo), en la línea de base del texto como se muestra a continuación:
Figura 15. Línea de base del texto
Con esto dibujaremos un texto en pantalla, pero es posible que nos interese
conocer las coordenadas que limitan el texto, para saber exactamente el espacio
que ocupa en el área de dibujo. En AWT podíamos usar para esto
un objeto FontMetrics
, pero este objeto no existe en MIDP. En MIDP
la información sobre las métricas de la fuente está encapsulada
en la misma clase Font
por lo que será más sencillo
acceder a esta información. Podemos obtener esta información utilizando
los siguientes métodos de la clase Font
:
stringWidth(cadena)
: Nos devuelve el ancho
que tendrá la cadena cadena en pixels.getHeight()
: Nos devuelve la altura de la
fuente, es decir, la distancia entre las líneas de base de dos líneas
consecutivas de texto. Llamamos ascenso (ascent) a la altura típica
que suelen subir los caracteres desde la línea de base, y descenso
(descent) a lo que suelen bajar desde esta línea. La altura
será la suma del ascenso y el descenso de la fuente, más un
margen para evitar que se junten los caracteres de las dos líneas.
Es la distancia existente entre el punto superior (TOP
) y el
punto inferior (BOTTOM
) de la cadena de texto.getBaselinePosition
()
:
Nos devuelve el ascenso de la fuente, es decir, la altura típica desde
la línea de base hasta la parte superior de la fuente.Con estas medidas podremos conocer exactamente los límites de una cadena de texto, tal como se muestra a continuación:
Figura 16. Métricas del texto
Imágenes
Hemos visto como crear imágenes y como utilizarlas en componentes de
alto nivel. Estas mismas imágenes encapsuladas en la clase Image
,
podrán ser mostradas también en cualquier posición de nuestro
área de dibujo.
Para ello utilizaremos el método:
g.drawImage(img, x, y, anchor);
En este caso podremos dibujar tanto imágenes mutables como inmutables.
Vimos que las imágenes mutables son aquellas cuyo contenido puede ser
modificado. Vamos a ver ahora como hacer esta modificación. Las imágenes
mutables, al igual que el canvas, tienen un contexto gráfico
asociado. En el caso de las imágenes, este contexto gráfico representa
el contenido de la imagen que es un raster en memoria, pero podremos
dibujar en él igual que lo hacíamos en el canvas. Esto
es así debido a que dibujaremos también mediante un objeto de
la clase Graphics
. Podemos obtener este objeto de contexto gráfico
en cualquier momento invocando el método getGraphics
de
la imagen:
Graphics offg = img.getGraphics();
Si queremos modificar una imagen que hemos cargado de un fichero o de la red, y que por lo tanto es inmutable, podemos crear una copia mutable de la imagen para poder modificarla. Para hacer esto lo primero que deberemos hacer es crear la imagen mutable con el mismo tamaño que la inmutable que queremos copiar. Una vez creada podremos obtener su contexto gráfico, y dibujar en él la imagen inmutable, con lo que habremos hecho la copia de la imagen inmutable a una imagen mutable, que podrá ser modificada más adelante.
Image img_mut = Image.createImage(img.getWidth(), img.getHeight());
Graphics offg = img_mut.getGraphics();
offg.drawImage(img, 0, 0, Graphics.TOP|Graphics.LEFT);
Hasta ahora hemos visto como dibujar gráficos en pantalla, pero lo único que hacemos es definir un método que se encargue de dibujar el contenido del componente, y ese método será invocado cuando el sistema necesite dibujar la ventana.
Sin embargo puede interesarnos cambiar dinámicamente los gráficos de la pantalla para realizar una animación. Para ello deberemos indicar el momento en el que queremos que se redibujen los gráficos.
Redibujado del área
Para forzar que se redibuje el área de la pantalla deberemos llamar
al método repaint
del canvas. Con eso estamos solicitando
al sistema que se repinte el contenido, pero no lo repinta en el mismo momento
en el que se llama. El sistema introducirá esta solicitud en la cola
de eventos pendientes y cuando tenga tiempo repintará su contenido.
MiCanvas mc = new MiCanvas(); ... mc.repaint();
En MIDP podemos forzar a que se realicen todos los repintados pendientes llamando
al método serviceRepaints
. La llamada a este método
nos bloqueará hasta que se hayan realizado todos los repintados pendientes.
Por esta razón deberemos tener cuidado de no causar un interbloqueo invocando
a este método.
mc.serviceRepaints();
Para repintar el contenido de la pantalla el sistema llamará al método
paint
, en MIDP no existe el método update
de
AWT. Por lo tanto, deberemos definir dentro de paint
qué
se va a dibujar en la pantalla en cada instante, de forma que el contenido de
la pantalla varíe con el tiempo y eso produzca el efecto de la animación.
Podemos optimizar el redibujado repintando únicamente el área
de la pantalla que haya cambiado. Para ello en MIDP tenemos una variante del
método repaint
que nos permitirá hacer esto.
repaint(x, y, ancho, alto);
Utilizando este método, la próxima vez que se redibuje se invocará
paint
pero se proporcionará un objeto de contexto gráfico
con un área de recorte establecida, correspondiente a la zona de la pantalla
que hemos solicitado que se redibuje.
Al dibujar cada frame de la animación deberemos borrar el contenido del frame anterior para evitar que quede el rastro, o al menos borrar la zona de la pantalla donde haya cambios.
Imaginemos que estamos moviendo un rectángulo por pantalla. El rectángulo irá cambiando de posición, y en cada momento lo dibujaremos en la posición en la que se encuentre. Pero si no borramos el contenido de la pantalla en el instante anterior, el rectángulo aparecerá en todos los lugares donde ha estado en instantes anteriores produciendo este efecto indeseable de dejar rastro. Por ello será necesario borrar el contenido anterior de la pantalla.
Sin embargo, el borrar la pantalla y volver a dibujar en cada frame muchas veces puede producir un efecto de parpadeo de los gráficos. Si además en el proceso de dibujado se deben dibujar varios componentes, y vamos dibujando uno detrás de otro directamente en la pantalla, en cada frame veremos como se va construyendo poco a poco la escena, cosa que también es un efecto poco deseable.
Para evitar que esto ocurra y conseguir unas animaciones limpias utilizaremos la técnica del doble buffer.
Técnica del doble buffer
La técnica del doble buffer consiste en dibujar todos los elementos que queremos mostrar en una imagen en memoria, que denominaremos backbuffer, y una vez se ha dibujado todo volcarlo a pantalla como una unidad. De esta forma, mientras se va dibujando la imagen, como no se hace directamente en pantalla no veremos efectos de parpadeo al borrar el contenido anterior, ni veremos como se va creando la imagen, en pantalla se volcará la imagen como una unidad cuando esté completa.
Para utilizar esta técnica lo primero que deberemos hacer es crearnos
el backbuffer. Para implementarlo en Java utilizaremos una imagen (objeto
Image
) con lo que tendremos un raster en memoria sobre
el que dibujar el contenido que queramos mostrar. Deberemos crear una imagen
del mismo tamaño de la pantalla en la que vamos a dibujar.
Crearemos para ello una imagen mutable en blanco, como hemos visto anteriormente, con las dimensiones del canvas donde vayamos a volcarla:
Image backbuffer = Image.createImage(getWidth(), getHeight());
Obtenemos su contexto gráfico para poder dibujar en su raster en memoria:
Graphics offScreen = backbuffer.getGraphics();
Una vez obtenido este contexto gráfico, dibujaremos todo lo que queremos mostrar en él, en lugar de hacerlo en pantalla. Una vez hemos dibujado todo el contenido en este contexto gráfico, deberemos volcar la imagen a pantalla (al contexto gráfico del canvas) para que ésta se haga visible:
g.drawImage(backbuffer, 0, 0, Graphics.TOP|Graphics.LEFT);
La imagen conviene crearla una única vez, ya que la animación puede redibujar frecuentemente, y si cada vez que lo hacemos creamos un nuevo objeto imagen estaremos malgastando memoria inútilmente. Es buena práctica de programación en Java instanciar nuevos objetos las mínimas veces posibles, intentando reutilizar los que ya tenemos.
Podemos ver como quedaría nuestra clase ahora:
public MiCanvas extends Canvas { // Backbuffer Image backbuffer = null; // Ancho y alto del backbuffer int width, height; // Coordenadas del rectangulo dibujado int x, y;
public void paint(Graphics g) { // Solo creamos la imagen la primera vez // o si el componente ha cambiado de tamaño if( backbuffer == null || width != getWidth() || height != getHeight() ) { width = getWidth(); height = getHeight(); backbuffer = Image.createImage(width, height); } Graphics offScreen = backbuffer.getGraphics(); // Vaciamos el área de dibujo offScreen.clearRect(0,0,getWidth(), getHeight()); // Dibujamos el contenido en offScreen offScreen.setColor(0x00FF0000); offScreen.fillRect(x,y,50,50); // Volcamos el back buffer a pantalla g.drawImage(backbuffer,0,0,Graphics.TOP|Graphics.LEFT); } }
En ese ejemplo se dibuja un rectángulo rojo en la posición (x,y) de la pantalla que podrá ser variable, tal como veremos a continuación añadiendo a este ejemplo métodos para realizar la animación.
Algunas implementaciones de MIDP ya realizan internamente el doble buffer,
por lo que en esos casos no será necesario que lo hagamos nosotros. Es
más, convendrá que no lo hagamos para no malgastar innecesariamente
el tiempo. Podemos saber si implementa el doble buffer o no llamando
al método isDoubleBuffered
del Canvas
.
Podemos modificar el ejemplo anterior para en caso de realizar el doble buffer la implementación de MIDP, no hacerla nosotros:
public MiCanvas extends Canvas { ...
public void paint(Graphics gScreen) {
boolean doblebuffer = isDoubleBuffered();
// Solo creamos el backbuffer si no hay doble buffer if( !doblebuffer ) {
if ( backbuffer == null || width != getWidth() || height != getHeight() ) { width = getWidth(); height = getHeight(); backbuffer = Image.createImage(width, height); }
} // g sera la pantalla o nuestro backbuffer segun si
// el doble buffer está ya implementado o no
Graphics g = null;
if(doblebuffer) {
g = gScreen;
} else {
g = backbuffer.getGraphics();
}
// Vaciamos el área de dibujo g.clearRect(0,0,getWidth(), getHeight()); // Dibujamos el contenido en g g.setColor(0x00FF0000); g.fillRect(x,y,50,50); // Volcamos si no hay doble buffer implementado
if(!doblebuffer) {
gScreen.drawImage(backbuffer,0,0,
Graphics.TOP|Graphics.LEFT); } } }
Código para la animación
Si queremos hacer una animación tendremos que ir cambiando ciertas propiedades de los objetos de la imagen (por ejemplo su posición) y solicitar que se redibuje tras cada cambio. Esta tarea deberá realizarla un hilo que se ejecute en segundo plano. El bucle para la animación podría ser el siguiente:
public class MiCanvas extends Canvas { ... public void run() { // El rectangulo comienza en (10,10) x = 10; y = 10; while(x < 100) { x++; repaint(); try { Thread.sleep(100); } catch(InterruptedException e) {} } } }
Con este código de ejemplo veremos una animación en la que el rectángulo que dibujamos partirá de la posición (10,10) y cada 100ms se moverá un pixel hacia la derecha, hasta llegar a la coordenada (100,10).
Si queremos que la animación se ponga en marcha nada más mostrarse
la pantalla del canvas, podremos hacer que este hilo comience a ejecutarse en
el método showNotify
como hemos visto anteriormente.
public class MiCanvas extends Canvas { ... public void showNotify() { Thread t = new Thread(this);
t.start();
} }
Para implementar estas animaciones podemos utilizar un hilo que duerma un determinado período tras cada iteración, como en el ejemplo anterior, o bien utilizar temporizadores que realicen tareas cada cierto periodo de tiempo. Los temporizadores nos pueden facilitar bastante la tarea de realizar animaciones, ya que simplemente deberemos crear una tarea que actualice los objetos de la escena en cada iteración, y será el temporizador el que se encargue de ejecutar cíclicamente dicha tarea.
Hilo de eventos
Hemos visto que existen una serie de métodos que se invocan cuando se produce algún determinado evento, y nosotros podemos redefinir estos métodos para indicar cómo dar respuesta a estos eventos. Estos métodos que definimos para que sean invocados cuando se produce un evento son denominados callbacks. Tenemos los siguientes callbacks:
showNotify
y hideNotify
,
para los eventos de aparición y ocultación del canvas.paint
para el evento de dibujado.commandAction
para el evento de ejecución
de un comando.keyPressed
, keyRepeated
,
keyReleased
, pointerPressed
,
pointerDragged
y pointerReleased
para los eventos de teclado y de puntero, que veremos más adelante.Estos eventos son ejecutados por el sistema de forma secuencial, desde un mismo hilo de eventos. Por lo tanto, estos callbacks deberán devolver el control cuanto antes, de forma que bloqueen al hilo de eventos el mínimo tiempo posible.
Si dentro de uno de estos callbacks tenemos que realizar una tarea que requiera tiempo, deberemos crear un hilo que realice la tarea en segundo plano, para que el hilo de eventos siga ejecutándose mientras tanto.
En algunas ocasiones puede interesarnos ejecutar alguna tarea de forma secuencial
dentro de este hilo de eventos. Por ejemplo esto será útil si
queremos ejecutar el código de nuestra animación sin que interfiera
con el método paint
. Podemos hacer esto con el método
callSerially
del objeto Display
. Deberemos proporcionar
un objeto Runnable
para ejecutar su método run
en serie dentro del hilo de eventos. La tarea que definamos dentro de este run
deberá terminar pronto, al igual que ocurre con el código definido
en los callbacks, para no bloquear el hilo de eventos.
Podemos utilizar callSerially
para ejecutar el código de
la animación de la siguiente forma:
public class MiCanvas extends Canvas implements Runnable { ...
public void anima() { // Inicia la animación
repaint(); mi_display.callSerially(this); }
public void run() { // Actualiza la animación
...
repaint(); mi_display.callSerially(this); } }
La llamada a callSerially
nos devuelve el control inmediatamente,
no espera a que el método run
sea ejecutado.
Optimización de imágenes
Si tenemos varias imágenes correspondientes a varios frames de una animación, podemos optimizar nuestra aplicación guardando todas estas imágenes como una única imagen. Las guardaremos en forma de mosaico dentro de un mismo fichero de tipo imagen, y en cada momento deberemos mostrar por pantalla sólo una de las imágenes dentro de este mosaico.
Figura 17. Imagen con los frames de una animación
De esta forma estamos reduciendo el número de ficheros que incluimos en el JAR de la aplicación, por lo que por una lado reduciremos el espacio de este fichero, y por otro lado tendremos que abrir sólo un fichero, y no varios.
Para mostrar sólo una de las imágenes del mosaico, lo que podemos hacer es establecer un área de recorte del tamaño de un elemento del mosaico en la posición donde queramos dibujar esta imagen. Una vez hecho esto, ajustaremos las coordenadas donde dibujar la imagen de forma que dentro del área de recorte caiga el elemento del mosaico que queremos mostrar en este momento. De esta forma, sólo será dibujado este elemento, ignorándose el resto.
La clase Canvas
nos permite acceder a los eventos de entrada del
usuario a bajo nivel. De esta forma podremos saber cuando el usuario pulsa o
suelta cualquier tecla del dispositivo. Cuando ocurra un evento en el teclado
se invocará uno de los siguientes métodos de la clase Canvas
:
keyPressed(int cod) |
Se ha presionado la tecla con código cod |
keyRepeated(int cod) |
Se mantiene presionada la tecla con código cod |
keyReleased(int cod) |
Se ha soltado la tecla con código cod |
Estos dispositivos, además de generar eventos cuando presionamos o soltamos una tecla, son capaces de generar eventos de repetición. Estos eventos se producirán cada cierto período de tiempo mientras mantengamos pulsada una tecla.
Al realizar aplicaciones para móviles debemos tener en cuenta que en la mayoría de estos dispositivos no se puede presionar más de una tecla al mismo tiempo. Hasta que no hayamos soltado la tecla que estemos pulsando, no se podrán recibir eventos de pulsación de ninguna otra tecla.
Para dar respuesta a estos eventos del teclado deberemos redefinir estos métodos
en nuestra subclase de Canvas
:
public class MiCanvas extends Canvas { ... public void keyPressed(int cod) {
// Se ha presionado la tecla con código cod
}
public void keyRepeated(int cod) {
// Se mantiene pulsada la tecla con código cod
}
public void keyReleased(int cod) {
// Se ha soltado la tecla con código cod
} }
Códigos del teclado
Cada tecla del teclado del dispositivo tiene asociado un código identificativo
que será el parámetro que se le proporcione a estos métodos
al presionarse o soltarse. Tenemos una serie de constantes en la clase Canvas
que representan los códigos de las teclas estándar:
Canvas.KEY_NUM0 |
0 |
Canvas.KEY_NUM1 |
1 |
Canvas.KEY_NUM2 |
2 |
Canvas.KEY_NUM3 |
3 |
Canvas.KEY_NUM4 |
4 |
Canvas.KEY_NUM5 |
5 |
Canvas.KEY_NUM6 |
6 |
Canvas.KEY_NUM7 |
7 |
Canvas.KEY_NUM8 |
8 |
Canvas.KEY_NUM9 |
9 |
Canvas.KEY_POUND |
# |
Canvas.KEY_STAR |
* |
Los teclados, además de estas teclas estándar, normalmente tendrán otras teclas, cada una con su propio código numérico. Es recomendable utilizar únicamente estas teclas definidas como constantes para asegurar la portabilidad de la aplicación, ya que si utilizamos cualquier otro código de tecla no podremos asegurar que esté disponible en todos los modelos de teléfonos.
Los códigos de tecla corresponden al código Unicode del carácter correspondiente a dicha tecla. Si la tecla no corresponde a ningún carácter Unicode entonces su código será negativo. De esta forma podremos obtener fácilmente el carácter correspondiente a cada tecla. Sin embargo, esto no será suficiente para realizar entrada de texto, ya que hay caracteres que corresponden a múltiples pulsaciones de una misma tecla, y a bajo nivel sólo tenemos constancia de que una misma tecla se ha pulsado varias veces, pero no sabemos a qué carácter corresponde ese número de pulsaciones. Si necesitamos que el usuario escriba texto, lo más sencillo será utilizar uno de los componentes de alto nivel.
Podemos obtener el nombre de la tecla correspondiente a un código dado
con el método getKeyName
de la clase Canvas
.
Acciones de juegos
Tenemos también definidas lo que se conoce como acciones de juegos (game actions) con las que representaremos las teclas que se utilizan normalmente para controlar los juegos, a modo de joystick. Las acciones de juegos principales son:
Canvas.LEFT |
Movimiento a la izquierda |
Canvas.RIGHT |
Movimiento a la derecha |
Canvas.UP |
Movimiento hacia arriba |
Canvas.DOWN |
Movimiento hacia abajo |
Canvas.FIRE |
Fuego |
Una misma acción puede estar asociada a varias teclas del teléfono, de forma que el usuario pueda elegir la que le resulte más cómoda. Las teclas asociadas a cada acción de juego serán dependientes de la implementación, cada modelo de teléfono puede asociar a las teclas las acciones de juego que considere más apropiadas según la distribución del teclado, para que el manejo sea cómodo. Por lo tanto, el utilizar estas acciones hará la aplicación más portable, ya que no tendremos que adaptar los controles del juego para cada modelo de móvil.
Para conocer la acción de juego asociada a un código de tecla dado utilizaremos el siguiente método:
int accion = getGameAction(keyCode);
De esta forma podremos realizar de una forma sencilla y portable aplicaciones que deban controlarse utilizando este tipo de acciones.
Podemos hacer la transformación inversa con:
int codigo = getKeyCode(accion);
Hemos de resaltar que una acción de código puede estar asociada a más de una tecla, pero con este método sólo podremos obtener la tecla principal que realiza dicha acción.
Punteros
Algunos dispositivos tienen punteros como dispositivos de entrada. Esto es común en los PDAs, pero no en los teléfonos móviles. Los callbacks que deberemos redefinir para dar respuesta a los eventos del puntero son los siguientes:
pointerPressed(int x, int y) |
Se ha pinchado con el puntero en (x,y) |
pointerDragged(int x, int y) |
Se ha arrastrado el puntero a (x,y) |
pointerReleased(int x, int y) |
Se ha soltado el puntero en (x,y) |
En todos estos métodos se proporcionarán las coordenadas (x,y) donde se ha producido el evento del puntero.
Existen APIs propietarias de diferentes vendedores, que añaden funcionalidades no soportadas por la especificación de MIDP. Los desarrolladores de estas APIs propietarias no deben incluir en ellas nada que pueda hacerse con MIDP. Estas APIs deben ser únicamente para permitir acceder a funcionalidades que MIDP no ofrece.
Es recomendable no utilizar estas APIs propietarias siempre que sea posible, para hacer aplicaciones que se ajusten al estándar de MIDP. Lo que podemos hacer es desarrollar aplicaciones que cumplan con el estándar MIDP, y en el caso que detecten que hay disponible una determinada API propietaria la utilicen para obtener alguna mejora. A continuación veremos como detectar en tiempo de ejecución si tenemos disponible una determinada API.
Vamos a ver la API Nokia UI, disponible en gran parte de los modelos de teléfonos
Nokia, que incorpora nuevas funcionalidades para la programación de la
interfaz de usuario no disponibles en MIDP 1.0. Esta API está contenida
en el paquete com.nokia.mid
.
Gráficos
En cuanto a los gráficos, tenemos disponibles una serie de mejoras respecto a MIDP.
Añade soporte para crear un canvas a pantalla completa. Para
crear este canvas utilizaremos la clase FullCanvas
de
la misma forma que utilizábamos Canvas
.
Define una extensión de la clase Graphics
, en la clase
DirectGraphics
. Para obtener este contexto gráfico extendido
utilizaremos el siguiente método:
DirectGraphics dg = DirectUtils.getDirectGraphics(g);
Siendo g
el objeto de contexto gráfico Graphics
en el que queremos dibujar. Este nuevo contexto gráfico añade:
0xAARRGGBB
.int
, short
o
byte
que codificará el color de dicho pixel.
Sonido
Una limitación de MIDP 1.0 es que no soporta sonido. Por ello para incluir sonido en las aplicaciones de dispositivos que sólo soporten MIDP 1.0 como API estándar deberemos recurrir a APIs propietarias para tener estas funcionalidades. La API Nokia UI nos permitirá solucionar esta carencia.
Nos permitirá reproducir sonidos como tonos o ficheros de onda (WAV). Los tipos de formatos soportados serán dependientes de cada dispositivo.
Control del dispositivo
Además de las características anteriores, esta API nos permitirá
utilizar funciones propias de los dispositivos. En la clase DeviceControl
tendremos métodos para controlar la vibración del móvil
y el parpadeo de las luces de la pantalla.
Detección de la API propietaria
Si utilizamos una API propietaria reduciremos la portabilidad de la aplicación. Por ejemplo, si usamos la API Nokia UI la aplicación sólo funcionará en algunos dispositivos de Nokia. Hay una forma de utilizar estas APIs propietarias sin afectar a la portabilidad de la aplicación. Podemos detectar en tiempo de ejecución si la API propietaria está disponible de la siguiente forma:
boolean hayNokiaUI = false;
try {
Class.forName("com.nokia.mid.sound.Sound");
hayNokiaUI = true;
} catch(ClassNotFoundException e) {}
De esta forma, si la API propietaria está disponible podremos utilizarla para incorporar más funcionalidades a la aplicación. En caso contrario, no deberemos ejecutar nunca ninguna instrucción que acceda a esta API propietaria.