J2ME a la carta — Para MIDP 1.0 y 2.0 Mauricio Monsalve 1 Estructura básica Ésta se entiende con el siguiente ejemplo: import javax.microedition.midlet.*; import javax.microedition.lcdui.*; (1) Imports básicos. Son el MIDlet y la interfaz. public class XXX extends MIDlet implements CommandListener { (2) La clase. Es MIDlet y tiene CommandListener ⇒ Posee void commandAction(...) private Command EC; private Display D; private Form F; Elementos básicos a usar en el MIDlet. public XXX() { (3) D=Display.getDisplay(this); EC=new Command(“Salir”,Command.EXIT,2); F=new Form(“Ejemplo”); StringItem si=new StringItem(“”,“Mini-MIDlet”); F.append(si); F.addCommand(EC); F.setCommandListener(this); } Hice el Display, agrego texto al formulario y hago que esta clase maneje comandos. public void startApp() throws MIDletStateChangeException{ (4) D.setCurrent(F); } El formulario usará el Display D. public void pauseApp() { } public void destroyApp(boolean z) { } (5) Abstractas: startApp, pauseApp, destroyApp... Deben salir siempre. public void commandAction(Command c, Displayable S) (6) { if (c==EC) { destroyApp(false); notifyDestroyed(); } } Aquı́ manejo la salida. Ojo: El código siempre acaba ası́. } El ejemplo anterior tiene como salida un MIDlet que dice “Mini-MIDlet” en el display y que al ejecutar el nico comando aqu, “Salir”, la aplicacin termina su ejecución. Eso básicamente y, en temas de generalidad, 1 es lo único que se puede saber con certeza sobre este MIDlet. La apariencia del MIDlet variará de equipo a equipo pues es decisión del fabricante cómo implementa los requerimientos (o especificaciones) que indica Sun Microsystems sobre la ejecución de J2ME, Java Micro Edition. Cada fabricante decide la apariencia de las interfaces de usuario generales (import javax.microedition.lcdui.*) según más estime conveniente. Lo siguiente es un ejemplo de cómo se ven las interfaces grficas en diferentes terminales (emulados con diferentes emuladores de Java Wireless Toolkit). Pese a las diferencias, queda claro que el MIDlet mostrar el texto “Mini-MIDlet” y el comando “Salir”, ambos de alguna forma (no necesariamente estarán siempre visibles, quizás el comando no se lea explicitamente, puede estar marcado como una flecha que lleve a un menú que tenga los comandos dispuestos por la aplicación). En cuanto a la estructura de la demo: (1) Son los imports básicos. Ambos comienzan con javax.microedition pues son paquetes oficiales Java. Ahora, midlet.* contiene la clase MIDlet, la más importante de todas. El celular tratará de cargar el MIDlet. Más aún, la primera clase de la aplicación, la referenciada por el jad1 , es un MIDlet. El otro contiene las clases con las interfaces gráficas. (2) Un MIDlet precisamente debe heredar la clase MIDlet. Nótese que el nombre del archivo fuente del MIDlet debe ser XXX.java puesto que el MIDlet se llama XXX. ActionListener es la interface que indica que la clase XXX capturará los eventos provocados por la activación de los comandos. Esto es, debe poseer el método void commandAction(Command,Displayable) que aparece más abajo (6). (3) public XXX() es el constructor. La máquina que corra la aplicación invocará este método como primera tarea. Nótese que posee el mismo nombre que la clase. Si la clase se llama Clase, entonces el método será public Clase(). En este caso, public XXX() construye los objetos necesarios a usar más adelante. Eso debiera ser lo tradicional. (4) Una vez construido el MIDlet, la máquina virtual ejecutará el método startApp. Este método debe ser escrito, aunque no contenga nada; es la exigencia del MIDlet. Pero nótese el contenido del método: Indica que el Display (acá ‘D’) debe mostrar el MIDlet XXX (D.setCurrent(this)). Eso debiera ocurrir siempre. (5) startApp, pauseApp y destroyApp deben ser escritos siempre. Han sido definidos como abstractos. (6) void commandAction(Command,Displayable) es el método que debió ser colocado a causa de haber implementado CommandListener en XXX. commandAction es llamado cada vez que una acción es ejecutada. Los parámetros son el comando y el displayable (el objeto que puede ser colocado en la pantalla, como el MIDlet y el Canvas) asociada a la circunstancia de activación del comando. En el ejemplo, aunque no era necesario, se identificó el comando que llamó al MIDlet. El ejemplo muestra la importancia de que los comandos sean variables globales. Como se aprecia, no hay void main(). La clase se corre primero por el constructor (en este caso XXX()) y luego se llama a startApp() del MIDlet. 1 Una aplicación J2ME consiste de dos archivos: un .JAD y un .JAR. 2 2 Interfaz de usuario Viene de lcdui. Es una interfaz de usuario de alto nivel muy simple de implementar. Esto se debe a que una pantalla de dispositivo móvil (sea Palm, teléfono, etc.) es muy pequeña. Ojo: Todo esto es compatible con MIDP 1.0, luego deberı́a funcionar en general. MIDP 2.0 sólo extiende las capacidades de la versión 1.0. Es compatible hacia atrás. 2.1 Comandos Son las opciones que suelen aparecer en la base de la pantalla. Sintaxis del constructor: X=new Command(”TXT”,Command.Y ,int prioridad); Y ∈ {OK,CANCEL,STOP,BACK,HELP,SCREEN,ITEM} ITEM y SCREEN son tipos genéricos. 2.2 Elementos (displayable) Cada uno de estos elementos utiliza la pantalla (el Display) en su totalidad, compartiendo espacio con los comandos. Para pasar de elemento a elemento, y procurar que la aplicación tenga más de una pantalla, se debe pasar cada elemento al display ası́: Display D; ... D.setCurrent(X); Aquı́ X es el elemento que usará la pantalla. Esto se usará cada vez que se necesite pasar de una pantalla a otra. Ello deberı́a ocurrir en el método commandAction() (caso de programa ası́ncrono). En tanto, X puede ser uno de los siguientes objetos: TextBox: Caja de texto (input). Constructor: TextBox(“Nombre”,“Def”,N max,TextField.Y); N max: Número máximo de letras. Y ∈ {ANY,NUMERIC,PHONENUMBER,URL, EMAILADDR,PASSWORD} getString() devuelve el valor ingresado. 3 Alert: Mensaje de alerta. Constructor: Alert(“Nombre”,“Msg”,Image,AlertType.Y); Image es una imagen. Puede ser null. Y ∈ {ALARM,CONFIRMATION,ERROR,INFO, WARNING} List: Selección múltiple. Constructor: List(“Nombre”,List.Y,String[] Elems,Image[] Imgs); Imgs puede ser null. Y ∈ {EXCLUSIVE,MULTIPLE,IMPLICIT2 } Obtención de la información: getSelectedFlags(boolean[]) guarda en el arreglo si un elemento fue elegido o no, en el caso que la lista fuese de tipo MULTIPLE. De lo contrario, getSelectedIndex() devuelve el ı́ndice del elemento (ı́ndice según el arreglo). Form: Contenedor. En el ejemplo se usó Form. Pero es suficientemente complicado para darle una sección. 2.3 Contenedor Form Form permite hacer pantallas a la medida. Métodos: Form(“Tı́tulo”) Constructor. La cadena “Tı́tutlo” deberá aparecer en la parte superior de la pantalla. append(Item) Agrega un ı́tem. En cuanto a los ı́temes, son los siguientes: StringItem: Etiqueta de texto. Constructor: StringItem(“Tı́tulo”,“Txt”); ImageItem: Una imagen. Constructor: ImageItem(“Tı́t”,Img,ImageItem.LAYOUT Y,“Al”); Img es de tipo Image. Al es texto alternativo a la imagen. Y ∈ {DEFAULT,LEFT,RIGHT,CENTER, NEWLINE BEFORE,NEWLINE AFTER} Para combinar LAYOUTs, usar or lógico. 2 Implı́cito porque el foco implica la selección. 4 TextField: Campo de texto. Constructor: TextField(“Tı́tulo”,“Txt”,N max,TextField.Y); Su uso es idéntico al de TextBox. DateField: Campo de fecha. Constructor: DateField(“Tı́tulo”,DateField.Y); Y ∈ {DATE,TIME,DATE TIME} getDate() retorna la información. Gauge: Barra de estado. Constructor: Gauge(“Tı́tulo”,Interactivo?,min,max); Interactivo? es de tipo boolean. Para manejar la información (estado de la barra) se usa setValue() y getValue(), siendo el primero el más importante. ChoiceGroup: Selección múltiple. Constructor: ChoiceGroup(“Tı́tulo”,Choice.Y,String[],Image[]); Y ∈ {EXCLUSIVE,IMPLICIT,MULTIPLE} Se usa de la misma manera (con los mismos métodos) que la clase List. 3 Generando aplicaciones Cuando se crea una aplicación para dispositivo móvil se necesita una serie de rutinas (o prácticas) a seguir. Todo esto se resuelve con el uso de la aplicación Java Wireless Toolkit creada por SunM icrosystems. Es una aplicación muy cómoda y disponible para Windows, Unix/Linux y Solaris. 3.1 Nuevo proyecto La instalación de la aplicación es super sencilla. Luego basta ejecutar KToolbar para iniciar el desarrollo de la aplicación. En N ew P roject se escribe el nombre del proyecto y el nombre de la clase principal (sin extensiones). Pueden ser el mismo. Naturalmente, OpenP roject abre un desarrollo existente, Build compila y Run ejecuta. En Settings se puede elegir, por ejemplo, el alcance de la aplicación: Si es MIDP 1.0, MIDP 2.0, si requiere Bluetooth, MMAPI, Java3D, etc. Ojo: La máxima compatibilidad se alcanza con MIDP 1.0. 5 Al crear un nuevo proyecto se crea una estructura de directorios dentro del directorio de proyectos. Por ejemplo, si creamos el proyecto XXX, aparecen: XXX/ XXX/bin/ XXX/src/ XXX/lib/ XXX/res/ Y pueden aparecer otros sobre la marcha (clases/, tmp...). Lo importante el codigo fuente debe ir siempre en src/ y que los recursos (imágenes y sonidos) deben ir siempre en res/. En el directorio bin/ aparecerá MANIFEST.MF, que es el archivo con la información del proyecto, y el archivo proyecto.jad. El archivo .jad es el descriptor de la aplicación. Si se empaqueta la aplicación (create package), entonces aparecerá un archivo proyecto.jar. Un archivo .jar es un mero archivo .zip. Pero mucho ojo, en el archivo .jad aparece el tamaño del archivo .jar; si no coinciden, habrá un fallo en la validación de la aplicación, por lo que no funcionará en los distintos equipos. Ahora bien, Wireless Toolkit no trae aplicaciones de edición de texto, ası́ que habrá que hacerlo a la antigua en el directorio src/ (como más se quiera, se puede usar write, notepad, eclipse, vim, emacs, gedit3 ). Ahora, KToolbar se puede dejar en segundo plano. Y cuando sea necesario probar, se traerá a primer plano y se ejecutará build y run. La aplicación correrá automágicamente en un emulador. Ojo: se pueden agregar dispositivos a emular. 3.2 Imágenes La imagen deberı́a ser siempre png4 . Es la alternativa abierta a GIF, ya que esta última tuvo muchos problemas de derechos. Se supone que todos los dispositivos móviles5 soportan png. Ojo: todas las imagenes deben estar en el directorio res. Una imagen se carga ası́: Image img; try{ img=Image.createImage("/imagen.png"); } catch(IOException e) { ... // Control de algun error } El slash (/) en el nombre del archivo indica que se está solicitando el archivo en la raı́z del directorio de recursos: res/. 3.3 Audio (MIDP 2.0) MIDP 2.0 Media API permite la reproducción de sonidos en los dispositivos móviles. Como es MIDP 2.0, no sirve para todos. Y si se desea más multimedia, MMAPI (Mobile Media API) es la solución, pero es un paquete opcional. MMAPI ya permite reproducción de MPEG, GIF animados, etc. Sólo algunos equipos lo soportan. Para cargar música midi, por ejemplo, se ejecuta lo siguiente: import java.io.*; import javax.microedition.media.* ... 3 Gnome editor, gedit, es sensacional. Crimson Editor, para Windows, es similar. es abreviatura de Portable Network Graphics. 5 Al menos todos aquellos que soportan imágenes. 4 PNG 6 try { InputStream ins = getClass().getResourceStream("jazz.mid"); Player p = Manager.createPlayer(ins,"audio/midi"); p.setLoopCount(5); p.start(); } catch (Exception e) { ... } En este ejemplo, se carga el archivo MIDI contenido en res/ y se ajusta al objeto de clase Player para que reproduzca 5 veces seguidas esa melodı́a MIDI. Para acabar con la ejecución, se puede invocar el método close() de Player (en este caso, serı́a p.stop() ). Lo anterior también libera los recursos del reproductor. Manager es la clase estática que construye los Player según cada circunstancia. Manager.createPlayer() hace un Player. Hay dos maneras de llamarlo: Manager.createPlayer("URL"); //Invocando un URL Manager.createPlayer(InputStream,"tipo"); //En este caso tipo=audio/midi. //Otro tipo pudo ser audio/x-wav, por ejemplo. Por supuesto que una aplicación compilada para MIDP 2.0 no funcionará en equipos con MIDP 1.0. 4 Gráficos a nivel más bajo No es realmente a bajo nivel, pero al menos un nivel suficientemente bajo como para tener un control decente sobre la gráfica del dispositivo. 4.1 Canvas De tipo Displayable, puede ser usado por Display al igual que Form, TextBox, etc. Ası́ mismo, pertenece a lcdui. Por eso mismo, una objeto Canvas acepta: - La inclusión de comandos, i.e., miCanvas.addCommand(BYE); - Ser puesto como Display, i.e., D.setCurrent(miCanvas); Una opción muy recomendada es extender Canvas cuando sea necesario. Eso da uso completo a éste. Las siguientes son funcionalidades que incluye Canvas: int getGameAction(int keycode) Dado un keycode, entrega si este es un Canvas.LEFT, Canvas.RIGHT, Canvas.UP, Canvas.DOWN, Canvas.FIRE, etc. void keyPressed(int keycode) Hay que implementarlo. Este método es invocado cuando se pulsa una tecla. En su interior es recomendado usar getGameAction para el caso de los juegos. void keyReleased(int keycode) Hay que implementarlo. Tal como el anterior, pero cuando se libera una tecla. int getWidth() Entrega el ancho del Canvas. int getHeight() Entrega el alto del Canvas. int paint(Graphics g) Hay que implementarlo. Este método es invocado cuando hay que dibujar la pantalla. repaint() Solicita dibujar la pantalla. 7 4.2 Graphics Este objeto controla los dibujos hechos en algún objeto que entrega algún objeto dibujable. setColor(R,G,B) Elige un color para dibujar. Cada parámetro está entre 0 y 255. setGrayScale(T) Elige un gris, T ∈ [0,255]. drawLine(x0 , y0 , x1 , y1 ) Dibuja una lı́nea entre (x0 , y0 ) y (x1 , y1 ). drawRect(x0 , y0 , ancho, alto) Dibuja un rectángulo vacı́o. fillRect(x0 , y0 , ancho, alto) Dibuja un rectángulo lleno. drawArc(x, y, r0 , r1 , ω0 , ω1 ) Dibuja un segmento de arco de una elipse centrada en x, y, entre los angulos ω0 y ω1 , cuyas distancias al centro son r0 y r1 respectivamente. Los ángulos son sexagesimales (0 a 360). drawFillArc(x, y, r0 , r1 , ω0 , ω1 ) Idéntico al método anterior, pero rellena desde el centro hasta el arco. drawString(“Texto”,x,y,Graphics.A) Dibuja una cadena de texto en la posición indicada. A ∈ {LEFT, RIGHT, HCENTER} ∪ {TOP, BOTTOM}. Para combinar opciones se usa or. drawImage(Image,x,y,Graphics.A) Dibuja una cadena de texto en la posición indicada. A ∈ {LEFT, RIGHT, HCENTER} ∪ {TOP, BOTTOM, VCENTER}. Para combinar opciones se usa or. Nótese que a diferencia de drawString, drawImage admite ancla en VCENTER (centro vertical). Nota muy importante: Todos los métodos que hay que implementar son protected. Al escribirlos, es posible colocar, o bien protected, o bien public, pero jamás colocarles private. ¡Deben ser vistos por otras clases! 4.3 Image & Graphics Una imagen, o sea, un objeto de tipo Image, puede ser dibujado mediante Graphics. Esto agrega una nueva forma de tener imágenes; haciéndolas en el código en vez de cargarlas de un archivo. Pero ojo, que Java no maneja el tema de transparencias. La única forma de tener transparencias es con imágenes PNG transparentes. El siguiente código es una receta general para crear imágenes mediante primitivas: Image X=Image.createImage(100,60); //100 pixeles de ancho, 60 de alto Graphics Y=X.getGraphics(); //Controlar de la grafica de X ... //hacer lo que se quiera en Y. Los cambios se veran en X. El siguiente ejemplo ilustra un cuadrado verde con degradado: Image X=Image.createImage(60,60); Graphics Y=X.getGraphics(); for(int i=0;i<60;i+=5) { Y.setColor(2*i,4*i,2*i); //Color primariamente verde Y.fillRect(0,i,60,5); //Rectangulo de 60x5 desde 0,i } 8 5 Lógica propuesta para juego J2ME Esta es sólo una idea personal, pero creo que es bastante efectiva a la hora de construir juegos. Consiste en dar todo el poder al Canvas, y hacerlo thread (hilo de ejecución). A continuación se muestra el esquema general. import javax.bla.bla.*; /bis/ public class X extends MIDlet implements CommandListener{ ... Display d; Thread t; Cvas c; ... public X() { d.getDisplay(this); c=new Cvas(); t=new Thread(c); ... } public startApp() thr... { t.start(); //Hilo de ejecucion paralelo d.setCurrent(t); } ... } //Fin del MIDlet public class Cvas extends Canvas implements Runnable{ ... public Cvas() { super(); ... //Lo pertinente } public void paint(Graphics g) { ... //Dibujar la pantalla } public void run() { ... //Codigo SECUENCIAL del juego :) ... sleep(50); //Ejemplo, una pausa, dormir 50 milisegundos ... repaint(); //Ejemplo, actualizar la pantalla ... } ... //Resto de la logica del Canvas } Y, por supuesto, se debe implementar todo lo indicado en la sección Canvas. Con esta estructura, más lo indicado en secciones anteriores, se podrı́a empezar a escribir un juego rápidamente. 9 6 Ejemplo: Diapositiva La siguiente clase, llamada Diapo, extiende a Canvas, y tiene la gracia de ser una visualizadora de diapositivas. La diapositiva será un arreglo de Strings (cadenas, texto) cuyos elementos sean nombres de imágenes. Al pulsar una tecla, la imagen cambiará. Nótese que lo siguiente no es un juego. Lo único que interesa es saber cuándo se presiona una tecla. Luego su estructura es radicalmente simple: La lógica queda en paint y en keyPressed. Teclas: Arriba o Izquierda hacen volver en la diapositiva, Abajo o Derecha hacen avanzar. import javax.microedition.lcdui.*; //Es solo un Canvas, no es un midlet public class Diapo extends Canvas{ //No necesito threads, no implemento Runnable int num_diap; Image[] foto; //El numero de la diapositiva //El arreglo con las fotos private Image Cargar(String nombre_foto) { //Carga una foto Image x; try { x = Image.createImage(nombre_foto); } //Trato de cargar la foto catch(Exception e) { x=null; } //Y si no se puede, anulo return x; } public Diapo(String[] txt) { //Constructor. Recibe un arreglo de Strings. foto = new Image[ txt.length ]; //Arreglo con tantos elementos como txt[] for( int i=0 ; i<txt.length ; i++ ) //Para recorrer todo el arreglo foto[i] = Cargar( txt[i] ); //La imagen[i] es el archivo[i] num_diap=0; //Fijo la primera diapositiva } private void paint(Graphics g) { //A dibujar. ’Graphics g’ es la pantalla. g.setColor( 255 , 255 , 255 ); //Color blanco g.fillRect( 0 , 0 , getWidth() , getHeight() ); //Pinto la pantalla if ( foto[ num_diap ] != null) //Solo dibujare algo que tengo g.drawImage( foto[ num_diap ] , getWidth()/2 , getHeight()/2, Graphics.HCENTER | Graphics.VCENTER ); //Dibujo la foto de forma que el centro de la imagen a pintar //coincida con el centro de la pantalla (imagen centrada) } private void keyPressed( int keycode ) { //Eventos de teclado int accion = getGameAction( keycode ); //A evento de juego if ( ( accion == UP ) || ( accion == LEFT ) ) num_diap--; //Arriba o izquierda => ir para atras if ( ( accion == DOWN ) || ( accion == RIGHT ) ) num_diap++; //Abajo o derecha => ir para delate //Los siguientes son casos de borde (correctivos) if ( num_diap < 0 ) num_diap = 0; if ( num_diap >= foto.length ) num_diap = foto.length - 1; repaint(); //Ahora tengo que dibujar } } 10 7 Ejemplo: MIDlet que utiliza la diapositiva El siguiente MIDlet requiere tener en el directorio res/ las imágenes f oto1.png, f oto2.png y f oto3.png. Se llama ppal, por tanto su archivo será ppal.java. import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class ppal extends MIDlet implements CommandListener{ Command EX; Display d; Diapo DI; public ppal() { d=Display.getDisplay(this); EX=new Command("Salir",Command.EXIT,1); String[] txt={"/foto1.png","/foto2.png","/foto3.png"}; DI=new Diapo(txt); DI.addCommand(EX); DI.setCommandListener(this); } public void startApp() throws MIDletStateChangeException{ d.setCurrent(DI); } public void pauseApp() {} public void destroyApp(boolean z) {} public void commandAction(Command c, Displayable dis) { destroyApp(false); notifyDestroyed(); } } 11