かずきのBlog@hatena

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

左右にスワイプするとにょきっとメニューが生えてくるものを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で綺麗に区切る感じじゃなくて、もっと泥臭いコードが必要そう。要検討。