かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

左右にスワイプするとにょきっとメニューが生えてくるものをUWPで(WP8.1でもWinStoreAppでも同じ)

こんな感じのUserControlを用意します。

<UserControl x:Class="App26.SwipeControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:App26"
             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"
             ManipulationMode="TranslateX,System">
    <Grid x:Name="Root" Background="White">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualState x:Name="NormalVisualState">
                    <Storyboard>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="0"
                                         Storyboard.TargetProperty="(FrameworkElement.Width)"
                                         Storyboard.TargetName="Left"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <CircleEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="0"
                                         Storyboard.TargetProperty="(FrameworkElement.Width)"
                                         Storyboard.TargetName="Right"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <CircleEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="LeftVisualState">
                    <Storyboard>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="100"
                                         Storyboard.TargetProperty="(FrameworkElement.Width)"
                                         Storyboard.TargetName="Left"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <CircleEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="0"
                                         Storyboard.TargetProperty="(FrameworkElement.Width)"
                                         Storyboard.TargetName="Right"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <CircleEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="RightVisualState">
                    <Storyboard>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="0"
                                         Storyboard.TargetProperty="(FrameworkElement.Width)"
                                         Storyboard.TargetName="Left"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <CircleEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                        <DoubleAnimation Duration="0:0:0.5"
                                         To="100"
                                         Storyboard.TargetProperty="(FrameworkElement.Width)"
                                         Storyboard.TargetName="Right"
                                         EnableDependentAnimation="True">
                            <DoubleAnimation.EasingFunction>
                                <CircleEase EasingMode="EaseOut" />
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Rectangle x:Name="Left"
                   Grid.Column="0"
                   Fill="Red"
                   Width="0">
        </Rectangle>
        <Rectangle x:Name="Right"
                   Grid.Column="2"
                   Fill="Green"
                   Width="0">
        </Rectangle>
        <TextBlock Grid.Column="1"
                   Text="Hello world"
                   Style="{ThemeResource BodyTextBlockStyle}" 
                   Foreground="Black"/>
    </Grid>
</UserControl>

アニメーションの定義が長いけど、幅を100にしたり0にしたり状態に応じて調整してるだけです。DoubleAnimationだけなので、よく見るとシンプル。UserControlのContentには左右のメニューがわりにRectangleを置いています。

あとは、コードビハインドでさくっとManipulationDeltaイベントを処理してやるだけ。

using Reactive.Bindings.Extensions;
using System;
using System.Linq;
using System.Reactive.Linq;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

// ユーザー コントロールのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234236 を参照してください

namespace App26
{
    public sealed partial class SwipeControl : UserControl
    {
        private const string NormalState = "NormalVisualState";
        private const string LeftState = "LeftVisualState";
        private const string RightState = "RightVisualState";

        public SwipeControl()
        {
            this.InitializeComponent();

            VisualStateManager.GoToState(this, NormalState, true);
            Observable.FromEvent<ManipulationDeltaEventHandler, ManipulationDeltaRoutedEventArgs>(
                x => (_, e) => x(e),
                x => this.ManipulationDelta += x,
                x => this.ManipulationDelta -= x)
                .Where(x => Math.Abs(x.Delta.Translation.X) >= 10)
                .Throttle(TimeSpan.FromMilliseconds(100))
                .ObserveOnUIDispatcher()
                .Subscribe(this.SwipeExecute);
        }

        private void SwipeExecute(ManipulationDeltaRoutedEventArgs e)
        {
            var currentState = VisualStateManager.GetVisualStateGroups(this.Root)
                .FirstOrDefault(x => x.Name == "VisualStateGroup")
                ?.CurrentState
                ?.Name ?? NormalState;

            switch (currentState)
            {
                case NormalState:
                    currentState = e.Delta.Translation.X < 0 ? RightState : LeftState;
                    break;
                case LeftState:
                    currentState = e.Delta.Translation.X < 0 ? NormalState : LeftState;
                    break;
                case RightState:
                    currentState = e.Delta.Translation.X < 0 ? RightState : NormalState;
                    break;
            }

            VisualStateManager.GoToState(this, currentState, true);
        }
    }
}

(RxとReactiveProperty使ってます。)

やってることは、現在のVisualStateと左右のどちらに指が移動したのかを見て次のVisualStateを決めているだけです。

実行すると初期状態はこんな感じ。

f:id:okazuki:20150630205138p:plain

右にスワイプするとこんな風になる(アニメーションしながらにょきっと出てくる)

f:id:okazuki:20150630205225p:plain

課題

指に追随する形で左右のメニューが出てくるようにするには、VisualStateで綺麗に区切る感じじゃなくて、もっと泥臭いコードが必要そう。要検討。