Animação e Transição com LWUIT



por Biswajit Sarkar
20/11/2008


Animações e transições são duas características muito interessantes do Lightweight User Interface Toolkit (LWUIT). Neste artigo veremos como usar esses recursos em aplicações reais. Temos também que desenvolver uma animação transição personalizada. Embora muito simples, esta transição demonstra plenamente o processo de tal personalização.
Tenho usado o Sprint Wireless Toolkit 3.3.1 (Sprint WTK) para as telas, e também para construir a CustomTransitionDemo. Tenho referido também o código fonte para o LWUIT aplicação demonstração, que vem com o pacote  LWUIT download para ilustrar o uso das capacidades de animação e transição do LWUIT. Para uma boa compreensão do presente artigo, o conjunto de ferramentas e o pacote LWUIT devem ser baixados e instalados em seu computador. Versão 3.3.2 do Sprint WTK foi disponibilizado; Eu testei-o e conclui que não há existe problema em executar nossa demonstração sobre a nova versão.
Animação
Foi possível animar imagens sobre a plataforma Java ME, mesmo antes do LWUIT aparecer em cena. Existe um número de funcionalidades no pacote javax.microedition.lcdui.game animação que suporta animação. Além disso, a API JSR 226 fornece suporte para animação SVG-based. LWUIT lida com animação um pouco diferente, fornecendo suporte para uma capacidade que é ampla e simples de usar. No contexto do LWUIT, animação essencialmente permite objetos para pintar quadros sucedendo em intervalos cronometrados. Animação suporta no LWUIT também tornar fácil a transição para ser implementada.
Para estar capacitado de ser animado, um objeto tem que implementar a interface Animation. Animation tem dois métodos.
     * animate: Este é um método callback invocado uma vez a cada frame.
     * paint: Este método é chamado se animate retorna true. Se o objeto a ser animado é um componente, ele terá um método paint e que é um método a ser chamado como a assinatura dos dois métodos, que é idêntico.

A classe Component estende Animation. Isto torna cada widget capaz de animação. No entanto, para realmente realizar animação, um objeto também deve ser registrado para animação por chamar o método  registerAnimated no seu formulário pai. O método animate de um objeto registrado é chamado uma vez para cada frame da animação, conforme determinado pela classe Display. Se animate retorna true, o método  paint do objeto é chamado e um repaint ocorre. Para parar a animação, o objeto deve ser desregistrado por chamar o método deregisterAnimated no formulário pai.






Figura 1. Dois quadros de demonstração de animação A demonstração mostra dois exemplos de animação: um usa uma imagem animada do Duke e o outro chama repetidamente um conjunto de imagens para simular movimento. Em ambos os casos, o componente básico que é animado é um botão. Outros componentes também podem ser animados de uma forma semelhante. Pode substituir os botões no código para AnimationDemo com labels e a demonstração continuará a funcionar bem. Basta lembrar de importar a classe Label.
Ao executar o método de AnimationDemo, o primeiro botão - animation - é instanciados e adicionados ao formulário (f), que foi passado como um parâmetro para o método:

 Button animation = new Button(UIDemoMIDlet.
getResource("duke").getAnimation("duke3_1.gif"));
animation.setBorderPainted(false);
animation.getStyle().setBgTransparency(0);
f.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
f.addComponent(animation);

A segunda e terceira linhas são apenas para efeitos visuais e a quarta define o gerenciador de layout para o formulário. A coisa interessante para notar aqui é que o botão não foi registrado para a animação. Isso é porque a própria imagem é animada e não exige repintagem. A imagem de Duke utilizado aqui é um gif animado. Neste momento LWUIT suporta apenas formato gif animado; como uma questão de fato, o Resource Editor não reconhece qualquer outro formato animado.

Para o segundo botão, o conjunto de imagens é criado para o método paint:


