スポンサーリンク

Androidで,複数のAnimationを「順番に」実行するためのライブラリ (XMLを使わずに「連続した動きの変化」を指定し,逐次実行するDSL)

重要なお知らせ:

この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。

より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。


AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...


Androidアプリの画面上で,ダイナミックな視覚効果を表現するためには,

SDKに組み込み済みの アニメーション API を利用する。


もし,複数のAnimationを組み合わせて利用する場合,

「複数」という語には,2通りの意味が存在する。

  • (1)AnimationSetとして合成されて,同一のタイミングで実行される。
  • (2)時間的な順序で連続して,「順番に」個別に実行される。


前者(1)のケースでは,複数のアニメーションを合成し,1つのアニメーションとして扱うことが可能だ。

したがって,合成結果である「AnimationSet」オブジェクトは,普通の1つの「Animation」オブジェクトとして取り扱う事ができる。

※AnimationSetがAnimationを継承しているため。



では,後者(2)のケースはどうか?


「移動する」「回転する」「アルファ値を遷移させる」「伸縮する」

などの複数のアニメーションを,順番に実行したい。

しかも,動作対象となるViewを,途中で自由に切り替えたい。

また,間にポーズ(一時停止)などを自由に挟みたい。

なおかつ,シンプルなコードにしたい。


こういう場合,どうするか。

「連続した複数のアニメーション」を,シンプルにコーディングしよう

上記の(1)と(2)の両方の要望を同時に叶えて,簡潔にプログラミングしよう。

例えば,下記のようなフローの連続したアニメーションを考える。

  1. View1が,下にスーッと移動。 その後,ちょっと停止。
  2. 次に,View1とView2が,いっぺんに右にスーッと移動。 その後,ちょっと停止。
  3. 次に,View1とView2が,同時にスーッと上に移動しながら,フェードアウト。 しばらくそのまま。
  4. 全てのアニメーションが終わったら,自動的に何らかの処理(ここでは画面遷移)を実行する。

このフローを,Activity上で下記のようにコーディングできたら便利ではないか?

Activity:

package com.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
import android.widget.TextView;

public class AnimTestActivity extends Activity implements OnClickListener {

    SequentialAnimationsRunner anim_runner;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        findViewById(R.id.main_btn).setOnClickListener(this);


        // アニメーションを定義


        TextView tv1 = (TextView)findViewById(R.id.tv1);
        TextView tv2 = (TextView)findViewById(R.id.tv2);
        final Activity activity = this;

        anim_runner = new SequentialAnimationsRunner(this)
            .add(

                // アニメーションのターゲットを設定
                new AnimationDescription().targetViews( tv1 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // 下方向に移動
                        Animation anim = new TranslateAnimation(
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 150f
                        );

                        return anim;
                    }

                    @Override
                    protected void modifyAfterAnimation(View v)
                    {
                        // 下にずらす
                        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)v.getLayoutParams();
                        lp.setMargins(lp.leftMargin, lp.topMargin + 150, lp.rightMargin, lp.bottomMargin);
                        v.setLayoutParams(lp);
                    }
                }.waitBefore(1000).duration( 2000 ).waitAfter( 1000 )
                ,

                // アニメーションのターゲットを変更
                new AnimationDescription().targetViews( tv1, tv2 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // 右方向に移動
                        Animation anim = new TranslateAnimation(
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 150f,
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 0f
                        );

                        return anim;
                    }

                    @Override
                    protected void modifyAfterAnimation(View v)
                    {
                        // 右にずらす
                        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)v.getLayoutParams();
                        lp.setMargins(lp.leftMargin + 150, lp.topMargin, lp.rightMargin, lp.bottomMargin);
                        v.setLayoutParams(lp);
                    }
                }.duration( 1000 ).waitAfter( 500 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // 上方向に移動
                        Animation anim1 = new TranslateAnimation(
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, -150f
                        );

                        // フェードアウト
                        Animation anim2 = new AlphaAnimation(1f, 0f);

                        // これらのアニメーションを合成して同時進行させる
                        AnimationSet anim_set = new AnimationSet(true);
                        anim_set.addAnimation(anim1);
                        anim_set.addAnimation(anim2);

                        return anim_set;
                    }

                    @Override
                    protected void modifyAfterAnimation(View v)
                    {
                        // 消える
                        v.setVisibility(View.GONE);
                    }
                }.duration( 2000 ).waitAfter( 2000 )

            )
            .onFinish( new AnimationsFinishListener(){
                @Override
                protected void exec()
                {
                    Log.d("AnimTest", "全アニメーションが終了したため,画面遷移します。");

                    // 終わった後の処理。画面遷移
                    activity.startActivity(
                        new Intent( activity, AnimTestActivity.class )
                    );
                }
            })
        ;
        // この場ですぐにstart() で開始することもできるが,
        // 実行中のロック保持のため,いったんインスタンスを保持する。

    }


    @Override
    public void onClick(View v) {

        Log.d("AnimTest", "開始ボタンが押されました。");

        // アニメ開始
        anim_runner.start();

    }
}

