連続したアニメーションをつなぐために、await/async を使う

連続したアニメーションをつなぐために、Completed イベントでつなげる | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/4071

の続きで、storyboard の Completed イベントで連続させるのではなくて、async/await を使ってつなげてみます。

■開始 Image と 終了 Image を渡してアニメする関数を作る

モーダルダイアログを作ったときと同じように、Completed イベントの発生待ち(アニメーションの完了待ち)をします。Task.Delay() をループさせて待つので、ダサいと言えばダサんですが。他によい方法があったら、また検討するということで。
Task.Run に渡すラムダ式に async がついているという不思議なコードですが、これで動くのだからたいしたものです。WinRT の場合には Thread.Sleep がなくて、非同期の Task.Delay を使うためにこんな風になっています。このあたり、Sleep 相当のブロッキング用のタイマーを自作すればよいのか?ちょっと思案中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// アニメーションする関数
/// </summary>
/// <param name="p1"></param>
/// <param name="p2"></param>
/// <returns></returns>
private async Task GoAnime( Image p1, Image p2 )
{
    SetMovePos(p1, p2);
    bool _complete = false;
    this.sbMove.Begin();
    this.sbMove.Completed += (s, e) => { _complete = true; };
    await Task.Run(async () =>
    {
        while (_complete == false)
        {
            await Task.Delay(100);
        }
    });
}

■アニメーションを羅列させる

アニメーションさせる GoAnime 関数ができたので、await を付けて羅列させてみます。Completed イベントで書くよりも、ちょっとは状態遷移がみやいかな、と。yeild return の場合はイテレーターを使う必要があって、ちょっとトリッキーな感じがするのですが、これだと自然かと。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 札をクリックして連続アニメーションを開始
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void pictAniClick(object sender, TappedRoutedEventArgs e)
{
    if (_moving == false)
    {
        _moving = true;
        await GoAnime(this.pict1, this.pict2);
        await GoAnime(this.pict2, this.pict3);
        await GoAnime(this.pict3, this.pict4);
        await GoAnime(this.pict4, this.pict1);
    }
    else
    {
        this.sbMove.Stop();
        _moving = false;
    }
}
bool _moving = false;

■汎用的にクラスのメソッドにしてみる

GoAnime メソッドは、画面の内部メソッドなので汎用性がありません。なので、クラスとして括りだすのが良かろうってことで、クラス化してみたのが、これです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class AnimeWaitable
{
    public Storyboard sb { get; set; }
    public EasingDoubleKeyFrame startPosX { get; set; }
    public EasingDoubleKeyFrame startPosY { get; set; }
    public EasingDoubleKeyFrame endPosX { get; set; }
    public EasingDoubleKeyFrame endPosY { get; set; }
    public Image pictAni { get; set; }
    public Image pictStart { get; set; }
    public Image pictEnd { get; set; }
 
    public bool IsCompleted { get; set; }
    public int Result { get; set; }
    public AnimeWaitable()
    {
    }
    public async Task<int> RunAsync(Image pictS, Image pictE)
    {
        this.pictStart = pictS;
        this.pictEnd = pictE;
 
        var ptS = this.pictStart.TransformToVisual(null).TransformPoint(new Point(0, 0));
        var ptE = this.pictEnd.TransformToVisual(null).TransformPoint(new Point(0, 0));
        this.startPosX.Value = ptS.X;
        this.startPosY.Value = ptS.Y;
        this.endPosX.Value = ptE.X;
        this.endPosY.Value = ptE.Y;
        sb.Completed += (s, e) =>
        {
            this.IsCompleted = true;
            this.Result = 1;
        };
        this.IsCompleted = false;
 
        this.sb.Begin();
        // アニメーションの完了待ち
        while (this.IsCompleted == false)
        {
            await Task.Delay(100);
        }
        return this.Result;
    }
    public void Stop()
    {
        this.sb.Stop();
        this.IsCompleted = true;
        this.Result = 0;
    }
}

storyboard に座標を設定していた SetMovePos メソッドをクラス内に展開しているので、冗長になっていますが、基本は GoAnime メソッドと同じで、Task.Delay() を使ってアニメーションの完了待ちをしています。
クラス名が AnimeWaitable なのは、awaitable パターンにすればよいのでは?って時の名残りです。最初 awaitable パターンを使おうと思ったのですが、そうする必要がなかったってことですね。awaitable については別途書こうと思います。

アニメーションを連続させるところは、次のように await ani.RunAsync を羅列していきます。ここでは、ぐるぐると回るように List を使っていますが。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
async void GoAnime()
{
    var ani = new AnimeWaitable();
    ani.sb = this.sbMove;
    ani.startPosX = this.sbStartX;
    ani.startPosY = this.sbStartY;
    ani.endPosX = this.sbEndX;
    ani.endPosY = this.sbEndY;
    ani.pictAni = this.pictAni;
    ani.pictStart = this.pict1;
    ani.pictEnd = this.pict2;
 
    var lstS = new List<Image>();
    var lstE = new List<Image>();
    lstS.Add(pict1); lstS.Add(pict2); lstS.Add(pict3); lstS.Add(pict4);
    lstE.Add(pict2); lstE.Add(pict3); lstE.Add(pict4); lstE.Add(pict1);
    this.textMsg.Text = "開始...";
    int i = 0;
    _go = true;
    _ani = ani;
    while (_go)
    {
        await ani.RunAsync(lstS[i], lstE[i]);
        i = ++i % 4;
        /* このように書き並べることができる
        await ani.RunAsync(pict1, pict2);
        await ani.RunAsync(pict2, pict3);
        await ani.RunAsync(pict3, pict4);
        await ani.RunAsync(pict4, pict1);
        */
    }
    this.textMsg.Text = "終了...";
    _go = false;
 
}
 
bool _go = false;
AnimeWaitable _ani;
 
private void StartClick(object sender, RoutedEventArgs e)
{
    if (_go == false)
    {
        GoAnime();
    }
    else
    {
        _ani.Stop();
        _go = false;
    }
}

ここでちょっと奇妙なのは、GoAnime メソッドの中で、while ループでぐるぐるしている間でも StartClick イベントが発生します。ええ、Start ボタンを押すことができます。一見すると while ループで画面が固まるのでは?と思えるのですが、ani.RunAsync は非同期メソッドなので、途中でボタンクリックイベントなどを入れられるんですよね。このあたりが、非同期な書き方 async/await の面白いところです。

逆にいえば、シーケンス図としてどう書き表すのか、というのが難しいところです。RunAsync という名前を付けてあるので非ブロッキングということがわかるので、手続きっぽい書き方をしても UI の各種イベントを阻害しないという不思議な≒ある意味で直感的な、書き方ができるかと。

カテゴリー: C#, WinRT パーマリンク