2012年4月18日水曜日

AndroidのTranslateAnimationのクセはひどいね

まず、AndroidでViewを移動させようとして困った。

getX()とかsetX()とか無いし。move()とかも無いし。じゃあどうするの?といったら、どうやらlayout(int left, int top, int right, int bottom)で移動させるらしい。('A`)マンドクセじゃないかよぉ。とは言え、これはちゃんとできたからよし。

問題は移動アニメーション(TranslateAnimation)だ。何だか移動アニメーションさせると元の位置に戻るという変態的な動きをするのはなぜだ??

調べてみると困っている人多数らしく、animation.setFillAfter(true)すればいいという声が多数。

Android TranslateAnimation animation - Stack Overflow

・・・
By default, the animation resets the object to the initial state. You need to specify fillAfter to true:

animation.setFillAfter(true);
・・・

そこで試してみたが問題は解決しなかった。移動アニメーションさせた後でタッチ操作でターゲットを移動させると、残像が残っている!何なんだコレは?

アニメーション後のターゲットオブジェクトの座標を調べてみると、全く動いていない!どうやらsetFillAfter()はアニメーション終了時に「ビジュアル」だけ残すというメソッドらしい。こんなの意味あんのかよぉ〜(^^);

さらに調べていくと、Androidの開発者による決定的な記事が見つかった。

Animation in Honeycomb | Android Developers Blog

・・・
Animation Prior to Honeycomb
・・・
Finally, the previous animations changed the visual appearance of the target objects... but they didn't actually change the objects themselves. You may have run into this problem.
・・・
The problem is that the animation changes where the button is drawn, but not where the button physically exists within the container
・・・
Property Animation in Honeycomb・・・ValueAnimator・・・ObjectAnimator
・・・

全くその通り。そして、その問題はHoneycomb(API Level 11)以降に追加されたValueAnimatorやObjectAnimatorによって解決されたらしい。

要するに、AndroidはGingerbread(API Level 10)でもまだ未完成だったということだと思う。Flashではアニメーションのことをいちいち「Property Animation」だ何だと区別しない。それが常識だから。やっぱりAndroidはICSでやっと完成されたOSになったのだろうなと感じた。

しかし、解決されてよかったね!とはならない。今、稼働しているAndroid機のほとんどはHoneycombレベル以下だから、ValueAnimatorとか使えないわけ。


そこで何とかすることにした。要するに、アニメーションが終わった後にターゲットを物理的に移動させればいいわけだ。ということでやってみたのがこんな風。


Eclipseプロジェクトファイル:TransformAnimation.zip (153KB)


res/layout/main.xml

<?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="vertical" >

    <TextView
        android:id="@+id/target"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#660000"
        android:gravity="center"
        android:text="target"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</LinearLayout>


TransformAnimationActivity.java

package jp.example;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;


public class TransformAnimationActivity extends Activity {


  // 移動アニメーションさせるターゲット
  protected View target;


  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    target = findViewById(R.id.target); // ターゲット取得
  } // END onCreate()


  @Override
  public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_UP:
        Log.v("onTouchEvent", "ACTION_UP x=" + target.getLeft() + " y=" + target.getTop());
        moveTarget(50, 50); // タッチアップでターゲット移動
        break;
    } // END switch
    return super.onTouchEvent(event);
  } // END onTouchEvent()


  /**
   * ターゲットを相対的に移動する
   * 
   * TranslateAnimationで移動アニメーションをすると、
   * 見かけ上は移動したように見えるが実際には移動していないらしい。
   * そこで、アニメーション完了時にlayout()を使って物理的にも移動させる。
   * 
   * @param dx X軸に対する相対移動量
   * @param dy Y軸に対する相対移動量
   */
  protected void moveTarget(int dx, int dy) {
    if (target.getAnimation() != null) {
      return; // アニメーション中なら何もしない
    }
    final int left = target.getLeft();
    final int top = target.getTop();
    final int toX = left + dx;
    final int toY = top + dy;
    TranslateAnimation anim = new TranslateAnimation(left, toX, top, toY);
    anim.setAnimationListener(new Animation.AnimationListener() {
      @Override
      public void onAnimationStart(Animation animation) {
      }
      @Override
      public void onAnimationRepeat(Animation animation) {
      }
      @Override
      public void onAnimationEnd(Animation animation) {
        target.layout(toX, toY, toX + target.getWidth(), toY + target.getHeight()); // 物理的にも移動
        target.setAnimation(null); //これをしないとアニメーション完了後にチラつく
      }
    });
    anim.setDuration(500); // セットしないとアニメーションしない
    target.layout(0, 0, target.getWidth(), target.getHeight()); // 初期位置に戻す。これをしないと2度目以降のアニメーションがおかしくなる(チラつく)
    target.startAnimation(anim);
  } // END moveTarget()


} // END class TransformAnimationActivity

重要な部分はmoveTarget()に集まっている。

第1のポイントは、アニメーションにAnimationListenerをセットしてアニメーション終了時のイベント取得して、そこで物理的にターゲットを移動する部分。

第2のポイントは、チラつき防止のためにターゲットを初期位置に戻す部分。チラつき方が何となくTransform Matrix関連っぽい感じだったので、とりあえず初期位置に戻してみたらどう?と思ってやってみたらうまくいったみたい。

---- 追記12.04.19

第3のポイントは、onAnimationEnd()でtarget.setAnimation(null)することだとわかった。これをしないとアニメーション完了後にチラつく。チラつきを見る前にこのコードを入れていたので気が付かなかったようだ。

---- END OF 追記

オブジェクトごとにいちいちこんなコードを書くようではたまらんので、どうにかして便利なクラスを書かないとダメだね。こりゃ。