単純に,時系列にアニメーション制御の仕様が記述されているので,

ソースコードから,アニメーションのフローを容易に理解できる。

一つのアニメーションの終了をListenしてリスナ内で次のアニメを呼び出して・・・という,非同期プログラミングでよく見られるコールバックの入れ子地獄も味わわなくて済む。


そして,このように時系列に記述したコード内容にしたがって,

各アニメが「逐次的に」(シーケンシャルに)ちゃんと動作する。


画面構成は下記の通りで,アニメーションを発動させるためのボタンが1個置いてある。

なお,ボタンが押されてから,全アニメーションが終了するまでの間,重複してボタンを押す事はできないように工夫してある。


レイアウトXML:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
>

  <Button
    android:id="@+id/main_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="開始"
    android:layout_marginTop="0px" />

  <TextView
    android:id="@+id/tv1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="内容1"
    android:layout_marginTop="100px" />

  <TextView
    android:id="@+id/tv2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="内容2"
    android:layout_marginTop="200px" />

</RelativeLayout>
  • 移動アニメーション後に相対位置で各Viewの座標を設定したりするので,LinearLayoutではなくRelativeLayoutを使う。


このようなコードを動作させるための,ライブラリ・クラスを実装する。

このライブラリは,「アニメーションを逐次実行するためのDSL」を提供する。

アニメーションを逐次実行するためのランナークラス

このライブラリを使えば,利用側のコードでは,スレッドの並列性を全く意識しなくてすむ所がポイント。


以下は具体的なコード。

1動作の指定をするDescriptionクラス(Animationだけでなく,AnimationSetも記述できる):

package com.example;

import android.util.Log;
import android.view.View;
import android.view.animation.Animation;

/**
 * 順番に実行したいアニメーションの説明を記述。
 * @author id:language_and_engineering
 *
 */
public class AnimationDescription {

    // アニメーションの適用対象
    public View[] new_target_views = null;

    // 開始前の待機時間
    public int wait_before = 0;

    // アニメーションの持続時間
    public int anim_duration = 0;

    // 終了後の待機時間
    public int wait_after = 0;



    // --------- ユーザ定義用 -----------


    /**
     * 具体的なアニメーションを定義。
     * AnimationまたはAnimationSetを返すこと。
     */
    protected Animation describe() {
        // Override me
        return null;
    }


    /**
     * アニメーション後に各種属性を変更(setFillAfter()が効かない問題への対処)
     */
    protected void modifyAfterAnimation(View v) {
        // Override me
    }


    // --------- setter -----------


    /**
     * アニメーションの適用対象をセット
     */
    public AnimationDescription targetViews(View...views) {
        this.new_target_views = views;
        return this;
    }


    /**
     * アニメーション開始前の待機時間をセット
     */
    public AnimationDescription waitBefore(int milli_sec) {
        this.wait_before = milli_sec;
        return this;
    }


    /**
     * アニメーションのdurationをセット
     */
    public AnimationDescription duration(int milli_sec) {
        this.anim_duration = milli_sec;
        return this;
    }


    /**
     * アニメーション終了後の待機時間をセット
     */
    public AnimationDescription waitAfter(int milli_sec) {
        this.wait_after = milli_sec;
        return this;
    }


    // --------- 待機処理を実行 -----------


    /**
     * 事前待機処理
     */
    public void execWaitBefore()
    {
        if( wait_before > 0 )
        {
            Log.d("AnimTest", "事前待機処理を実行します。");
            sleepMS(wait_before);
        }
    }


    /**
     * 事後待機処理
     */
    public void execWaitAfter()
    {
        if( wait_after > 0 )
        {
            Log.d("AnimTest", "事後待機処理を実行します。");
            sleepMS(wait_after);
        }
    }


