Experiments Never Fail

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

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 が使えるライブラリ<RxJava でも可>を使えばマシにはなります)。

これが 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 と混ぜて使うことができるのがよいですね。

published at tags: Kotlin Android Java Xamarin C#