Resources images = UIDemoMIDlet.getResource("images");
Image a = images.getImage("progress0.png");
Image b = images.getImage("progress1.png");
Image c = images.getImage("progress2.png");
final Image[] secondAnimation = new Image[] {
a,
b,
c,
a.rotate(90),
b.rotate(90),
c.rotate(90),
a.rotate(180),
b.rotate(180),
c.rotate(180),
a.rotate(270),
b.rotate(270),
c.rotate(270),
};


Temos agora um conjunto de doze imagens, que estão desenhadas sequencialmente. O código para criar o segundo botão parece com o que segue:

 Button animation2 = new Button() {

private int currentImage = 0;
private long lastInvoke;

public boolean animate() {
long current = System.currentTimeMillis();
if (current - lastInvoke > 50) {
lastInvoke = current;
currentImage++;
if (currentImage == secondAnimation.length) {
currentImage = 0;
}
return true;
}
return false;
}

public void paint(Graphics g) {
g.drawImage(secondAnimation[currentImage],
getX(), getY());
}
};

Aqui temos um botão que implementa ambos os métodos animate e paint. O método animate será chamado a cada intervalo frame. Se mais de 50 milissegundos tiverem decorrido desde a última chamada, animate incrementará o ponteiro para o conjunto de imagens e retorna true, caso contrário, ele retornará apenas falsa. Se true for devolvido, em seguida, o método paint será chamado e as imagens serão tiradas.
O resto do código para o segundo botão é principalmente para a fixação de parâmetros visuais e para adicionar o botão ao formulário. Observe a última linha de código, o qual mostra que o botão foi registrado pela animação. Isso garante que o método animate será chamado para cada frame.

 animation2.setPreferredSize(new Dimension
(secondAnimation[0].getWidth(),
secondAnimation[0].getHeight()));
animation2.setBorderPainted(false);
animation2.getStyle().setBgTransparency(0);
f.addComponent(animation2);

f.registerAnimated(animation2);

E isso é tudo o que existe para aplicar a animação!
Transições
O termo transition refere-se para o caminho um formulário que é trazido para dentro (animate em transição) e levadas para fora do visor (animate fora transição). A classe base para a execução de transição é Transition,, que implementa Animation. Isto é porque transições trabalha basicamente como animações e utiliza o mesmo mecanismo de chamada como qualquer outro componente animados. No entanto, Transition é diferente de Component no sentido de que um Component anima a sua própria renderização enquanto Transition controla a renderização de formulários e diálogos animados.
Existe também uma diferença entre os caminhos em que componente animações e transições são utilizados. Para usar transições, não é necessário registrar o formulário em questão de animação. Quando um formulário trasição ocorre, o objeto Display diretamente coloca o formulário na fila para receber chamadas.
A classe Transition é um abstract e não pode ser instanciado. A biblioteca LWUIT inclui duas subclasses concretas de Transition que pode ser utilizada para aplicar transições. Essas classes são: 


  • CommonTransitions
  • Transition3D
CommonTransitions inclui atualmente dois tipos de animações transição.
  • Slide: O novo formulário slides dentro, empurrando o atual.
  • Fade:  O novo formulário fades dentro, enquanto  o atual fades sai.

CommonTransitions trabalha com um suporte a classe - Motion - que fornece modelos para simulação de movimento físico. Os três tipos de movimentos que contém Motion são:

  • Friction
  • Spline
  • Linear

A classe Transition3D trabalha com a API M3G (JSR 184) para fornecer animações de transições com efeitos 3D. JSR 184 support é necessário para estas transições para funcionar corretamente, e assim a classe Motion não é requerida para Transition3D. Uma limitação atual desta classe é que as suas transições trabalham apenas com formulários, mas que é susceptível de alterar  soon. O 3D suite de transições contém o que segue.

  • Cube: Simula um cubo giratório, com a forma atual, sobre a face rotativa fora da tela e do novos em face rotativa na tela.
  • Fly In: O novo arquivo formulário dentro.
  • Rotation: Uma versão de duas dimensões de Cube iem que um avião em vez de um cubo roda.
  • Static Rotation: Aplicável a uma transição de um formulário dentro de um dialogo. Só o diálogo gira enquanto o formulário permanece estático.
  • Swing In: O formulário oscila em qualquer lugar, de cima para baixo ou de baixo para cima.