    /**
     * アニメーション実行中の待機処理
     */
    public void execWaitDuration() {
        Log.d("AnimTest", "duration分の待機処理を実行します。");
        sleepMS(anim_duration);
    }


    /**
     * 指定されたミリ秒だけスリープ
     */
    private void sleepMS(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException ignore) {
        }
    }
}


Descriptionを複数詰めて走らせるランナー:

package com.example;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.Activity;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;


/**
 * 複数のAnimationを順番に実行するためのランナー。
 * @author id:language_and_engineering
 *
 */
public class SequentialAnimationsRunner {

    // アニメーションを走らせる画面
    private Activity target_activity = null;

    // アニメーション詳細設定たち
    private List<AnimationDescription> descriptions = new ArrayList<AnimationDescription>();

    // 全終了後のリスナ
    private AnimationsFinishListener animationsFinishListener = null;

    // アニメーション適用対象Viewたち
    private ArrayList<View> current_target_views = new ArrayList<View>();

    // 現在取り扱い中の詳細設定のインデックス
    private int current_description_cursor = 0;

    // 1スレッドを使いまわすサービス
    private ExecutorService exService = null;

    // 全アニメーションを実行途中であるかどうか(簡易ロック用)
    private boolean executing_flag;


    // ------- 初期化処理 --------


    /**
     * 初期化
     */
    public SequentialAnimationsRunner(Activity activity)
    {
        this.target_activity = activity;
    }


    /**
     * アニメーション詳細設定たちを追加。
     * 可変長引数で何個でも可能。
     */
    public SequentialAnimationsRunner add( AnimationDescription...descs )
    {
        for( AnimationDescription desc : descs )
        {
            descriptions.add( desc );

            Log.d("AnimTest", descriptions.size() + "個目のdescriptionがaddされました。");
        }
        return this;
    }


    /**
     * アニメーション終了時の挙動を設定。
     */
    public SequentialAnimationsRunner onFinish(AnimationsFinishListener animationsFinishListener)
    {
        this.animationsFinishListener = animationsFinishListener;

        Log.d("AnimTest", "runnerにanimationsFinishListenerがセットされました。");
        return this;
    }


    // ------- 全Descriptionのスキャン処理 --------


    /**
     * 全アニメーションを開始する。
     */
    public void start()
    {
        // 開始済み?
        if( executionAlreadyStarted() )
        {
            // ランナーのインスタンス単位で排他する。
            Log.d("AnimTest", "このインスタンスのアニメーションは既に開始済みです。");
        }
        else
        {
            executing_flag = true;

            // 全部実行開始
            execAllDescriptions();
        }
    }


    /**
     * 登録された全詳細を実行開始
     */
    private void execAllDescriptions()
    {
        // NOTE: 1個以上の追加は前提とする

        // アニメーションはパフォーマンスを気にすべき処理なので
        // シングルスレッドを使いまわして毎回のスレッド生成のオーバーヘッドを省く
        exService = Executors.newSingleThreadExecutor();
            // @see http://www.techscore.com/tech/Java/JavaSE/Thread/7-2/
            // https://gist.github.com/1764033

        // カーソルを先頭にセット
        current_description_cursor = 0;
        Log.d("AnimTest", "最初の詳細を実行開始します。");

        // 開始
        execCurrentDescription();
    }


    /**
     * 現在のカーソルが指し示すアニメーション詳細設定を実行する。
     */
    private void execCurrentDescription()
    {
        // 現在のDescriptionを取得
        AnimationDescription desc = descriptions.get(current_description_cursor);

        // 現在のターゲットとなるViewを覚えさせる
        updateTargetsIfSpecified(desc);

        // 具体的なAnimationの指示が返されたか
        final Animation anim = desc.describe();
        if( anim != null )
        {
            Log.d("AnimTest", "実行すべきAnimationが返されました。");
            executeDescribedAnimation(desc, anim);
        }
        else
        {
            // 元スレッドに終了を通知
            Log.d("AnimTest", "実行すべきアニメーションは無かったため,ランナーに1ステップの終了を通知します。");
            onCurrentDescriptionFinished();
        }
    }


    // ------- 個別のアニメーション実行処理 --------


