Android で「浮いてるように見える」アニメーションを実装する機会がありまして。次の画像のようなものなんですが。
このアニメーションは、
を「連続で」「繰り返し」実行させることで実現しています。 「連続で」とは、 1. のアニメーションが終わったら 2. のアニメーションを開始する、という意味です。
これを 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 でやってみました。
まず、「アニメーションを実行して、アニメーションが終わったら次へ継続する関数」を作成します。
ここでは 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#(つまり 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 と混ぜて使うことができるのがよいですね。