Transições são muito fáceis de usar, como se pode ver o código seguinte, a partir de fragmentos (da classe TransitionDemo da aplicação demonstração LWUIT), que são exemplos de como criar transições usando a métodos factory aplicável:

 //creates an out transition
//with a right-to-left outgoing slide
out = CommonTransitions.createSlide(
CommonTransitions.SLIDE_HORIZONTAL, false, runSpeed);

//creates an out transition
//with a left-to-right incoming slide
in = CommonTransitions.createSlide(
CommonTransitions.SLIDE_HORIZONTAL, true, runSpeed);
.
.
.
//creates outgoing and incoming fade transitions
out = CommonTransitions.createFade(runSpeed);
in = CommonTransitions.createFade(runSpeed);
.
.
.
//creates outgoing and incoming fly in transitions
out = Transition3D.createFlyIn(runSpeed);
in = Transition3D.createFlyIn(runSpeed);

Você pode ver que não existe diferença na forma como é criada uma transição, independentemente de saber se é uma transição comum ou uma transição 3D. Depois de criar uma transição, tem que ser instalado por uma forma desejada. Isso também é simples:

 //f is the form for which transitions are being set
f.setTransitionOutAnimator(out);
f.setTransitionInAnimator(in);

Note que se você definir a saída de transição para uma forma que não seja necessária definir, a futura transição para o nova forma que está entrando na tela; conforme a forma antiga sai é automaticamente substituída por uma nova. Quando uma aplicação tem uma série de formas, acho que é uma boa prática definir apenas a saída de transições para todas as formas. Esta é a forma de se evitar conflitos e transições perdidas.
Uma Transição Personalizada Uma das grandes coisas sobre LWUIT, é que ele suporta um bom negócio de personalização. Portanto, se você deseja uma transição que não está disponível classes transition, que vêm com a biblioteca, você pode escrever facilmente as suas próprias. Nesta seção iremos desenvolver uma simples transição para demonstrar as técnicas básicas para a criação de transições personalizadas. Antes de darmos início em nosso projeto, porém, gostaria de acrescentar uma ressalva aqui. A nova transição é apenas uma ferramenta para entender como transitions e motions trabalham. Para manter as coisas simples, tenho impostas algumas restrições que tornam esta classe imprópria para o uso geral. Alguns destes são:

  • Não suporta diálogos.
  • Não é configurável e tem apenas um tivo de movimento -- horizontal, para a direita.
  • Não verifica todas condições que podem se rencontradas em um dispositivo real.

Nossa transição é chamada Step Transition. Como o próprio nome sugere, ele faz a tela destino mover para a posição de uma série de passos discretos, ao contrário do Slide Transition. Outra diferença entre as duas transições é que com step transition, a próxima tela não aparece para empurrar o fonte da tela de saída - que se move sobre a fonte, gradualmente cobrindo-a.
Para criar uma transição personalizada, temos que escrever uma classe que estende Transition. Neste caso, a classe é StepTransition. Precisamos também de um modelo adequado para o movimento e, para a nossa demonstração, que é a classe StepMotion.
Começamos por olhar para a classe StepTransition. A classe Transition tem o seguintes métodos abstratos, que devem ser implementados por StepTransition:  

  • public abstract Transition copy()
  • public abstract boolean animate()
  • public abstract void paint(Graphics g)