    /**
     * 1つのアニメーションまたはAnimationSetを実行
     */
    private void executeDescribedAnimation(
        final AnimationDescription desc,
        final Animation anim
    )
    {
        // スレッド生成のコストを省きつつ,別スレッドでアニメを開始。
        // NOTE: 別スレッドに分ける理由は待機処理などが入るから。
        exService.execute(new Runnable(){
            @Override
            public void run() {
                // 別スレッドでアニメーションを実行
                carryAnimationFlowOnOtherThread(desc, anim);
            }
        });
    }


    /**
     * 1アニメーション描画のメインの処理フロー。
     * 別スレッド上で実行される。
     */
    protected void carryAnimationFlowOnOtherThread(
        final AnimationDescription desc,
        final Animation anim
    )
    {
        // 事前待機処理を実行
        desc.execWaitBefore();

        // 全ターゲットViewでアニメ実行
        kickOneAnimationForAllTargetViews(desc, anim);

        // アニメ実行時間+事後待機時間の分だけ,このスレッドは待つ
        desc.execWaitDuration();
        desc.execWaitAfter();

        // 全ターゲットViewで事後処理を実行
        modifyAfterForAllTargetViews(desc);

        // 元スレッドに終了を通知
        onCurrentDescriptionFinished();
    }


    /**
     * 全ターゲットViewでアニメーションを実行
     */
    private void kickOneAnimationForAllTargetViews(
        AnimationDescription desc,
        final Animation anim
    )
    {
        // Animationにdurationをセット
        if( desc.anim_duration > 0 )
        {
            anim.setDuration( desc.anim_duration );
        }

        // NOTE: アニメーション前後で効果が続くようにしたい(連続実行を前提とするので)
        // しかし,下記のメソッドは機能しない。
        anim.setFillEnabled(true);
        //anim.setFillBefore(true);
        anim.setFillAfter(true);
            // @see http://graphics-geek.blogspot.jp/2011/08/mysterious-behavior-of-fillbefore.html
            // "When fillEnabled is true, the value of fillBefore will be taken into account"
        // アニメーション終了後の状態を確実に保つためには,終了タイミングで属性をアニメどおりに変化させるしかない。
            // @see http://www.androiddiscuss.com/1-android-discuss/75731.html
            // http://stackoverflow.com/questions/3345084/how-can-i-animate-a-view-in-android-and-have-it-stay-in-the-new-position-size


        // 個々のターゲットViewごとに,具体的なAnimationを実行開始
        for( final View v : current_target_views )
        {
            Log.d("AnimTest", "アニメーションの開始を登録します。");

            // UI上の処理なので,UIスレッドにゆだねる
            target_activity.runOnUiThread(new Runnable(){
                @Override
                public void run() {
                    Log.d("AnimTest", "アニメーションを開始します。");

                    // キック
                    v.startAnimation(anim);
                }
            });
        }
            // UIスレッドでキックしておいたアニメーションはこのまま放任しておく。
            // 終了のリスナなどもセットせず,このスレッドからはもう関知しない。
    }


    /**
     * 全ターゲットViewで事後処理を実行
     */
    private void modifyAfterForAllTargetViews(final AnimationDescription desc)
    {
        for( final View v : current_target_views )
        {
            // 属性変更が主なので,UIスレッドに頼む
            target_activity.runOnUiThread(new Runnable(){
                @Override
                public void run() {
                    // このViewに対する事後処理
                    desc.modifyAfterAnimation(v);
                }
            });
        }
    }


    // ------- 複数アニメーションの制御 --------


    /**
     * アニメーションの実行が開始しているかどうか
     */
    private boolean executionAlreadyStarted()
    {
        return executing_flag;
    }


    /**
     * 1つ分の詳細を扱い終わった際に呼ばれる。
     */
    protected void onCurrentDescriptionFinished()
    {
        Log.d("AnimTest", "1ステップのアニメーション実行完了時点として,ランナーに終了を通知します。");

        // これで全部終わりか判定
        if( allDescriptionsFinished() )
        {
            Log.d("AnimTest", "全詳細の扱いが終了しました。");

            // 全部実行終了
            target_activity.runOnUiThread(new Runnable(){
                @Override
                public void run() {
                    // 事後処理
                    Log.d("AnimTest", "全詳細の事後処理を開始します。");
                    afterAllExecuted();
                }
            });

            executing_flag = false;
        }
        else
        {
            // 次の詳細へ
            execNextDescription();
        }
    }


