segunda-feira, 16 de abril de 2012

Animações no Android 3+

Olá povo,

Essa semana, meu colega Felipe Vasconcelos me lembrou de um problema existente na API de animações do Android 2 (que falei nesse post).
Digamos que você tenha um botão em uma posição x/y, e que esse botão seja posicionado em outro lugar através de uma animação (uma TranslateAnimation pra ser mais exato). Se você clicar nesse botão após a animação, você não conseguirá. Isso se deve a uma característica (ou bug?) da API de animação do Android até o 2.x, que só altera o aspecto da View, mas não seu conteúdo interno. Isso quer dizer que, apesar do botão mudar a posição visualmente, ele continua na posição antiga. Ou seja, se você clicar no botão na posição anterior à animação, o evento de clique ocorrerá.

Para corrigir esse problema, o Google introduziu uma nova API de animação no Android 3.0, que traz ainda o benefício de acompanhar cada passo da animação, coisa que não acontecia na antiga API.
Para entender melhor o problema e a nova API, vamos criar um exemplo bem simples. Abaixo temos o arquivo XML que define a tela da nossa aplicação:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="horizontal">

  <Button
    android:id="@+id/btnAnimacaoAntiga"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onClickAnimarAntigo"
    android:text="Animar 2.x" />

  <Button
    android:id="@+id/btnAnimarNova"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onClickAnimarNovo"
    android:text="Animar Honeycomb" />

</LinearLayout>
A tela é bem simples, apenas dois botões que serão animados até a posição 200 na coordenada Y. O primeiro animará utilizando a API antiga, e o segundo a nova. Abaixo, temos a Activity da aplicação:
public class AnimationHoneycombActivity 
  extends Activity {
 
  private boolean b1Desceu;
  private boolean b2Desceu;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }
    
  public void onClickAnimarAntigo(View v){
    int yInicial, yFinal;
    if (b1Desceu){
      yInicial = 200;
      yFinal = 0;
    } else {
      yInicial = 0;
      yFinal = 200;
    }
     
    TranslateAnimation animacao = 
      new TranslateAnimation(0, 0, yInicial, yFinal); 
    animacao.setDuration(2000);
    animacao.setFillAfter(true);
    v.startAnimation(animacao);
    b1Desceu = !b1Desceu;
  }
    
  public void onClickAnimarNovo(final View v){
      
    ValueAnimator animacao = 
      ValueAnimator.ofFloat(0, 200);

    animacao.setTarget(v);
    animacao.setDuration(2000);
    animacao.addUpdateListener(
      new ValueAnimator.AnimatorUpdateListener() {
   
        @Override
        public void onAnimationUpdate(
          ValueAnimator animation) {

          Float valor = (Float)
            animation.getAnimatedValue();
          v.setY(valor);
       }
     });

    if (b2Desceu){
      animacao.reverse();
    } else {
      animacao.start();
    }
    b2Desceu = !b2Desceu;
  }
}
No primeiro método, utilizamos a API antiga de animações. Utilizei uma flag para determinar se o botão desceu, para determinar a posição inicial e final na coordenada Y. Em seguida, criei uma TranslateAnimation, defini a duração, e em seguida, com o método setFillAfter determinei que ao terminar a animação, o botão deve permanecer onde a animação terminou. Por fim, mandei animar o botão.
Já no segundo método, estou usando a nova API de animações. A classe ValueAnimator é a chave dessa API, e ela tem basicamente: um valor inicial e final; um target, que é um objeto View; a duração em milissegundos; e um AnimatorUpdateListener. Esse último objeto é chamado N vezes para realizar a animação, onde N depende do valor final e sua respectiva duração. É nesse objeto que devemos alterar a propriedade da View que queremos animar, que no nosso caso, é a propriedade Y do botão.
No final do método vemos se o botão desceu, em caso positivo, solicitamos que a animação seja feita ao contrário com o método reverse; caso contrário, mandamos animar para a posição desejada.
Para observar o "bug" da antiga API de animação, clique no primeiro botão e ele descerá. Em seguida, clique no botão e você notará que nada acontecerá. Agora tente clicar onde o botão estava... Magicamente o botão voltará para a posição original.

Em resumo, se você precisar interagir com uma View que sofreu uma animação, ou ainda precisar obter informações internas sobre elas, você terá que contornar essa limitação da API antiga de animações. Mas se você estiver pensando em desenvolver para tablets Android ou ainda para os novos aparelhos com Android 4 ICS, utilize a nova API de animações.

4br4ç05,
nglauber

4 comentários:

FelipeVasconcelos disse...

Gláuber,

Ainda fazendo uma busca na API, encontrei essa outra maneira de implementar:

ValueAnimator anim = ObjectAnimator.ofFloat(mView, "x", xInicial, XFinal);
anim.setDuration(mDuracao);
anim.start();

mais info: http://developer.android.com/guide/topics/graphics/prop-animation.html#object-animator

[]'s.

Nelson Glauber de Vasconcelos Leal disse...

Muito bom Felipe,

A vantagem da maneira que coloquei é que você pode implementar alguma lógica durante o andamento da animação, mas caso isso não seja necessário, sua abordagem é melhor mesmo.

4br4ç05,
nglauber

Felipe Bonezi disse...

Oi Glauber, infelizmente animações em Android 2.x realmente não é legal! :(

Uma das coisas mais chatas é que uma animação (por exemplo, andar 20px à esquerda) ao terminar sua execução não fica no lugar final, voltando para sua posição inicial e tratar isso realmente é chatinho :(

Nelson Glauber de Vasconcelos Leal disse...

Oi Felipe,

Visualmente fica no lugar sim. Basta usar o método setFillAfter(true) como tem no exemplo acima.
O problema é se você quiser interagir com ela, aí a View é considerada ocupando o lugar anterior à animação.

4br4ç05,
nglauber