Além disso, há o método vazio initTransition  que deverá ser devidamente aplicado. Dependendo da natureza da transição, esse método não pode ser exigido a todos, caso em que você pode omiti-lo totalmente.
O método initTransition é uma chamada que é invocada no início de cada transição. Então este é o lugar para inicializar todos os parâmetros para o início da transição. Também deve-se instanciar o objeto StepMotion, que vai trabalhar fora da posição para renderizar a tela destino (lembre-se que essa é a única tela que se move) para cada frame. Nosso código para initTransition é bastante simples:

 public void initTransition()
{
//initialize variables
//required to set up motion
Component source = getSource();
int width = source.getWidth();
position = 0;
int startoffset = 0;

//create a StepMotion object for this transition
motion = new StepMotion(startoffset,
width, duration, step);

//start the movement for the transition
motion.start();
}

Graças à simplicidade das nossas especificações, precisamos fazer apenas algumas coisas para iniciar a transição. O que fazemos é inicializar as variáveis necessárias para criar o objeto motion, criar o motion, e iniciá-lo em execução.
O método animate é chamado uma vez para cada frame. Este método verifica se o objeto motion está ausente ou se a transição está maior. A verificação para a conclusão da transição envolve chamada para o método isFinished de StepMotion, que retorna true se a transição for feita. Se lá não existir nenhum objeto motion ou se a transição estiver concluída, false é retornado para que esta atividade transição seja arquivada. Caso contrário, a transição continuará e a posição para desenhar a tela destino correspondente ao próximo frame é obtido. Finalmente, animate retorna true, assim, o próximo frame pode ser desenhado:

public boolean animate()
{
//see if there is a motion object
//and check if the transition is completed
if(motion == null || motion.isFinished())
{
//in either case terminate transition
return false;
}

//if transition is to continue
//get the new position
position = motion.getStep();

//continue with transition
return true;
}

Desde que StepTransition implementa Animation, tem um método paint que é invocado para recarregar a tela destino para cada frame. Como vemos no segmento de código acima, a posição para desenhar a tela destino é retornado pelo método getStep da classe StepMotion. Desde que o valor da posição altera apenas em passos discretos, ele pode permanecer inalterado ao longo de uma série de frames. Desenhando a(s) tela(s) para cada frame seria um desperdício de tempo de CPU. Para evitar isto, getStep retorna -1 se a situação manteve-se inalterada desde o último frame. O método paint verifica o valor da posição e chama paintSlideAtPosition para fazer a própria pintura somente quando necessário. O código para o método paint é dado abaixo:

public void paint(Graphics g)
{
//paint only if new position is available
if(position > -1)
{
paintSlideAtPosition(g, position, 0);
return;
}
else
{
return;
}
}

A atual renderização é feita pelo método paintComponent da classe Component após os contextos gráficos foi preparado pelos seguintes métodos:


  • private void paintSlideAtPosition(Graphics g, int slideX, int slideY)
  • private void paint(Graphics g, Component cmp, int x, int y)

Para a nossa aplicação simples, poderíamos ter combinado todos os métodos paint. No entanto, tem conservada a estrutura da classe CommonTransitions que vem com a biblioteca. Espero que isto torne mais fácil de seguir a sequência de método para quem deseja estudar o código fonte.
O método paintSlideAtPosition primeiro verifica se a primeira forma de aplicação está sendo exibida e, nesse caso, inibe a transição. Caso contrário, configura a região clip e, mais importante para esta aplicação, as coordenadas para a tradução, de modo que o deslocamento apropriado da tela destino possa ocorrer. O próximo método cuida da real tradução, chama o paintComponent no Component para extrair a tela destino na posição certa e restaura a tradução original. O programa demonstrativo, assim, faz com que a tela destino mova-se sobre uma fonte estacionária.  Isto acontece porque apenas a tela destino é repintada para cada nova posição. Se você quer conseguir um efeito push, basta descomentar as duas linhas de código em negrito no método paintSlideAtPosition. Isto fará com que a tela fonte igualmente seja repintada e você verá mover-se fora da área de exposição como a tela destino se move dentro. Os dois métodos são mostrados abaixo:


 private void paintSlideAtPosition(
Graphics g, int slideX, int slideY)
{
Component source = getSource();

//if this is the first form being displayed we
//can't do a step transition
//since we have no source form
if (source == null)
{
return;
}

Component dest = getDestination();

int w = -source.getWidth();
int h = 0;

//set the clips

//uncomment the two following lines
//for the "push" effect
//g.setClip(source.getAbsoluteX(),
source.getAbsoluteY(), source.getWidth(),
source.getHeight());
//paint(g, source, slideX , slideY );