    /**
     * 全詳細を扱い終えたかどうか判定
     */
    private boolean allDescriptionsFinished()
    {
        return ( current_description_cursor == ( descriptions.size() - 1 ) );
    }


    /**
     * 次の詳細を実行する
     */
    private void execNextDescription()
    {
        // カーソルをインクリメント
        current_description_cursor ++;

        Log.d("AnimTest", "次の詳細へ進みます。現在のカーソルは" + current_description_cursor);

        // 開始
        execCurrentDescription();
    }


    /**
     * もし必要なら,詳細の指示通りに,アニメーション適用対象を変更する。
     */
    private void updateTargetsIfSpecified(AnimationDescription desc)
    {
        // ターゲットが変更されたか
        if( desc.new_target_views != null )
        {
            // まず全部クリア
            current_target_views.clear();

            // 1個ずつ登録しなおす
            for( View target_view : desc.new_target_views )
            {
                current_target_views.add( target_view );
            }

            Log.d("AnimTest", "ターゲットが変更されました。個数は" + current_target_views.size() );
        }
    }


    /**
     * 全アニメーション終了後の処理をUIスレッド上で実行
     */
    private void afterAllExecuted()
    {
        // 登録されていれば
        if( animationsFinishListener != null )
        {
            animationsFinishListener.exec();
        }
    }

}

プログラム中のコードに書いてある通り,アニメーション実行後に,Viewをそのままの描画状態に保つのが難しい。

setFillAfter() などのメソッドが,うまく機能しない場合があるのだ。


だから,ライブラリ利用側のコードで,アニメーション終了後に各Viewの属性値をセットし直している。

手動でsetFillAfter() してしのいでいるわけだ。

冒頭のコードでは,MarginLayoutParamsを設定することで,Viewの相対座標を変化させ,アニメーションによる「見かけ上の移動」の後の,「実質的な移動」を行なっている。

レイアウト(3)-ウィジェットのパディングとマージン
http://ichitcltk.hustle.ne.jp/gudon/m...

  • 「Androidでは、基本的にはウィジェットを配置する位置を座標で自由に指定する事ができない。」と述べたが、 マージンを設定する事で、結果的にウィジェットの表示位置を調整する事にもなる


なお,このランナークラスの挙動と設計をシーケンス図で詳しく知りたい場合,下記のエントリが参考になるだろう。

Javaの非同期処理を,シングルスレッドのようにシンプルにコーディングするための設計パターン (並列処理を逐次処理にする)
http://language-and-engineering.hatenablog.jp/entry/20120205/p1


簡単なリスナ:

package com.example;

/**
 * 全アニメーション終了後のリスナ。
 * @author id:language_and_engineering
 *
 */
public abstract class AnimationsFinishListener {

    /**
     * 全アニメーション終了後の処理。
     */
    abstract protected void exec();

}

以上で,ライブラリは終わり。これで動作する。


動作結果のログの出力:

