31
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Androidでアニメーションを連続で実行するのが面倒なのを Kotlin で便利にした話

Last updated at Posted at 2017-07-24

Android で「浮いてるように見える」アニメーションを実装する機会がありまして。次の画像のようなものなんですが。

このアニメーションは、

  1. 2秒かけて上へ少し移動する
  2. 2秒かけて下へ少し移動する

を「連続で」「繰り返し」実行させることで実現しています。
「連続で」とは、 1. のアニメーションが終わったら 2. のアニメーションを開始する、という意味です。

Java-Android では…

これを Android の View のアニメーションAPI で実現すると、普通にひどいコードになります。次がそれ。

// 2秒かけて上へ移動するアニメーション
final TranslateAnimation anim1 = new TranslateAnimation(
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF, -0.1f);
anim1.setDuration(2000);

// 2秒かけて下へ移動するアニメーション
final TranslateAnimation anim2 = new TranslateAnimation(
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF, -0.1f,
        Animation.RELATIVE_TO_SELF,  0.0f);
anim2.setDuration(2000);

anim1.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) { }

    @Override
    public void onAnimationEnd(Animation animation) {
        anim2.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) { }

            @Override
            public void onAnimationEnd(Animation animation) {
                // 3. 下へのアニメーションが終わったら、上へ移動するアニメーションをまた開始
                view.startAnimation(anim1);
            }

            @Override
            public void onAnimationRepeat(Animation animation) { }
        });

        // 2. 上へのアニメーションが終わったら、下へ移動するアニメーションを開始
        view.startAnimation(anim2);
    }

    @Override
    public void onAnimationRepeat(Animation animation) { }
});

// 1. 上へ移動するアニメーションを開始
view.startAnimation(anim1);

コールバックのネストに、行いたい処理とコードの記述順が逆という二重苦、これはやってられません。

これだけで Kotlin を使いたい案件です(Java でも Deferred が使えるライブラリを使えばマシにはなります)。

(Kotlin つかうまでもなかった…)

@glayash さんがコメントして下さったように、今回のようなアニメーションの繰り返しなら、次のように書けるので、コールバック地獄にならないし、Kotlin 使うまでもありませんでした。

// 2秒かけて上へ移動するアニメーション
final TranslateAnimation anim1 = new TranslateAnimation(
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF,  0.0f,
        Animation.RELATIVE_TO_SELF, -0.1f);
anim1.setDuration(2000);
// 反転させて無限にくりかえす
anim1.setRepeatMode(Animation.REVERSE);
anim1.setRepeatCount(Animation.INFINITE);
view.startAnimation(anim1);

これが Kotlin だと…

というわけで Kotlin でやってみました。

まず、「アニメーションを実行して、アニメーションが終わったら次へ継続する関数」を作成します。
ここでは View の拡張関数として定義してみました。

package net.amay077.animsample

import android.view.View
import android.view.animation.Animation
import kotlin.coroutines.experimental.suspendCoroutine

suspend fun View.startAnimationAsync(anim: Animation) {

    return suspendCoroutine { continuation ->
        anim.setAnimationListener(object : Animation.AnimationListener {
            override fun onAnimationStart(animation: Animation?) { }

            override fun onAnimationEnd(animation: Animation?) {
                continuation.resume(Unit)
            }

            override fun onAnimationRepeat(animation: Animation?) { }
        })

        this.startAnimation(anim)
    }
}

呼び出し側は次のような感じ。
コールバック地獄の Java に比べて天国かよここは…。
アニメーションはUIスレッドから呼び出す必要があるので async() { } ではなく launch(UI) { } を使う必要があるようです。

val button1 = findViewById(R.id.button1)

val anim1 = TranslateAnimation(
        Animation.RELATIVE_TO_SELF, 0.0f,
        Animation.RELATIVE_TO_SELF, 0.0f,
        Animation.RELATIVE_TO_SELF, 0.0f,
        Animation.RELATIVE_TO_SELF, -0.5f)
anim1.duration = 2000

val anim2 = TranslateAnimation(
        Animation.RELATIVE_TO_SELF, 0.0f,
        Animation.RELATIVE_TO_SELF, 0.0f,
        Animation.RELATIVE_TO_SELF, -0.5f,
        Animation.RELATIVE_TO_SELF, 0.0f)
anim2.duration = 2000

launch(UI) { // メインスレッドから async するよ
    // ずっとくりかえし
    while (true) {
        button1.startAnimationAsync(anim1) // 1. 2秒かけて上へ移動するアニメーションを実行
        button1.startAnimationAsync(anim2) // 2. 2秒かけて下へ移動するアニメーションを実行
    }
}

Kotlin をまともに使うのが初めてなのでまだ改善できるかも。。
よいコードがありましたらご指摘ください。

※Kotlin の coroutine(async/await) は 2017年7月現在、正式リリースされていません(experimental 版です)。

Kotlin での実装には、次のサイトを参考にさせていただきました

ちなみに C# でもできます

C#(つまり Xamarin.Android)でも async/await(つまり Task)TaskCompletionSource を組み合わせて実現できます。

C# にも拡張メソッドがあり、次のように定義することができます。

public static class ViewAnimationExtensions
{
    public static Task<bool> StartAnimationAsync(this View view, Animation anim)
    {
        var source = new TaskCompletionSource<bool>();
        EventHandler<Animation.AnimationEndEventArgs> handler = null;

        handler = (sender, e) =>
        {
            anim.AnimationEnd -= handler; // 購読解除を忘れずに
            source.SetResult(true); // kotlin の continuation.resume(Unit) にあたるトコ
        };
        anim.AnimationEnd += handler; // イベントを購読

        view.StartAnimation(anim);
        return source.Task;
    }
}

よびだし側はこう。
呼び出し時に await キーワードをつけ、それが含まれるメソッド(ここでは OnCreate)に async キーワードをつけます。

protected async override void OnCreate(Bundle savedInstanceState)
{
    /* 省略 */

    while (true)
    {
        await button1.StartAnimationAsync(anim1);
        await button1.StartAnimationAsync(anim2);
    }
}

Kotlin は同一プロジェクト内に Java と混ぜて使うことができるのがよいですね。

31
34
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?