g.clipRect(dest.getAbsoluteX(),
dest.getAbsoluteY(), source.getWidth(),
source.getHeight());
paint(g, dest, slideX + w, slideY + h);

}

private void paint(Graphics g, Component cmp, int x, int y)
{
//get original clip details
int cx = g.getClipX();
int cy = g.getClipY();
int cw = g.getClipWidth();
int ch = g.getClipHeight();

//translate to proper position
//for drawing the form
g.translate(x, y);

//get it painted
cmp.paintComponent(g, false);

//restore original values
g.translate(-x, -y);
g.setClip(cx, cy, cw, ch);
}

O método copy retorna apenas uma cópia da etapa do objeto transição. Note que uma cópia é retornada e não o próprio objeto, assim a classe Display quer trabalhar com uma cópia.


//return a functionally equivalent transition object
public Transition copy()
{
return new StepTransition(duration, step);
}

O construtor de StepMotion tem quatro inteiros como parâmetros.

  • sourcevalue: Esta é a posição inicial. Se a nova tela começa em movimento a partir da borda esquerda, então este será zero.
  • destinationvalue: Este é a posição de finalização. Se a nova tela deverá parar na margem direita, então esta vai ser igual à largura da área de exposição.
  • duration: O período de tempo (em milisegundos) que ao longo da transição deverá durar.
  • steps: O número de passos de transição que deverá assumir.

O construtor, calcula o valor pelo qual a posição da tela destino tem que ser incrementada de um passo para o outro. Ele também calcula o tempo mínimo que deve decorrer antes da tela poder ser recarregada.



public StepMotion(int sourcevalue, int destinationvalue,
int duration, int steps)
{
this.destinationvalue = destinationvalue;
this.duration = duration;
this.steps = steps;

//the size of a step
stepsize = (destinationvalue - sourcevalue)/steps;

//the time interval between two successive steps
interval = duration/steps;
}

Já temos encontrado os três métodos de StepMotion. O único ponto a notar aqui é que o tamanho do passo calculado não pode ser um fator da distância total (destinationvalue - sourcevalue) a serem abordados, como estamos usando inteiro matemático. Nesse caso, o método getStep assegurará que a distância faltante é feita como se ela sempre pega mais um passo do que calculado número de passos. Isso é verdade porque isFinished retorna true quando o número de passos dados (stepnumber) excede o número de passos. Esse passo extra realmente não causa qualquer problema, como a position não é permitida para ultrapassar destinationvalue. Os métodos são:

//save the time as the beginning of an interval
public void start()
{
starttime = System.currentTimeMillis();
}

//return true if all steps have been taken care of
public boolean isFinished()
{
return stepnumber > steps;
}

//return the position
public int getStep()
{
//check if interval for next step is over
//if so increment stepnumber
//and return (stepsize*stepnumber)
//also reset starttime by calling start()
//if (stepsize*stepnumber>destinationvalue)
//then return destinationValue
//if interval not over return -1

if(System.currentTimeMillis() >=
starttime + interval)
{
stepnumber++;
int offset = stepnumber*stepsize;
if(offset > destinationvalue)
{
offset = destinationvalue;
}

start();

return offset;
}
else
{
return -1;
}
}