22:42:19.613: DEBUG/AnimTest(8634): 1個目のdescriptionがaddされました。
22:42:19.613: DEBUG/AnimTest(8634): 2個目のdescriptionがaddされました。
22:42:19.613: DEBUG/AnimTest(8634): 3個目のdescriptionがaddされました。
22:42:19.613: DEBUG/AnimTest(8634): 4個目のdescriptionがaddされました。
22:42:19.613: DEBUG/AnimTest(8634): 5個目のdescriptionがaddされました。
22:42:19.613: DEBUG/AnimTest(8634): runnerにanimationsFinishListenerがセットされました。
22:42:23.096: DEBUG/AnimTest(8634): 開始ボタンが押されました。
22:42:23.096: DEBUG/AnimTest(8634): 最初の詳細を実行開始します。
22:42:23.096: DEBUG/AnimTest(8634): ターゲットが変更されました。個数は1
22:42:23.096: DEBUG/AnimTest(8634): 実行すべきアニメーションは無かったため,ランナーに1ステップの終了を通知します。
22:42:23.096: DEBUG/AnimTest(8634): 1ステップのアニメーション実行完了時点として,ランナーに終了を通知します。
22:42:23.096: DEBUG/AnimTest(8634): 次の詳細へ進みます。現在のカーソルは1
22:42:23.096: DEBUG/AnimTest(8634): 実行すべきAnimationが返されました。
22:42:23.106: DEBUG/AnimTest(8634): 事前待機処理を実行します。
22:42:24.117: DEBUG/AnimTest(8634): アニメーションの開始を登録します。
22:42:24.117: DEBUG/AnimTest(8634): duration分の待機処理を実行します。
22:42:24.117: DEBUG/AnimTest(8634): アニメーションを開始します。
22:42:26.119: DEBUG/AnimTest(8634): 事後待機処理を実行します。
22:42:27.120: DEBUG/AnimTest(8634): 1ステップのアニメーション実行完了時点として,ランナーに終了を通知します。
22:42:27.120: DEBUG/AnimTest(8634): 次の詳細へ進みます。現在のカーソルは2
22:42:27.120: DEBUG/AnimTest(8634): ターゲットが変更されました。個数は2
22:42:27.120: DEBUG/AnimTest(8634): 実行すべきアニメーションは無かったため,ランナーに1ステップの終了を通知します。
22:42:27.120: DEBUG/AnimTest(8634): 1ステップのアニメーション実行完了時点として,ランナーに終了を通知します。
22:42:27.120: DEBUG/AnimTest(8634): 次の詳細へ進みます。現在のカーソルは3
22:42:27.120: DEBUG/AnimTest(8634): 実行すべきAnimationが返されました。
22:42:27.120: DEBUG/AnimTest(8634): アニメーションの開始を登録します。
22:42:27.120: DEBUG/AnimTest(8634): アニメーションの開始を登録します。
22:42:27.120: DEBUG/AnimTest(8634): duration分の待機処理を実行します。
22:42:27.120: DEBUG/AnimTest(8634): アニメーションを開始します。
22:42:27.120: DEBUG/AnimTest(8634): アニメーションを開始します。
22:42:28.121: DEBUG/AnimTest(8634): 事後待機処理を実行します。
22:42:28.622: DEBUG/AnimTest(8634): 1ステップのアニメーション実行完了時点として,ランナーに終了を通知します。
22:42:28.622: DEBUG/AnimTest(8634): 次の詳細へ進みます。現在のカーソルは4
22:42:28.622: DEBUG/AnimTest(8634): 実行すべきAnimationが返されました。
22:42:28.622: DEBUG/AnimTest(8634): アニメーションの開始を登録します。
22:42:28.622: DEBUG/AnimTest(8634): アニメーションの開始を登録します。
22:42:28.622: DEBUG/AnimTest(8634): duration分の待機処理を実行します。
22:42:28.632: DEBUG/AnimTest(8634): アニメーションを開始します。
22:42:28.632: DEBUG/AnimTest(8634): アニメーションを開始します。
22:42:30.634: DEBUG/AnimTest(8634): 1ステップのアニメーション実行完了時点として,ランナーに終了を通知します。
22:42:30.634: DEBUG/AnimTest(8634): 全詳細の扱いが終了しました。
22:42:30.654: DEBUG/AnimTest(8634): 全詳細の事後処理を開始します。
22:42:30.654: DEBUG/AnimTest(8634): 全アニメーションが終了したため,画面遷移します。
22:42:30.654: INFO/ActivityManager(307): Starting: Intent { cmp=com.example/.AnimTestActivity } from pid 8634
22:42:30.734: DEBUG/AnimTest(8634): 1個目のdescriptionがaddされました。
22:42:30.734: DEBUG/AnimTest(8634): 2個目のdescriptionがaddされました。
22:42:30.734: DEBUG/AnimTest(8634): 3個目のdescriptionがaddされました。
22:42:30.734: DEBUG/AnimTest(8634): 4個目のdescriptionがaddされました。
22:42:30.734: DEBUG/AnimTest(8634): 5個目のdescriptionがaddされました。
22:42:30.734: DEBUG/AnimTest(8634): runnerにanimationsFinishListenerがセットされました。

各ラインの時刻に注目。

待機処理が開始してから,きっちり時間通りに次のステップが開始している。

サンプルコード2

もうちょっと複雑なアニメーションをさせてみる。

回転が絡むと,到着地点の制御が難しくなる。

