かずきのBlog@hatena

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

UWP の画面遷移でいい感じのアニメーションをさせよう

Connected Animation というものを使うと出来ます。

ConnectedAnimationService.GetForCurrentView() で取得した ConnectedAnimationService に対して画面遷移前と画面遷移後で対応するコントロールの紐づけをしてやる感じです。なので、画面遷移前に画面遷移前のページでアニメーションさせたいコントロールを登録して、画面遷移先でもアニメーションさせたいコントロールを指示するという処理が必要になります。

こんなイメージです。

// 画面遷移前
var a = ConnectedAnimationService.GetForCurrentView();
a.PrepareToAnimate("text", textBlock);
a.PrepareToAnimate("button", button);

Frame.Navigate(typeof(NextPage));


// 画面遷移先の OnNavigatedTo メソッドにて
var a = ConnectedAnimationService.GetForCurrentView();
a.GetAnimation("text")?.TryStart(textBlock);
a.GetAnimation("button")?.TryStart(button);

PrepareToAnimate や TryStart に渡してるのがアニメーションさせたいコントロールです。こんな感じで動きます。

f:id:okazuki:20180211233700g:plain

ListView の要素でアニメーション

普通は ListView タップしたら詳細画面に行くってパターンが多いですよね。そのときしゅっとアニメーションしたらかっこいい…!やってみよう。

データの入れ物作ります。

public class Person
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Name { get; set; }
}

とりあえずお試しなので App クラスあたりにグローバルでリスト持たせておきましょう。

public static Person[] People { get; } = Enumerable.Range(1, 100)
    .Select(x => new Person
    {
        Name = $"tanaka-{x}",
    })
    .ToArray();

MainPage.xaml で以下のようにバインドします。

<Page x:Class="App14.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App14"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView x:Name="listView">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:Person">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>

                        <TextBlock x:Name="textBlockName"
                                   Text="{x:Bind Name}"
                                   Style="{ThemeResource BodyTextBlockStyle}" />
                        <Border x:Name="border"
                                Background="Red"
                                Width="50"
                                Height="50"
                                Margin="5"
                                Grid.Column="1" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>

コードビハインドでデータを ListView に追加しておきます。

using Windows.UI.Xaml.Controls;

// 空白ページの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x411 を参照してください

namespace App14
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            listView.ItemsSource = App.People;
        }
    }
}

ListView を押したら画面遷移するようにします。ItemClick イベントを拾って

<ListView x:Name="listView"
          IsItemClickEnabled="True"
          SelectionMode="None"
          ItemClick="listView_ItemClick">

ListView の PrepareConnectedAnimation で下準備して画面遷移します。

private void listView_ItemClick(object sender, ItemClickEventArgs e)
{
    listView.PrepareConnectedAnimation("text", e.ClickedItem, "textBlockName");
    listView.PrepareConnectedAnimation("border", e.ClickedItem, "border");
    Frame.Navigate(typeof(NextPage), ((Person)e.ClickedItem).Id);
}

遷移先のページを作ります。

<Page x:Class="App14.NextPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App14"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel VerticalAlignment="Center"
                    HorizontalAlignment="Center">
            <TextBlock x:Name="textBlock"
                       Style="{ThemeResource HeaderTextBlockStyle}" />
            <Border x:Name="border"
                    Width="100"
                    Height="100"
                    Background="Red" />
        </StackPanel>
        
        <Button x:Name="button"
                Content="Back"
                Click="Button_Click" />
    </Grid>
</Page>

OnNavigatedTo メソッドでアニメーションの紐づけを行います。

private Person _target;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    _target = App.People.First(x => x.Id == (string)e.Parameter);
    textBlock.Text = _target.Name;

    var a = ConnectedAnimationService.GetForCurrentView();
    a.GetAnimation("text")?.TryStart(textBlock);
    a.GetAnimation("border")?.TryStart(border);
}

戻る方向もアニメーションをつけます。遷移先のページから戻る前にアニメーションの下準備をして…

private void Button_Click(object sender, RoutedEventArgs e)
{
    var a = ConnectedAnimationService.GetForCurrentView();
    a.PrepareToAnimate("text", textBlock);
    a.PrepareToAnimate("border", border);
    Frame.Navigate(typeof(MainPage), _target.Id);
}

戻った先でアニメーションを開始します。

<Page x:Class="App14.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App14"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView x:Name="listView"
                  IsItemClickEnabled="True"
                  SelectionMode="None"
                  ItemClick="listView_ItemClick"
                  Loaded="listView_Loaded">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:Person">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>

                        <TextBlock x:Name="textBlockName"
                                   Text="{x:Bind Name}"
                                   Style="{ThemeResource BodyTextBlockStyle}" />
                        <Border x:Name="border"
                                Background="Red"
                                Width="50"
                                Height="50"
                                Margin="5"
                                Grid.Column="1" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>
using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Navigation;

// 空白ページの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x411 を参照してください

namespace App14
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            listView.ItemsSource = App.People;
        }

        private Person _scrollTarget;

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            if (e.Parameter is string id)
            {
                _scrollTarget = App.People.FirstOrDefault(x => x.Id == id);
            }
        }

        private void listView_ItemClick(object sender, ItemClickEventArgs e)
        {
            listView.PrepareConnectedAnimation("text", e.ClickedItem, "textBlockName");
            listView.PrepareConnectedAnimation("border", e.ClickedItem, "border");
            Frame.Navigate(typeof(NextPage), ((Person)e.ClickedItem).Id);
        }

        private async void listView_Loaded(object sender, RoutedEventArgs e)
        {
            if (_scrollTarget == null)
            {
                return;
            }

            listView.ScrollIntoView(_scrollTarget);
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
            {
                var a = ConnectedAnimationService.GetForCurrentView();
                await Task.WhenAll(
                    listView.TryStartConnectedAnimationAsync(a.GetAnimation("text"), _scrollTarget, "textBlockName").AsTask(),
                    listView.TryStartConnectedAnimationAsync(a.GetAnimation("border"), _scrollTarget, "border").AsTask());
            });
        }
    }
}

ポイントは ListView の Loaded イベントでアニメーションをしていることです。 OnNavigatedTo だとタイミングが早すぎるのか例外が出てしまいました。

そして、ListView で表示対象のコントロールが出てくるまでスクロールしてアニメーションをしています。アニメーションは、Dispatcher を使って少し遅らせてやってます。これをしないと自分のところではアニメーションしてくれなかった…。

ドキュメントではそんなことしてないんだけど。謎い。

docs.microsoft.com

実行するとこんな感じです。

f:id:okazuki:20180212001639g:plain