Neutral Scent このページをアンテナに追加 RSSフィード Twitter

2011-12-19

[][]アイテムがさささっとアニメーションするListBoxを作る

Windows Phone Advent Calendar 2011絶賛開催中。
これは19日目のエントリーです。

サンプルコードをダウンロード: AnimationListSample.zip 直

Windows Phoneといえば、Silverlightといえばアニメーションですよね。
で、アプリ作るなら、どうせやるならひらりひらりと華麗なアニメーションをキメたくなります。

今回のAdvent Calendarでもid:tmyt先生が紹介してくれたように、ページ単位のTransitionであればSilverlight Toolkitに出来合いのモノがありまして、それだけでも十分いい感じになるのですが、せっかくだからHomeタイルや標準のメールのように個々のアイテムまでささささっとアニメーションさせてみたい! ...というわけで、今回はアイテム単位でアニメーションするListBox派生クラスのサンプルを用意してみました。

このサンプルはコードが読みやすいようにできるだけシンプルな形に落とし込んであるので、あれやこれやと変更していろんなアニメーションにトライしてみてください。

基本的なコンセプトは単純で、ListBox継承クラスを一つ作成し、OnItemsChanged()をoverrideし、追加されてきたアイテム達に対して時間差をつけたStoryboardを片っ端から適用してやる、ただそれだけです。ね、簡単でしょ?

ただし、一口にListBoxのアイテム追加といってもその方法・状況は様々。アイテムたちは、XAMLで記述される場合もあれば、Add()されたり、Bindingされたり、OnItemsChanged()もまとめて呼ばれて来たりバラバラに叩かれたり...。

なので、Bindingの場合はまずその実体のコントロールを取得し、

FrameworkElement elm = item as FrameworkElement;
if (elm == null)
    elm = base.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;

一旦バッファとなるList<FrameworkElement>に溜めながらカウントしてStoryboardの時差を付ける処理を行っています。

private void AppendToList()
{
    for (int i = 0; i < appendingItems.Count; i++)
    {
        var item = appendingItems[i];
        AnimateItem(item, i);
    }
    appendingItems.Clear();
}

StoryboardなんてふつうBlendでしょ? C#のコードでStoryboardなんてやったことないし超面倒そう! と思います? その通り! 面倒だけど、そんなに複雑でなければ単にプロパティを付けてくだけですよ。

Storyboard storyboard = new Storyboard();
...
element.RenderTransform = new CompositeTransform();
DoubleAnimation slideAni = new DoubleAnimation();
slideAni.From = -100.0;
slideAni.To = 0;
slideAni.Duration = duration;
slideAni.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseIn, Exponent = 5.0 };
Storyboard.SetTarget(slideAni, element);
Storyboard.SetTargetProperty(slideAni, new PropertyPath("UIElement.RenderTransform.TranslateX"));
storyboard.Children.Add(slideAni);

double delay = itemInterval * delayCount;
storyboard.BeginTime = new TimeSpan?(TimeSpan.FromSeconds(delay));
storyboard.Begin();

まぁ、これはサンプルなので、実際にはリソースなどで定義済みのアニメーションを引っ張ってくるという手もあるでしょう。
このサンプルは単純にRenderTransform.TranslateXを使っていますが、このあたりを変えるだけで色々とバリエーションを作るのは簡単です。

というわけで、クラス全体の実装は以下のような感じになっています。

public class AnimationListBox : ListBox
{
    private List<FrameworkElement> appendingItems = new List<FrameworkElement>();
    private Duration duration = new Duration(TimeSpan.FromSeconds(1.0));
    private double itemInterval = 0.2;

    public AnimationListBox()
    {
        this.CacheMode = new BitmapCache();
    }

    protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (e.Action == NotifyCollectionChangedAction.Reset)
        {
            appendingItems.Clear();
        }
        else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        {
            foreach (var item in e.NewItems)
            {
                this.UpdateLayout();
                FrameworkElement elm = item as FrameworkElement;
                if (elm == null)
                    elm = base.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
                if (elm != null)
                {
                    elm.Opacity = 0.0;
                    appendingItems.Add(elm);
                    Dispatcher.BeginInvoke(() => { AppendToList(); });
                }
            }
        }
    }

    private void AppendToList()
    {
        for (int i = 0; i < appendingItems.Count; i++)
        {
            var item = appendingItems[i];
            AnimateItem(item, i);
        }
        appendingItems.Clear();
    }

    private void AnimateItem(FrameworkElement element, int delayCount)
    {
        Storyboard storyboard = new Storyboard();

        DoubleAnimation opaAni = new DoubleAnimation();
        opaAni.From = 0.0;
        opaAni.To = 1.0;
        opaAni.Duration = duration;
        opaAni.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseIn, Exponent = 10.0 };
        Storyboard.SetTarget(opaAni, element);
        Storyboard.SetTargetProperty(opaAni, new PropertyPath(UIElement.OpacityProperty));
        element.Opacity = 0;
        storyboard.Children.Add(opaAni);

        element.RenderTransform = new CompositeTransform();
        DoubleAnimation slideAni = new DoubleAnimation();
        slideAni.From = -100.0;
        slideAni.To = 0;
        slideAni.Duration = duration;
        slideAni.EasingFunction = new ExponentialEase() { EasingMode = EasingMode.EaseIn, Exponent = 5.0 };
        Storyboard.SetTarget(slideAni, element);
        Storyboard.SetTargetProperty(slideAni, new PropertyPath("UIElement.RenderTransform.TranslateX"));
        storyboard.Children.Add(slideAni);

        double delay = itemInterval * delayCount;
        storyboard.BeginTime = new TimeSpan?(TimeSpan.FromSeconds(delay));
        storyboard.Begin();
    }
}

UserControlなどでなくListBox派生クラスなので使い方も簡単! ふつうのListBoxと同様にツールボックスから挿入するか、<ListBox>を<my:AnimationListBox>などに置き換えるだけです。(名前空間の参照を忘れずに)
DataBindingでも基本動きますが、コレクションをまとめてどんとBindすると、Windows PhoneではOnItemsChanged()を呼んでくれないので、このサンプルではアニメーションされません。このままで使うなら、コレクションを追加してからコレクションにAdd()してやってください。

<my:AnimationListBox>
    <TextBlock Text="List Item 1"/>
...
</my:AnimationListBox>

基本的に、これは一つのやり方なので他にも様々なアプローチがあると思います。
また、Storyboard全部作りっぱな、流しっぱなかよ、というツッコミも有りで、このあたりは気持ち悪ければstoryboard.CompletedをハンドルしてStop()などを後処理したりしてみてください。まぁ延々と追加と削除を繰り返したりしなければいいかな...、などと思い今回は削ってあります。

というわけで、みなさま、よいクリスマスを!

プログラミングWindows Phone (MSDNプログラミングシリーズ)

プログラミングWindows Phone (MSDNプログラミングシリーズ)

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

トラックバック - http://d.hatena.ne.jp/kaorun/20111219/1324288358