package com.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.CycleInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.RotateAnimation;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class AnimTestActivity extends Activity implements OnClickListener {

    SequentialAnimationsRunner anim_runner;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        findViewById(R.id.main_btn).setOnClickListener(this);


        // アニメーションを定義


        TextView tv1 = (TextView)findViewById(R.id.tv1);
        TextView tv2 = (TextView)findViewById(R.id.tv2);
        final Activity activity = this;

        anim_runner = new SequentialAnimationsRunner(this)
            .add(

                // アニメーションのターゲットを1つ設定
                new AnimationDescription().targetViews( tv1 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // 拡大
                        Animation anim = new ScaleAnimation(
                            1.0f, 2.0f, // X方向に2倍に拡大する
                            1.0f, 1.0f, // Y方向はそのまま
                            Animation.RELATIVE_TO_SELF, 0.5f, // X方向の中心点を軸に拡大する
                            Animation.RELATIVE_TO_SELF, 0.0f
                        );

                        return anim;
                    }

                    @Override
                    protected void modifyAfterAnimation(View v)
                    {
                        // 横に2倍に引き延ばす
                        ((TextView)v).setTextScaleX( 2f );
                    }
                }.waitBefore(1000).duration( 2000 ).waitAfter( 1000 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // 小刻みに振動
                        Animation anim = new TranslateAnimation(
                            Animation.ABSOLUTE, -5f,
                            Animation.ABSOLUTE, 5f,
                            Animation.ABSOLUTE, 0f,
                            Animation.ABSOLUTE, 0f
                        );

                        Interpolator repeat_iptr = new CycleInterpolator(10);
                        anim.setInterpolator(repeat_iptr);

                        return anim;
                    }
                }.duration( 500 ).waitAfter( 1000 )
                ,

                // アニメーションのターゲットを変更
                new AnimationDescription().targetViews( tv2 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // 回転
                        Animation anim1 = new RotateAnimation(
                            0, 360, // 角度は一回転する
                            Animation.RELATIVE_TO_SELF, 0,
                            Animation.RELATIVE_TO_SELF, 0 // 回転の中心は自分の左上
                        );

                        // 右方向に移動
                        Animation anim2 = new TranslateAnimation(
                                Animation.ABSOLUTE, 0f,
                                Animation.ABSOLUTE, 150f,
                                Animation.ABSOLUTE, 0f,
                                Animation.ABSOLUTE, 0f
                            );

                        // これらのアニメーションを合成して同時進行させる
                        AnimationSet anim_set = new AnimationSet(true);
                        anim_set.addAnimation(anim1);
                        anim_set.addAnimation(anim2);
                            // NOTE:合成
                            // http://www.atmarkit.co.jp/fsmart/articles/android20/android20_2.html

                            // NOTE:回転の次に移動をsetしないと変な動きになる
                            // http://d.hatena.ne.jp/hs_hachi/20110810/1312967455
                            // http://blog.tenshodo.org/2009/08/resanimrotet.html

                        return anim_set;
                    }

                    @Override
                    protected void modifyAfterAnimation(View v)
                    {
                        // 右にずらす
                        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
                            RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT
                        );
                        lp.setMargins(150, 200, 0, 0);
                        v.setLayoutParams(lp);
                    }
                }.duration( 2000 )
                ,

                // アニメーションのターゲットを変更
                new AnimationDescription().targetViews( tv1, tv2 )
                ,

                new AnimationDescription(){
                    @Override
                    protected Animation describe()
                    {
                        // フェードアウト
                        Animation anim = new AlphaAnimation( 1f, 0f );

                        return anim;
                    }
                }.duration( 4000 ).waitAfter( 2000 )

            )
            .onFinish( new AnimationsFinishListener(){
                @Override
                protected void exec()
                {
                    Log.d("AnimTest", "全アニメーションが終了したため,画面遷移します。");

                    // 終わった後の処理。画面遷移とか
                    activity.startActivity(
                        new Intent( activity, AnimTestActivity.class )
                    );
                }
            })
        ;
        // この場ですぐにstart() で開始することもできるが,
        // 実行中のロック保持のため,いったんインスタンスを保持する。

    }


    @Override
    public void onClick(View v) {

        Log.d("AnimTest", "開始ボタンが押されました。");

        // アニメ開始
        anim_runner.start();

    }
}

何とか動く。でも,ちょっとカクカクとぶれる。


拡大と回転と移動なんかが組み合わさると,微調整がきつい。

移動+透過の組み合わせぐらいなら余裕で大丈夫なんだが。


Viewの属性値の制御が複雑な場合,方針を変えて,Canvasの利用を視野に入れて考え直した方がいいかもしれない。