かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ListBoxに追加されたアイテムを一回だけアニメーションさせたい

というプログラムのリクエストを受けたので作ってみました。とりあえず、響きだけ見てみると簡単そうなのですが、これが意外に厄介。克服しなければならない課題がいくつかあります。
課題は主にListBoxのコレクションの仮想化に起因する問題になります。

  1. Loadedイベントでアニメーション(VSMの切替やStoryboardのキック)するとListBoxItem再利用時にアニメーションが実行されない
  2. ListBoxのItemTemplateをUserControlに切り出してUserControlのDataContextの変更を監視して、アニメーションすると、新規追加じゃないアイテムでListBoxItemの再利用が行われるとアニメーションしてしまう

ということでやってみよう

基本的なアプローチはListBoxのItemTemplateをUserControlに切り出してUserControlのDataContextの変更を監視する方法にします。再利用時に行われるアニメーションへの対応は、悲しいけどViewModelにフラグを持たせる方法で今回は対応しました。

ViewModelの定義

ViewModelといっても、今回のケースでは単に値の入れ物です。

public class Item
{
    public bool Animated { get; set; }
    private DateTime v = DateTime.Now;

    public override string ToString()
    {
        return v.ToString();
    }
}

ポイントはAnimatedプロパティで、こいつがアニメーションされたかどうかのフラグになります。

ListBoxの定義

画面にListBoxを設置します。ListBoxは、ItemTemplateを追加しているだけで、特に目新しいことはしていません。その他のプロパティは単純に今回作った画面のレイアウトの都合で追加されたプロパティです。

<ListBox 
    x:Name="listBox" 
    HorizontalAlignment="Left" 
    Height="553" 
    Margin="120,65,0,0" 
    Grid.Row="1" 
    VerticalAlignment="Top" Width="1236">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <local:ListBoxItemControl />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

ListBoxItemControl(自作UserControlの定義)

次に自作のUserControlを定義します。このポイントはNormalとAddedとAnimatedという3つのVisualStateを定義して、それぞれアニメーション前、アニメーション、アニメーション後という状態を定義している点です。

<UserControl
    x:Class="AddItemAnimation.ListBoxItemControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AddItemAnimation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <StackPanel Orientation="Horizontal">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="Added">
                    <Storyboard>
                        <ColorAnimation Duration="0:0:5" To="Red" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle" d:IsOptimized="True"/>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="Animated">
                    <Storyboard>
                        <ColorAnimation Duration="0:0:0" To="Red" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle" d:IsOptimized="True"/>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <TextBlock MinWidth="50" Text="{Binding}" Margin="10,10,10,0" />
        <Rectangle x:Name="rectangle" Fill="Blue" Stroke="Black" Width="40" Height="40" />
    </StackPanel>
</UserControl>

Rectangleの色をBlueからRedにアニメーションするようにしています。

次に、コードビハインドです。コードビハインドではDataContextの変更を監視して、変更があった時の新しい値のAnimatedプロパティを確認してVisualStateを変更しています。DataContextの変更の監視は、WinRTにDataContextChagnedイベントが見当たらなかったために、MyDataContextという依存プロパティを定義して、それとDataContextをバインドしています。MyDataContextにプロパティ変更のコールバックを登録して、間接的にDataContextの変更を監視しています。

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;

namespace AddItemAnimation
{
    public sealed partial class ListBoxItemControl : UserControl
    {
        // DataContextを間接的に監視するための添付プロパティ
        private object MyDataContext
        {
            get { return (object)GetValue(MyDataContextProperty); }
            set { SetValue(MyDataContextProperty, value); }
        }

        private static readonly DependencyProperty MyDataContextProperty =
            DependencyProperty.Register(
                "MyDataContext", 
                typeof(object), 
                typeof(ListBoxItemControl), 
                new PropertyMetadata(null, (s, e) =>
                {
                    ((ListBoxItemControl)s).Changed(e);
                }));

        public ListBoxItemControl()
        {
            this.InitializeComponent();
            // 自分で定義した添付プロパティとDataContextをバインド
            this.SetBinding(MyDataContextProperty, new Binding());
        }

        // DataContext変更時の処理
        private void Changed(DependencyPropertyChangedEventArgs e)
        {
            // 新しいDataContextのAnimatedプロパティを見て状態を切り替える
            var item = e.NewValue as Item;
            if (item.Animated)
            {
                // アニメーション完了状態
                VisualStateManager.GoToState(this, "Animated", false);
                return;
            }

            // アニメーション前のまっさらな状態
            item.Animated = true;
            VisualStateManager.GoToState(this, "Normal", false);
            VisualStateManager.GoToState(this, "Added", false);
        }
    }
}

以上で完成です。これでなんとなく追加されたアイテムをアニメーションさせることができます。なんとなくというのは、UIの仮想化の関係でユーザが激しくスクロールしたりしたときにアイテムの追加がされたら、微妙にアニメーションされないでアニメーション完了状態にいってしまう要素がたまにあるかもしれません・・・。

もっといい実装方法知ってる人いたら教えてください・・・!