O MIDlet que põe todo o conjunto, também é bastante simples. Depois de chamar a init em Display, que inicia os parâmetros para a transição, e define-se o theme. Em seguida, cria dois rótulos para os dois formulários que serão utilizados na demonstração. Gostaríamos que os dois formulários fosse olhado um tanto diferente, de modo que a transição se tornasse visualmente proeminente. Então, o primeiro formulário é criado e suas barras de títulos são feitas diferentes a partir do tema configurado. O rótulo srclabel é acrescentado ao primeiro formulário e, assim, são os comandos. O MIDlet é então definido como o comando ouvinte para o formulário. Por último, o primeiro formulário é feito visível:

 //init the LWUIT Display
Display.init(this);

//duration of transition in milliseconds
int duration = 3000;

//number of steps in transition
int numofsteps = 10;

//get the theme and set it
try
{
Resources r = Resources.open("/SDTheme1.res");
UIManager.getInstance().setThemeProps(
r.getTheme("SDTheme1"));
}
catch (java.io.IOException e)
{
}

//create two labels
Label srclabel = new Label("This is the source screen");
Label dstlabel = new Label(
"This is the destination screen");

//create the first form
first = new Form("Source");

//we make the titlebar different
//from the theme setting
first.getTitleStyle().setBgImage(null);

//add srclabel to the first form
first.addComponent(srclabel);

//add 'Next' command to first form
//the command id is 1
first.addCommand(new Command("Next", 1));

//add 'Exit' command to first form
//the command id is 0
first.addCommand(new Command("Exit", 0));

//this MIDlet is the listener
//for the first form's commands
first.setCommandListener(this);

//show the first form
first.show();


A próxima parte do código, que trata do segundo formulário, é quase o mesmo. A diferença fundamental é que as transições in e out são definidas para este formulário. Como esta demonstração tem apenas dois formulários, foi violada a própria recomendação e configuradas ambas transições para o mesmo formulário.

 //create the second form
second = new Form("Destination");

//set the Out and In transitions for the second form
second.setTransitionOutAnimator(new StepTransition
(duration, numofsteps));
second.setTransitionInAnimator(new StepTransition
(duration, numofsteps));

//set Menu and background images for second form
try
{
second.getMenuStyle().setBgImage(Image.createImage
("/pattern3.png"));

}
catch(java.io.IOException ioe)
{
}

try
{
second.getStyle().setBgImage(Image.createImage
("/sdsym4.png"));
}
catch(java.io.IOException ioe)
{
}

//add dstlabel to second form
second.addComponent(dstlabel);

//add 'Previous' command to first form
//the command id is 2
second.addCommand(new Command("Previous", 2));

//this MIDlet is the listener
//for the second form's commands
second.setCommandListener(this);


Toda a ação tem lugar no método startApp. Agora, vamos cuidar dos comandos:

 public void actionPerformed(ActionEvent ae)
{
Command cmd = ae.getCommand();
switch (cmd.getId())
{
//'Exit' command
case 0:
notifyDestroyed();
break;

//'Next' command
//show second form
case 1:
second.show();
break;

//'Previous' command
//show first form
case 2:
first.show();
}
}


As três telas nas figuras 2 a 4 mostram imagens progressivas do passo da transição do primeiro para o segundo formulário.


Figura 2. Passo número 2 da transição


Figura 3. Passo número 5 da transição


Figura 4. Passo número 8 da transição
Conclusão
Vimos como usar as funções de animação e transição do LWUIT e como criar a nossa própria transição. LWUIT é uma biblioteca de evolução e somos obrigados a ver acréscimos ao seu atual repertório. Os desenvolvedores terão que atualizar constantemente os seus conhecimentos para permanecer a par das novas funções, capacidades e refinamentos. Vai ser preciso algum trabalho duro, mas será muito divertido também!
Recursos


Biswajit Sarkar é engenheiro elétrico com especialização em programação em automação industrial.