Artigo Java Magazine 46 - Programação Java EE: Parte 3
Conheça a API de interface de usuário da MIDP, e comece a explorar técnicas de programação e características de desempenho de dispositivos limitados, além de ver como utilizar mais uma ferramenta, o NetBeans Mobility Pack.
Nesta terceira parte do mini-curso de programação para Java ME/MIDP1, nosso fogo muda para codificação e APIs. Começamos explorando uma das necessidades fundamentais de qualquer aplicação: a interface com o usuário.
As aplicações Micro Edition também necessitarão de outras coisas como estruturas de dados, I/O, threads e por aí vai. Mas como estamos supondo que o leitor já tem conhecimentos de programação em Java SE, a idéia é focar no que é novo ou diferente no Java ME. Como já vimos nas partes anteriores, diversas APIs do Java ME diferem das tradicionais APIs do Java SE, mas essas diferenças variam:
- Há algumas APIs que foram simplificadas para caber nos dispositivos (ex.: coleções). Isso exige que aprendamos a contornar algumas limitações. Por exemplo, como a java.util do MIDP não tem coleções Set, podemos usar uma Hashtable2 com a chave igual ao valor.
- Outras APIs, totalmente novas, oferecem funcionalidades ”verticais”, específicas à necessidade dos dispositivos. Um bom exemplo é a WMA (Wireless Messaging API), que permite enviar mensagens SMS/MMS.
- Finalmente, há APIs que têm correspondente na Java SE, mas foram totalmente reprojetadas para a plataforma Java ME, seja devido ao tamanho excessivo da API da Java SE, seja por causa da inadequação do seu design às especificidades dos dispositivos ME.
As APIs de interface gráfica (GUI) pertencem à terceira categoria. Iniciantes em Java ME muitas vezes reclamam – precisava mesmo de um API novinha, que dá mais trabalho para aprender e não permite reusar nenhum código do Java SE? Infelizmente, precisava. Pelo critério do tamanho, as APIs de GUI do Java SE são grandes demais: somando AWT, Java 2D, Swing e acessórios, temos um toolkit que é maior do que alguns runtimes Java ME inteiros3!
Saiba mais: Curso Completo de Java
E pelo critério de design, a Swing também é inadequada: por exemplo, ela pressupõe que todos os componentes serão desenhados por código Java, faz double buffering de tudo, possui um sistema de look-and-fells sofisticado, uma arquitetura MVC robusta. Essas e outras características tornam a Swing extremamente flexível e poderosa. Mas também a tornam pesada para dispositivos com CPU fraca, RAM apertada e JVMs menos avançadas.
O projeto MicroMandel
Para exercitar a programação de GUIs com a API LCDUI, o ideal é um projeto combinando dois tipos de código: telas do tipo “formulário” de entrada/saída de dados convencionais, usando componentes tais como checkbox, texto editável, menus etc.; e telas “desenhadas”, aplicando as APIs gráficas de baixo nível: desenho de linhas,textos,polígonos;uso de preenchimentos etc.
Quem lê esta coluna há anos talvez lembre que seu autor gosta de gráficos fractais. Aproveitei esta oportunidade para reciclar o meu modesto gerador de gráficos de Mandelbrot (já apresentado na Edição 19, no artigo “Matemática em Java”.) Vamos então construir uma aplicação com duas telas. A primeira é um formulário de entrada de dados, onde poderemos visualizar e opcionalmente alterar os parâmetros iniciais do gráfico. A segunda tela faz a exibição do gráfico.
Para construir esta aplicação, inicialmente utilizaremos o NetBeans com o Mobility Pack, conjunto que se destaca pelo seu suporte avançado para Java ME. O NetBeans permite criar GUIs MIDP de forma visual, sendo ideal para muitos tipos de aplicações centradas em formulários.
Vamos apresentar as etapas e as telas do NetBeans, mas o artigo não é exclusivo para usuários deste IDE. Mostraremos também o código gerado, que poderá ser usado com qualquer IDE com suporte a Java ME. Portanto, este artigo da série é “otimizado” para o NetBeans mas é “portável” para outros IDEs. (Ao final desta parte encerramos a cobertura de IDEs, que já contemplou o Eclipse na edição anterior. A partir do próximo artigo, falaremos só de APIs e técnicas de programação.)
Instalando o NetBeans e o Mobility Pack
Para este artigo, utilizei o NetBeans 5.5. Você precisará baixar e executar, na seqüência, os instaladores do IDE e o Mobility Pack. (Quando o NetBeans 6.0 estiver pronto, deverá haver um novo instalador unificado em nbi.netbeans.org.)
O CLDC Mobility Pack 5.54 irá instalar uma cópia do Sun Wireless Toolkit (WTK) 2.2 no diretório do seu usuário do sistema operacional. Por exemplo, no Windows, isso fica em %USERPROFILE%\.netbeans\\emulators\wtk22_win\emulator\wtk22, onde USERPROFILE geralmente aponta para C:\Documents and Settings\. Mas vamos ignorar isso e usar o WTK 2.5, que é muito superior. Há duas opções:
- Faça o download e instalação do WTK 2.5 (veja links) e, no NetBeans, vá em Tools>Java Platform Manager>Add Platform; escolha o tipo Java Micro Edition Platform Emulator e selecione o WTK 2.5, que será automaticamente detectado. Pelo mesmo processo, você poderá também instalar versões do WTK customizadas para seu celular, incluídas no SDK para Java ME do fornecedor (Nokia, Motorola, SonyEricson etc.)
- Em Tools > Update Center, selecione o Java ME Platform SDK Catalog, e será fornecida a opção do WTK 2.5. O Update Center fará o download do WTK, lançará seu instalador, e após a instalação, abrirá o Platform Manager já detectando o novo WTK; bastará confirmar.
Uma vez instalado o WTK, no mesmo diálogo Java Platform Manager, pode-se ir na aba Tools and Extensions e acionar as opções:
- Open Preferences: Executa o utilitário prefs do WTK.
- Open Utilities: Executa o utilitário utils do WTK.
- Use WTK Extensions: Memory Monitor, Network Monitor e Profiler: Ativam cada uma destas ferramentas de monitoria e diagnóstico do WTK, quando o emulador for lançado.
Veja a edição anterior para detalhes sobre estes utilitários.
Criando o projeto
Comece com New Project > Mobile > Mobile Application. No diálogo Name and Location, entre com o nome do projeto – “MicroMandel” – e o seu diretório. Em Default Platform Selection, escolha Emulator Platform = SunJava™ Wireless Toolkit 2.5 for CLDC, Device Configuration = CLDC-1.1 (é o default), e Device Profile = MIDP-2.0. Aceite os defaults para todas as demais opções e finalize a criação do projeto.
O assistente de criação de projetos irá nos presentear com um projeto MIDP que já possui uma hello.HelloMidlet completa, com um helloForm que exibe a clássica mensagem “Hello, wolrd!”, e um comando (opção de menu) que permite finalizar a MIDlet. É uma boa ideia começar já executando Run Project para verificar se toda a sua configuração está correta. Vendo que funcionou, podemos apagar o pacote hello e começar a criar a nossa própria MIDlet.
Antes de iniciar o desenvolvimento, vamos examinar a página de propriedades do projeto, onde você verá muitas opções específicas para Java ME. A categoria Platform é a mais importante. Além das opções já informadas no assistente de criação de projetos, podemos ativar individualmente vários packages (APIs) opcionais. Por default, praticamente todas estas APIs vêm ativadas, mas isso não é uma boa ideia. O melhor é deixar ativadas somente aquelas que o projeto realmente precisará. Isso reduz o risco de utilizarmos por acidente alguma API não planejada (ex.: via auto-completamento do editor ou reuso de código).
Lembre que quanto mais APIs opcionais você usar, menor será a porcentagem de dispositivos do mercado capazes de rodar a sua aplicação. Para o nosso projeto, só precisamos das APIs da CLDC e do MIDP, portanto podemos desativar todos os check-boxes Optional Packages, como na Figura 1.
Acione agora New > Visual MIDlet. Em Name & Location, entre com “MicroMandel” como nome da MIDlet e da classe e ”mandel” para o pacote. O NetBeans criará uma classe mandel.MicroMandel que estende a classe MIDlet e define implementações vazias dos métodos starApp(), pauseApp() e destroyApp(). Esses métodos de ciclo de vida permitem executar tarefas de inicialização ou de encerramento. Também existirão métodos initialize(), getDisplay() e exitMidlet(). Você não tem liberdade total de editar esses métodos (especificamente, as linhas em cinza), porque são controlados pelo editor visual. Para quem já usou o editor de GUIs AWT/Swing do NetBeans (Matisse), é a mesma ideia.
Criando o fluxo de navegação
No cabeçalho do editor da classe MicroMandel, você verá três botões de modo de edição:
- Souce permite editar o código-fonte;
- Screen Design edita a tela visualmente (deverá aparecer uma tela vazia);
- Flow Design edita, também visualmente, o fluxo de navegação entre múltiplas telas (deverá aparecer apenas uma figura de um celular, intitulada Mobile Device).
Já sabemos que a aplicação terá duas telas, então vamos criá-las. No modo Flow Design, vá na Pallete e selecione Screens/Form. Clique numa área vazia do editor visual para criar o form, que será nomeado form1. Renomeie-o (janela Properties, Instance name) para formParams. Crie um segundo form chamado formFractal.
No formParams, após usuário definir os parâmetros, teremos um comando (opção ativada por um menu do dispositivo) que fará a transição para o formFractal. Para criar este comando, clique em Pallete > Commands > Ok Command e depois clique sobre a figura do formParams. Vai aparecer um quadradinho com o rótulo “Ok” do lado. Crie também, no formParams, um comando do tipo Exit, e no formFractal, um botão do tipo Back; veja a Figura 2.
Agora você pode fazer as conexões entre eventos e pontos de entrada/saída de telas, que definem o fluxo de navegação. Clique no quadradinho Start Point do Mobile Device, e arraste uma seta até o quadradinho sem identificação à esquerda do formParams. Da mesma forma, ligue formParams / OK ao ponto de entrada de formFractal; formParams / Exit ao Exit Point do Mobile Device; e formFractal / Back também ao ponto de entrada de formParams. Arraste as figuras do Mobile Device e dos forms de maneira a deixar o diagrama mais claro (isto não terá efeito no código).
Ao criarmos comandos, o NetBeans gera variáveis com nomes como okCommand1, o que pode ser confuso se tivermos muitos comandos. Também irá gerar rótulos como “Exit”, apropriados apenas para um GUI em inglês. Clicando em cada comando/seta e usando a janela de propriedades, edite seus Instance Name para paramsOkCommand, paramsExitCommand e fractalBackCommand, e os rótulos (Label) para “Fractal”, “Sair” e “Voltar”. Feito isto, você terá algo como a Figura 3.
O diagrama de fluxo é bastante intuitivo, portanto só vamos resumir seus elementos e conceitos:
- A tela formParams será exibida assim que a aplicação for iniciada, e terá dois comandos. O comando paramOkCommand (“Sair”) encerra a aplicação, e o comando paramExitcommand (“Fractal”) faz a transição para a tela formFractal.
- A tela formFractal, que só é exibida ao acionar formParams / “Fractal”, possui apenas um comando paramFractalBack (“Voltar”), que faz a aplicação retornar à tela formParams.
Para ver esta navegação em ação, você pode executar novamente a MIDlet. Na tela correspondente ao formParams, por exemplo, você deverá ver o comando “Sair” associado ao botão superior esquerdo do celular emulado, e o comando “Fractal” associado ao botão esquerdo direito.
Este tutorial tem uma abordagem prática, sendo orientado pelo editor visual e pelo código. Mas no quadro, “Examinando a LCDUI” você pode ver os conceitos e a estrutura básica desta API de uma forma mais direcionada.
Analisando o código
Antes de continuar o desenvolvimento, vejamos o código gerado pelo NetBeans. Com algumas edições (espaços em branco e alguns dos comentários removidos), este código está na Listagem 1.
Agora vamos “destrinchar” o código para ver o que está acontecendo e como funciona a LCDUI. “Você não achou que este era um artigo sobre como clicar botões Next”, achou?)
package mandel;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class MicroMandel extends MIDlet implements CommandListener {
public MicroMandel() { initialize();
}
private Form formParams;
private Form formFractal;
private Command paramsOkCommand;
private Command paramsExitCommand;
private Command fractalBackCommand;
/**Called by the system to indicate that a command has been invoked on
* a particular displayed.
* @param command the Command that ws invoked
* @param displayble the Displayable on which the command was invoked
*/
public void commandAction(Command command. Displayable displayable) {
// Insert global pre-action code here
if (displayable == formFractal) {
if (command == fractalBackommand) {
// Insert pre-action code here getDisplay().setCurrent(get_formParams());
// Insert post-action code here
}
} else if (displayable == formParams) {
if command == paramsExitCommand) {
// Insert pre-action code here exitMIDlet();
// Insert post-action code here
} else if (command == paramsOkCommand) {
// Insert pre-action code here getDisplay().setCurrent(get_formFractal());
// Insert post-action code here
}
}
// Insert global post-action code here
}
/ ** This method initializes UI of the application. */
private void initialize() {
getDisplay().setCurrent(get_formParams());
}
public Display getDisplay() {
return Display.getDisplay(this);
}
public void exitMIDlet() {
getDisplay().setCurrent(null);
destroyApp(true);
notifyDestroyed();
}
public Form get_formParams() {
if (formParams == null) {
formParams = new Form(null, new Item[0]);
formParams.addCommand(get_paramsOkCommand());
formParams.addCommand(get_paramsExitCommand());
formParams.setCommandListener(this);
}
return formParams;
}
public Form get_formFractal() {
if (formFractal == null) {
formFractal = new Form(null), new Item[0]);
formFractal.addCommand(get_fractalBackCommand());
formFractal.setCommandListener(this);
}
return formFractal;
}
public Command get_paramsOkCommand() {
if (paramsOkCommand == null)
paramsOkCommand = new Command(“Fractal”, Command.Ok, 1);
return paramsOkCommand;
}
public Command get_paramsExitcommand() {
if (paramsExitCommand = null)
paramsExitCommand = new Command(“Sair”, Command.EXIT, 1);
return paramsExitCommand;
}
public Command get_fractalBackCommand() {
if (fractalBackcommand == null)
fractalBackcommand = new Command(“Voltar”), Command.BACK, 1);
return fractalBackCommand;
}
public void startApp() {}
public void pauseApp() {}
public void destroyApp(Boolean unconditional1) {}
}
Já criamos os dois botões do formParams e estamos criando o botão Back no formFractal: basta primeiro clicar em Back Command na Palette, depois clicar no formFractal
Uso de poucas classes
Para quem já programou GUIs com a Swing, AWT,SWT ou quase qualquer outro toolkit de GUI e qualquer linguagem OO, a primeira coisa que causa estranheza é que temos uma única classe! Não existem classes separadas para cada form, nem classes específicas para tratar eventos (como as inner classes usadas para listeners de eventos da AWT/Swing).
Ao escrever a MIDlet à mão, se quiser você pode encapsular cada tela e evento numa classe separada. Mas o estilo de geração de código do editor do Mobility Pack é o estilo recomendado: a maioria dos desenvolvedores de Java ME experientes, se forem escrever esta MIDlet à mão, farão do mesmo jeito. O principal motivo é a redução da extensão do código.
Como não usamos classes internas, o código faz a própria classe de MIDlet implementar a interface ComandListener, que define o método commandAction(). Todos os forms criados usam o código como “formParams.setCommandListener(this);”. Como o commandAction() único tratará eventos de todos os comando de todos os forms, seu código precisa de duas cascatas de ifs aninhados: os ifs mais externos selecionam o form, e os mais internos selecionam o comando do form.
Note que alguns deste ifs podem ser desnecessários. Por exemplo, o formFractal só possui um comando, portanto poderíamos dispensar o “if (command==fractalBackCommand)”. Mas esse é um custo de usar geradores de código automáticos ao invés de escrever tudo à mão. Por outro lado, o código é simples e bem organizado.
Inicialização lazy (tardia): quando usar?
Todos os métodos que inicializam componentes de GUI (forms e comandos) possuem um if que tem o efeito de executar a inicialização somente na primeira chamada. A partir daí, o objeto construído é armazenado num atributo, e retornado imediatamente por chamadas futuras ao método. Esta técnica, usada por default nos componentes criados pelo editor visual, pode reduzir o tempo de inicialização de uma MIDlet ou form. Embora as JVMs para Java ME sejam muito menores que as JVMs SE, os dispositivos onde executam também têm desempenho muito inferior aos nossos PCs. O tempo de inicialização da JVM pode ser perceptível, e será pior se a aplicação gastar tempo inicializando objetos que talvez não sejam imediatamente necessários!
Mas também existe o outro lado da moeda: a inicialização tardia exige mais código, e não oferece vantagem para componentes que são utilizados imediatamente. Para desativar a inicialização tardia, nas propriedades de um componente visual (ex.: form, checkbox ou comando), basta desabilitar Code Properties / Lazy Initialized. Você verá que o código da MIDlet ficará muito mais enxuto. Se desabilitarmos a opção lazy para todos os componentes do formParams, todos os métodos get_xxx() serão eliminados, dando lugar a um método initialize() gigante.
Como a tela formParams (bem como todos os seus componentes) é exibida assim que a aplicação inicia, a inicialização lazy não tem utilidade. Seria útil, por exemplo, para uma tela que exibe alguns de seus componentes opcionalmente, por exemplo conforme o valor dos dados fornecidos por uma tela anterior. Assim, decidi por desabilitar esta opção na implementação do formParams.
Código editável e não editável
Como o código da Listagem 1 foi criado e será mantido pelo editor visual do NetBeans, algumas partes críticas não podem ser alteradas. Os métodos gerados pelo editor visual só podem ser modificados em locais específicos. Destacamos o método commandAction(), onde preservamos os comentários que indicam estes locais. Por exemplo, ao processar o acionamento do fractalBackCommand, o NetBeans gera este código:
//Insert pre-action code here
getDisplay().setCurrent(get_formParams());
//Insert post-action code here
A linha de código central, que faz a transição para o formParams é obrigatória. É o mapeamento da seta que você arrastou no editor de fluxo, conectando o fractalBackCommand ao ponto de entrada do formParams. Este código só pode ser editado indiretamente, pelo editor visual. Mas se você quiser inserir código adicional no processamento deste evento – digamos, para fazer um registro em log – pode fazer isso nos pontos indicados por comentários “// Insert...code here”. Por exemplo:
getDisplay().setCurrent(get_formParams());
System.out.println(“Voltei ao formParams!”);
Note que o System.out.println() acima funciona. Mas isso só no emulador, que exibirá a mensagem no seu console (no NetBeans, na janela Output). Num dispositivo real, tipicamente, não veremos esta mensagem em lugar algum, pois MIDlets não possuem um console. Mas este comportamento não é garantido; num build de produção da sua aplicação, não deixe prinln()’s de depuração espalhados pelo seu programa Java ME.
Preenchendo o form de entrada de dados
O próximo passo é completar nosso formParams, com componentes que permitem visualizar e editar os parâmetros para geração do fractal.
Fractais são figuras geradas pela projeção, para o espaço convencional (euclidiano), de certas funções matemáticas iterativas, entre elas a função de Mandelbrot (Zn+1=zn2+C, com n=1 ?+8). Os parâmetros do programa são os seguintes. As quatro coordenadas determinam o domínio da função (valores mínimo e máximo de C – quatro números, pois C é um número complexo). O limite máximo de interações evita que o cálculo dure até o fim dos tempos (mas quanto mais interações, melhor a qualidade da figura). O último parâmetro é o número de passos usado pelo nosso algoritmo de cálculo multi-passos: recalculamos a mesma figura repetidas vezes de forma incremental (aumentando a resolução a cada passo). Isso produz um tempo total igual ao de um algoritmo mais simples, mas permite ao usuário visualizar a forma aproximada da figura mais cedo, o que é muito bom se o cálculo total for muito demorado.
A criação completa do “miolo” do formParams também pode ser feita de maneira visual, com o modo de edição Screen Design do Mobility Pack. A Figura 4 mostra o nosso objetivo, que você poderá reproduzir com os seguintes passos:
- Selecione o componente StringItem e arraste-o para o form. Edite as propriedades: InstanceName=strCoords,Label=”Coordenadas”,Text=”” e Layout=LAYOUT_CENTER.
- Crie quatro TextField:tfX1 com Label=”x1:”,String=”-1.5”;tfy1 com Label=”y1:” e String=”-1.5”;tfX2 com Label=”x2:” e String=”1.5”; e tfY2 com Label =”x2:” e String =”1.5”. Todos devem ter Maximum Size=10, Constraints=DECIMAL e Layout=LAYOUT_LEFT.
- Crie um StringItem strPerf similar ao primeiro, mas com Label=”Desempenho”.
- Crie um ChoiceGroup chgMaxIter, com Label=”Interações Máximas” e Type=EXCLUSIVE.
- Arraste para “cima” deste ChoiceGroup um Choice Element, definindo para este, String=”50”. Crie mais dois elementos: “100” com Selected ativado e “200”.
- Crie outro ChoiceGroup chgSteps, com Label=”Passos” e Type=EXCLUSIVE.
- Crie neste grupo dos ChoiceElements”1”, “2” e “4” , sendo o último Selected.
Em todos estes componentes, desabilite também a opção Lazy Initialized. É só isso, não precisamos escrever nenhum código. Observe que nos beneficiamos da propriedade Constraint, que permite determinar que os nossos TextFields só devem conter números decimais (reais). É um recurso de validação simples, mas bem melhor do que nada, para algo que “cabe” no Java ME.
A Listagem 2 apresenta as novidades no código (novamente, editado para poupar espaço). Este código também não apresenta surpresas. É só o trabalho de criar os objetos que representam cada componente, passando através de construtores ou setters os valores desejados para as propriedades que queremos customizar com valores não-default.
O detalhe menos intuitivo é que não há APIs separadas para “radio buttons” e checkboxes. Um grupo de radio buttons é um ChoiceGroup que permite uma única seleção (Type= Choice.EXCLUSIVE). Já um grupo de checkboxes é a mesma classe configurada para permitir seleção múltipla (Choice.MULTIPLE). Outra opção, Choice.POPUP, é similar aEXCLUSIVE, mas só exibe a opção selecionada; as demais só são exibidas (tipicamente por um menu pop-up) quando o foco estiver neste componente.
Podemos ver, também, que as APIs LCDUI são bastante econômicas: só oferecem propriedades e método estritamente necessário. Por exemplo, um Choice Element é um simples conjunto de três objetos: uma String, uma Image (que não usamos no exemplo) e um boolean (indicando o estado da seleção). Não é possível atribuir, para cada elemento, um “valor de retorno” diferente do texto exibido para o usuário; a mesma String é usada para ambas as finalidades. Nem sonhe, então, com perfumarias como labels em texto formatado, eventos de drag-and-drop ou programação MVC.
Eu falei MVC? Isso é tudo de luxo de APIs para Java SE – ChoiceGroup não define nem sequer uma classe para encapsular a trinca String-Image-boolean de cada item...
public class MicroMandel extends MIDlet implements CommandListener {
…
private StringItem strCoords, strPerf;
private TextField tfX1, tfY1, tfX2, tfY2;
private Choicegroup chgMaxIter, chgSteps;
…
pivate void initialize() {
chgSteps = new ChoiceGroup(“Passos”, Choice.EXCLUSIVE,
new String[] {“1”, “2”, “4”}, new Image[] {null, null, null});
chgSteps.setSelectedFlags(new Boolean[] {false, false, true});
tfY2 = new TextField(“y2: “, “1.5”, 10, TextField.DECIMAL);
tfY2.setLayout(Item.LAYOUT_LEFT);
paramsExitCommand = new Command(“Sair”, Command.EXIT, 1);
paramsOkCommand = new Command(“Fractal”, Command.Ok, 1);
tfX1 = new TextField(“x1: “, “-1.5”, 10, TextField.DECIMAL);
tfX1.setLayout(Item.LAYOUT.LEFT);
tfX1.setInitialInputMode(“null”);
strCoords = new StringItem(“Coordenadas”, null);
strCoords.setLayout(Item.LAYOUT_CENTER);
tfX2 = new TextField(“x2: “, “1.5”, 10, TextField.DECIMAL);
tfX2.setLayout(Item.LAYOUT_LEFT);
chgMaxIter = new ChoiceGroup (“Itera\u00E7\u00F5es m\u00E1ximas”,
Choice.EXCLUSIVE, new String[] {“50”, “100”, “200”},
new Image[] {null, null, null});
chgMaxIter.setSelectedFlags(new Boolean[] {false, true, false});
tfY1 = new TextField(“y1: “, “-1.5”, 10, TextField.DECIMAL);
tfY1.setLayout(Item.LAYOUT.LEFT);
strPerf = new StringItem(“Desempenho”, null);
strPerf.setLayout(Item.LAYOUT_CENTER);
formParams = new Form(“Par\u00E2metros do Fractal”, new Item[] {
strCoords, tfX1, tfY1, tfX2, tfY2, strPerf, chgMaxIter, chgSteps});
formParams.addCommand(paramsOkCommand);
formParams.addCommand(paramsExitCommand);
formParams.setCommandListener(this);
getDisplay().setCurrent(formParams);
}
…
}
Desenhando o fractal
Na última etapa, vamos finalmente deixar de lado os editores visuais e escrever um código por conta própria! A tela formFractal não será, na realidade, um form (instância da classe Form, ou de uma subclasse desta). Ao invés disso, precisaremos de uma subclasse de Canvas, que, como vimos no início, representa uma tela sobre a qual temos controle total de desenho.
Nem todas as aplicações podem se dar ao luxo de usar somente form. A oferta de componentes da LCDUI é pequena – por exemplo, cadê os equivalentes das nossas fiéis javax.swing.JTable ou javax.swing.JTabbedPane? Não há. E mesmo nos componentes existentes, as funcionalidades disponíveis também são limitadas. Você encontrará bibliotecas de terceiros para suprir muitas destas necessidades (não deixe de conferir o projeto open source J2ME Polish:www.j2mepolish.org). Ainda assim, é muito comum, em Java ME, a necessidade de escapar ao conforto dos editores visuais e botar a “mão na massa”, desenhando telas pixel por pixel. Usar APIs como Canvas e Graphics não é uma necessidade exclusiva de programadores de jogos ou de media players. Portanto, vamos demonstrar também um pouco das APIs do MIDP para criação de gráficos 2D.
Infelizmente, o editor visual do NetBeans Mobility Pack não nos dá nenhum apoio para criar uma MIDlet com um Canvas. Só criei o formFractal para exercitar o editor e prototipar a aplicação completa, mas agora teremos que refazer esta parte da GUI à mão.
Comece criando uma nova classe Fractal, que estende Canvas e implementa o método abstrato paint(). Por enquanto deixe este método vazio. Crie quatro atributos privados double com nomes x1, y1, x2 e y2, e dois atributos int com nomes maxIter e steps. Finalmente, crie um construtor que inicializa estes atributos, recebendo seis parâmetros de mesmos nome. Com isso, já temos o “esqueleto” da classe que irá gerar e exibir o fractal, sendo inicializada com os dados entrados no formParams.
De volta à classe MicroMandel, faça as alterações da Listagem 3 para exibir o novo componente. Note que quando apagamos o formFractal pelo editor visual, o NetBeans removeu o método commandAction() o código que fazia a transição de paramsOkCommand para o formFractal. Mas ele deixou o esqueleto do tratamento do evento para aquele comando: ficou apenas o if do comando com um comentário “//Do nothing”. Este comentário ocupa a posição do código que seria gerado pelo editor visual, portanto não pode ser editado. Mas podemos inserir, logo antes ou depois deste comentário, o código novo que faz a transição que desejamos para nosso componente Fractal.
E este código é simples. Primeiro construímos um objeto Fractal, fornecendo como parâmetros os valores numéricos obtidos dos componentes do formParams. Segundo, getDisplay().setCurrent(...) define o novo objeto como componente ativo do display da aplicação, ou seja, o que está sendo exibido. O método setCurrent() exige um objeto Displayable, a raiz dos objetos visuais da MIDP. O método getDisplay() (gerado pelo NetBeans) é só um “atalho” para Display.getDisplay(this):Display representa a tela, e o método estático getDisplay(MIDlet) retorna o Display associado à MIDlet.
Um dispositivo MIDP 1.0 ou 2.x pode ter vários displays, mas cada MIDlet só pode executar em um deles. No MIDP3.0 (ainda em definição), uma MIDlet poderá usar vários displays, algo útil em celulares que possuem um display secundário no flip, por exemplo.
Finalmente, o novo método formFractal() será invocado quando, na tela Fractal, o usuário acionar a tecla que solicita o retorno ao formParams. Recebemos de volta os parâmetros do fractal que podem ter sido alterados visualmente naquela tela (que suporta operações como zoom e deslocamento, as quais alteram as coordenadas), e modificamos os valores dos componentes do form de acordo.
Para motivar, começaremos já apresentando a Figura 5, que mostra o fractal gerado no display do dispositivo. Agora vamos examinar os detalhes interessantes da Listagem 4, que implementa a geração deste gráfico. O grosso desta listagem é o código de cálculo do fractal, que é um tanto complexo, em especial devido a sutilezas como o uso de um thread de cálculo (na LCDUI, assim como a AWT e Swing, é um pecado mortal executar operações longas no thread de eventos!). mas o código que nos interessa especialmente –usos de classes e métodos da LCDUI – está destacado em negrito.
Lendo resources... com limitações de API
O gerador de fractais utiliza um arquivo DEFAULT.MAP que faz o mapeamento de cores do fractal: os valores n (número de iterações) gerados pela fórmula de Mandelbrot são mapeados para cores pelo código COLORS[n & 0xFF], portanto precisamos de uma paleta de 256 cores. Como é trabalhoso criar boas paletas de cores, eu aproveito as paletas que fazem parte do FractInt, um gerador de fractais de domínio público. Achei mais fácil fazer um parser para o formato MAP do FractInt, do que converter à mão estes arquivos para um “new int[]{...}” com 256 valores.
Para isso, o inicializador static{} da classe Fractal utiliza a API Class.getResourceAsStream(), que é suportada pela MIDP. O arquivo MAP será incluído no JAR como resource, e pode ser lido facilmente. A maior dificuldade é que na MIDP não temos a classe java.util.StringTokenizer que eu normalmente usaria para fazer o parsing do arquivo (cujo formato é “RGB\nRGB\nRGB...”, sendo R, G e B números inteiros em decimal). Tive que escrever um código mais complexo, que examina o stream um caractere de cada vez, mantendo variáveis que armazenam as últimas três strings separadas por espaço, e convertendo-as numa cor a cada fim-de-linha.
Seria possível evitar este código de parsing complicado no programa? Fácil: bastaria criar um programa Java SE que executasse o parsing fora do dispositivo, convertendo os arquivos MAP num formato binário eficiente e fácil de carregar. Esse arquivo teria 768 bytes (256*R+G+B), e poderíamos incluí-lo no programa como resource e carregá-lo com um código bem mais simples e eficiente. Reduziríamos o tamanho do JAR (tanto com o código menor quanto com os arquivos de paleta: o que usei tem 2.755 bytes antes da compressão, ou seja, é 3,5 vezes maior que no formato binário). Também melhoraríamos o tempo de inicialização da aplicação.
Este é um bom exemplo de truque de “pré-processamento” que podemos usar para reduzir o tamanho e a complexidade, e aumentar o desempenho, de programas Java ME. (Esta melhoria fica como exercício para o leitor!)
Migrando da AWT/Java 2D para a LCDUI
O funcionamento básico da classe Fractal é similar ao de qualquer componente AWT/Swing que desejássemos desenhar à mão. Redefinimos um método paint(Graphics), que é chamado pelo toolkit sempre que a imagem do componente tiver que ser exibida, seja devia à criação do componente, ou devido à sua exposição (ex.: o componente foi encoberto por outra janela, talvez um pop-up do celular dizendo que você recebeu uma mensagem, e depois esta janela foi removida).
As APIs essenciais utilizadas por este programa (feito originalmente com a AWT são parecidas. A classe Graphics permite desenhar em algum lugar – seja diretamente no display, seja numa imagem offscreen. A classe Image permite criar um buffer para desenho offscreen e também guardar a imagem completa, permitindo redesenho instantâneo após exposições, sem “piscadas” da tela. Mas estas classes não são as mesmas, são só parecidas. Os pacotes são diferentes: ao invés de java.awt.Graphics, temos javax.microedition.Icdui.Graphics, e assim por diante.
O fato de o nome qualificado (incluindo o pacote) ser diferente já é um indício que nenhuma compatibilidade é prometida. Nem mesmo aquela “compatibilidade de subconjunto” que explicamos no primeiro artigo da série: ter menos métodos que na Java SE, mas os métodos que existirem serem iguais. Todavia, os projetistas das APIs para Java ME procuraram manter uma semelhança onde possível, permitindo reaproveitar uma quantidade razoável de código-fonte e de experiência, ainda que exigindo algum “reparo” do código.
Por exemplo, para copiar pixels de uma Image para a saída gráfica (tela ou imagem) representada por um Graphics usamos o método Graphics.drawImage(Image img, int x, int y, int anchor). Este método é muito parecido com o usado na versão Java SE: Graphics.drawImage(Image img, int x, int y, ImageObserver observer). A diferença é que na classe da LCDUI, o ImageObserver não existe (a LCDUI não suporta a funcionalidade de observadores de imagens). Por outro lado, há um novo parâmetro anchor. A falta do ImageObserver não foi um problema porque eu não usava a funcionalidade relacionada a este parâmetro, e passava null como argumento.
Já o anchor é um novo conceito da LCDUI que vale a pena examinarmos antes de escrever qualquer código gráfico (pois muitos métodos exigem este parâmetro). Essa âncora é utilizada para determinar o posicionamento final da operação gráfica, podendo ser um OR entre LEFT, HCENTER ou RIGHT (âncora horizontal) e TOP, BASELINE ou BOTTOM. O valor 0 equivale a LETF|TOP, portanto drawImage(img,x,y,0) desenha a imagem de forma que seu canto superior esquerdo fique na posição (x,y). mas se tivéssemos passado HCENTER|BOTTOM, a imagem seria desenhada de tal forma que a coordenada (x,y) passada correspondesse ao seu ponto central no sentido horizontal, e no extremo inferior no sentido vertical.
O conceito de âncora é ainda mais útil para operações drawString(), pois ao contrário do que acontece com imagens que têm uma forma retangular explícita, ao se trabalhar com textos costuma ser mais difícil computar as dimensões para fazer cálculos como centralização. Note que nas operações com textos, a flag BASELINE não significa o centro vertical e sim a linha de base (baseline) da fonte de caracteres utilizada. A linha de base da letra ‘q’, por exemplo, é a sua posição inferior após ignorarmos a sua “perninha” inferior (chamada “descendente” pelos tipógrafos).
Outra diferença importante entre a AWT e a LCDUI é que, nesta última não existem classes como Paint ou Color. Para representar uma cor, a LCDUI utiliza simplesmente valores do tipo int, no formato 0x00RRGGBB. Alocar um objeto no heap para algo tão simples quanto uma cor é um abuso de desempenho que programadores de dispositivos limitados sequer cogitam!
Tratando eventos de teclado
Mostramos também como tratar diretamente eventos de teclado de baixo nível. Ao invés dos Command dos forms, podemos redefinir os seguintes métodos de Canvas:keyPressed(), keyReleased() e keyRepeated() (usamos somente o primeiro). Todos recebem um int que é o código da tela. Este código dá acesso a todas as teclas disponíveis em qualquer dispositivo, sendo que as seguintes teclas possuem valores representados por constantes de Canvas: KEY_NUM0...NUM9,KEY_STAR (’*’) e KEY_POUND (’#’). Em geral, o código da tecla é o código ASCII do caractere correspondente, ex.: KEY_NUM5=’5’, e se o seu dispositivo tiver um teclado alfabético, receberá ’Q’ para a tecla Q e assim por diante. Mas as únicas teclas que você pode “confiar” que existam em todos os dispositivos MIDP são as doze do keypad numérico.
E quanto àquelas teclas especiais que mesmo os celulares com keypad numérico possuem – setas direcionais, botões de menu e outras? Estas teclas não são padronizadas, mas são suportadas: os métodos de evento serão invocados com o código numérico da tecla. Tais códigos não são portáveis, mas a MIDP apresenta uma solução para os casos mais comuns – as teclas direcionais e de controle de jogos.
O método getGameAction(tecla) traduz um código de tecla para um dos valores UP, DOWN, LEFT ou RIGHT (direcionais), ou FIRE, GAME_A, GAME_B,GAME_C e GAME_D (teclas de disparo e controle). Esta tradução é necessária porque nem todos os dispositivos possuem teclas separadas para estas funções. Num dispositivo sem teclas direcionais, pode ser que as teclas 8/2/4/6 sejam usadas com esse objetivo, portanto teremos getgameAction(KEY_NUM8)==UP e assim por diante. Neste caso, será impossível distinguir um evento “dígito 8” de um evento “seta para cima”, por isso você deve tomar cuidado ao escrever código que trate de forma diferente ambos os eventos. Esse código só funcionará corretamente em dispositivos que possuam teclas independentes.
Há também os métodos (que não utilizamos) pointerPressed(), pointerDragged() e pointerReleased() para eventos de mouse, ou controle equivalente.
public void commandAction(Command command, Displayable displayable) {
if (displayable == formParams){
if (command == paramsExitCommand) {
exitMIDlet();
} else if (command == paramsOkCommand) {
// Do nothing
getDisplay().setCurrent(new Fractal(
Double.parseDouble(tfX1.getString()),
Double.parseDouble(tfY1.getString()),
Double.parseDouble(tfX2.getString()),
Double.parseDouble(tfY2.getString()),
Integer.parseInt(chgMaxIter.getString(
chgMaxIter.getSelectedIndex())),
Integer.parseInt(chgSteps.getString(
chgSteps.getSelectedIndex()))));
}
}
}
public static void fromFractal (double x1, double y1,
double x2, double y2)
{
instance.tfX1.setString(Double.toString(x1));
instance.tfX2.setString(Double.toString(y1));
instance.tfY1.setString(Double.toString(x2));
instance.tfY2.setString(Double.toString(y2));
Display.getDisplay(instance).setCurrent(instance.formParams);
}
packcage mandel;
import java.io.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;
public class Fractal extends Canvas implements Runnable {
private double x1, y1, x2, y2; // Coordenadas do espaço de Mandelbrot.
private int maxIter, maxSteps;
private long time;
private double da,db; // Dimensões do espaço de Mandelbrot.
private Image fractal;
private static final int[] COLORS = new int [256]
private static int COLOR_BLACK = 0x000000;
private static final int REFRESH_LINES = 16;
private int winWidth, winHeight; //Dimensões da tela em pixels
private volatile Thread calcThread;
private String status = “”;
static {
InputStream is = null;
try {
is = Fractal.class.getResourceAsStream(“DEFAULT.MAP”);
int iColor = 0, c;
StringBuffer[] sb = { // Para R, G e B
new StringBuffer(), new StringBuffer(), new StringBuffer()};
int iSB = 0; // Lendo 0 = R, 1 = G ou 2 = B
while ((c == is.read()) != -1) {
if (Character.isDigt((char)c)) sb[iSB].append(c);
else if (c == ‘ ’ ) ++iSB;
else if (c == ‘\n’) {
COLORS[iColor++]=
(Integer.parseInt(sb[0].toString()) << 16) |
(Integer.parseInt(sb[1].toString()) << 8) |
(Integer.parseInt(sb[2].toString());
// Limpa tudo , para próxima trinca RGB
for (int s = 0; s < sb.length; ++s) sb[s].setLength(0);
iSB = 0;
}
}
}
catch (IOException e) { System.out.println(e); }
finally { if (is != null) try { is.close(); }
catch (IOEXception e) {} }
}
public Fractal (double x1. double y1, double x2, double y2,
int maxIter, int maxSteps) {
setFullScreenMode(true);
this.x1 = x1; ; this.y1 = y1; this.x2 = x2; this.y2 = y2;
this.maxIter = maxIter;
this.maxSteps = maxSteps;
this.algorithm = algorithm;
initParams();
}
public void initParams () {
winWidth = getWidth(); winHeight = (getHeigth() + 1) & ~0x0F;
da = (x2 – x1) / winWidth; db = (y2 – y1) / winHeight;
}
public void run () {
calcThrerad = Thread.currentThread();
String st = “ ”;
try {
// Faz o cálculo e exibição, em vários passos.
for (int step = maxSteps; step != 0; step >>= 1) {
calcStep(step);
long total = System.currentTimeMillis() – time;
st = Integer.toString(winWidth) + ‘x’ + winHeight +
“ – ” + (total / 1000.0) + “s”;
System.outprintln(“Step + “;” + status);
}
repaint(); // Garante paint da área espelhada, se houver
}
finally { calcThread = null;}
status = st;
}
public void calcStep (int step) {
int stepMask = (step == maxSteps) ? -1 : step + step – 1;
Graphics gf = fractal.getGrafphics();
int yMax;
int hAxisY = - (int) (y1 / db);
if (y1 < 0 %% y2> 0) {
yMax = hAxisY; // Só calcula até o eixo da simetria
// correção de erro
double error = y1 + db * hAxisy;
y1 -= error; y2-= error;
}
else yMax = winHeight – 1; // sem Simetria; calcula tudo
boolean mirrorJump = false;
// Agora sim, vamos calcular o fractal…
for (int yPos = 0; yPos< winHeight; yPos += step) {
boolean forceY = (yPos & stepMask) != 0;
for (int xPos = 0; xPos< winWidth; xPos += step) {
// A partir do segundo passo, nao recalculamos o quadrante
// noroeste de cada bloco, pois o resultado seria igual
// ao do passo anterior!
if (forceY || ((xPos & stepMask) != 0)) {
int n = calc(xPos, yPos);
// Atualiza pixels só na Image, que serve como buffer.
gf.setColor(COLORS[n & 0xFF]);
gf.fillRect(xPos, yPos, step, step);
}
}
// Faz o “rebatimento” da parte espelhada da imagem, se houver.
boolen mirror = hAxisY + hAxisY – yPos < winHeight;
if (mirror)
gf.copyArea(0, yPos, winWidth, step, 0, 2 * hAxisY -
yPos – step, 0);
// A cada REFRRESH_LINES linhas, atualiza o display
if (((yPos & REFRESH_LINES – 1)) == 0) && (yPos != 0)) {
repaint(0, yPos – REFRESH_LINES, winWidth, REFRESH_LINES;
if (mirror)
repaint(0, 2 * hAxisY – yPos – step, winWidth, REFRESH_LINES);
}
// Completo até eixo de Simetria? Pula linhas espelhadas
if (!mirrorJump && yPos >= yMax) {
mirrorJump = true;
yPos += yPos;
}
}
}
private void initCalc () {
// Inicializa buffer e variáveis para cálculo do fractal.
time = System.currentTimeMillis();
fractal = Image.createImage(winWidth, winHeight);
Graphics gf = fractal.getGraphics();
gf.setColor(COLOR_BLACK);
gf.fillRect(0, 0, winWidth, winHeight);
new Thread(this).start();
}
public void paint (Graphics g) {
if (fractal == null) {
g.setColor(COLOR_BLACK);
g;drawRect(0, 0, getWidth(), getHeigth());
initCalc();
}
else {
int x = (getWidth() – fractal.getWidth())/2;
int y = (getHeight() – fractal.getHeight())/2;
g.drawImage(fractal), x, y, 0);
g.drawString(status), x, y, 0);
}
}
private void zoomOut () {
double dx = (x2 – x1) / 2, dy = (y2 – y1) / 2;
x1 -= dx; x2 += dx; y1 -= dy; y2 += dy;
}
private void zoomIn ( ) {
double dx = (x2 – x1) / 4, double dy = (y2 – y1) / 4;
x1 += dx; x2 -= dx; y1 += dy; y2 -= dy;
}
private void pan (int x, int y) {
double dx = x * (x2 – x1) / 2, dy = y * (y2 – y1) / 2;
x1 += dx; x2 += dx; y1 += dy; y2 += dy;
}
private void stopcalc () {
Thread ct = calcThread;
if (ct != null) {
ct.interrupt ();
while (calcThread != null) // Espera se ainda não interrompeu.
try { Thread.sleep(100); } cath (InterruptedException e) {}
}
fractal = null;
status = “”;
}
/** Eventos de teclado. */
protected void keyPressed (int key) {
stopCalc();
switch (getGameAction(key)) {
// Teclas direcionais movem as coordenadas (“pan” do gráfico).
case UP; pan (0, -1); break;
case DOWN; pan (0, +1); break;
case LEFT; pan (-1, 0); break;
case RIGHT; pan (+1, 0); break;
default: // Não é uma game action
switch (key) {
case KEY_NUM1: zoomIn(); break;
case KEY_NUM7: zoomOut(); break;
case KEY_POUND: // Volta à formParams, com coordenadas atualizadas.
MicroMandel.fromFractal(x1, y1, x2, y2);
return; // Evita o repaint.
}
}
initParams
repaint();
}
/** O algoritmo de Mandelbrot. */
private int calc (int xPos, int yPos) {
double a = x1 + xPos * da, b = y1 + yPos * db;
double x = 0.0d, y = 0.0d;
for (int n = maxIter; n >= 0; --n) {
double x2 = x * x, y2 = y * y;
if (x2 + y2>= 4.0d) return n;
double xx = x2 – y2 + a;
y = (x + x) * y + b;
x = xx;
}
return 0;
}
}
Conclusões
Neste artigo, construímos uma MIDlet que ilustra a utilização da LCDUI, a API de GUI do MIDP. Como os dispositivos MIDP não oferecem APIs alternativas (nem mesmo em modo texto), esta é uma das primeiras APIs específicas ao MIDP que qualquer desenvolvedor precisa conhecer.
Vimos que existem IDEs que automatizam praticamente todo o trabalho de layout e navegação de GUI, especialmente para “aplicações baseadas em formulários”. Aproveitamos também para começar a investigar a fundo as diferenças entre as APIs e as técnicas de programação para Java SE (ou qualquer linguagem /SDK de desenvolvimento para plataformas “normais”, não limitadas) e para Java ME. É preciso conhecer as novas ferramentas e APIs, mas isso não é suficiente para programar bem para Java ME. A parte mais difícil é compreender o “espírito” de uma plataforma mais limitada, que às vezes impões simplificações severas às APIs e nos deixa em apuros – por exemplo, “como é que eu me viro sem a classe StringTokenizer?”. A solução ideal nem sempre é a mais óbvia (como escrever código equivalente à funcionalidade que falta – o que fizemos aqui). Às vezes precisaremos de alternativas como a que sugerimos para este problema (usar um formato de dados mais eficiente).
- Utilizaremos “MIDP” como abreviação de “CLDC 1.1/MIDP 2.x”, o par configuração/perfil enfocado pela série.
- O MIDP não suporta as novas collections do Java 2, como Map.
- No Sun JDK 6, contabizei cerca de 15 Mb (descompactados) somando classes Java e DLLs nativas envolvidas com GUI.
- Tambémhá um “CDC Mobility Pack”; não pegue este, que dá suporte à configuração CDC; vamos usar a CLDC.
Examinando a LCDUI
Não temos espaço para discutir todas as classes, interfaces e funcionalidades da LCDUI, mas vamos agora conhecer a sua arquitetura geral. A Figura Q1 mostra um modelo UML razoavelmente completo da LCDUI (javax.microedition.lcdui), e também do pacote lcdui.game, a extensão da API para jogos e animações 2D.
Apresentaremos a seguir alguns conceitos específicos da LCDUI, que a diferenciam de outras APIs de GUI que você pode conhecer, como a Swing.
Screen versus Canvas
Há dois tipos de componentes. Os Screen são como os componentes da AWT, sendo associados a componentes do toolkit nativo, o que proporciona desempenho e look-and-feel nativos, mas reduz o grau de controle do programa (em especial, não é possível desenhar o componente do jeito que se quiser).
Já Canvas nos permite desenhar a tela como quisermos, e também ter controle preciso sobre eventos de entada. Mas não podemos misturar as duas coisas. O display inteiro deve ser ocupado ou por algum tipo de Screen (ex.: um Form), ou por algum Canvas. Não pode usar nenhum daqueles componentes nativos (embora possa criar os seus próprios componentes, assumindo todo o trabalho de pintura e de tratamento de eventos).
Resumindo, a LCDUI é otimizada para os dois casos extremos mais comuns. De um lado, telas tipo formulário com componentes de exibição de strings e imagens, edição de textos e datas, checkboxes etc. No outro lado, telas totalmente desenhadas pelo programa.
Comandos
Ao executar nosso projeto MicroMandel num dispositivo MIDP real, é possível que a apresentação a o acionamento dos comandos seja diferente do que aparece no emulador. Por exemplo, pode ser que outras teclas (ou controles diferentes, talvez até de tela sensível ao toque) sejam associados a estes comandos.
A LCDUI utiliza o conceito de “comando” (Command) para mascarar as diferenças entre os mecanismos de controle e entrada de dados que podem ser radicalmente diferentes entre dispositivos. O comando Ok, por exemplo, é definido por Command Ok, e representa uma ação de confirmação ou uma “ação positiva” em geral. No seu celular, sempre que você precisa confirmar uma ação (ex.: enviar uma mensagem SMS), o aparelho apresenta a opção “Sim”, “Yes”, “Ok”, “Enviar” ou equivalente numa posição padronizada da tela, com acionamento por uma tecla também padronizada. É esta mesma posição e tecla que serão associadas – neste dispositivo específico – ao Command.Ok da LCDUI.
Revista Java Magazine Edição 46
:
JBoss de Ponta a Ponta - Instalação, Arquitetura, Configuração , Tuning e Administração - Conheça detalhes sobre o mais popular servidor de aplicações Java EE open source em um mega-artigo!
Saiu na DevMedia!
- Buscas semânticas com Elasticsearch:
Elasticsearch é uma ferramenta de busca de alta performance. Com ela podemos armazenar grandes volumes de dados, analisá-los e processá-los, obtendo resultados em tempo real.
Saiba mais sobre Java ;)
- Guia do Programador Java:
Aprender Java não é uma tarefa simples, mas seguindo a ordem proposta nesse Guia, você evitará muitas confusões e perdas de tempo no seu aprendizado. Vem aprender java de verdade, vem!
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo