Metro UIのボタンエフェクトをWindows Phoneに適用する

Windows Phoneで遊び始めている。
Windows Phone自体もそうだが、WPFも殆ど触ったことがないので、とても苦しいw
取り合えず、簡単なアプリを実際に公開してみて感触をつかみつつ、WPF理解への足掛かりにしたいなと思っている。

で、早速製作中なのだが、Metro UI(名称がぽしゃったので、何と呼べばいいのか困るなぁ)に使用するユーザーインターフェイスのボタン、

WP8MetroUI

のマウスカーソルのあたりをタップした時に、ボタンの隅が押されて変形したような挙動でフィードバックがある、アレをやりたいと思ったのだが、どうも簡単に出来ないようだ。
で、WPFの変形の基礎とか、コントロールのカスタマイズの方法など、色々調べて以下のコードを書いた。

(ここまで紆余曲折の末、約2日 orz 直前にGeometry/Path/RenderTargetBitmapだけいじっていたのが幸いした。でないと、座標がdoubleというだけでも悶絶していたかもしれない…)

public sealed class TiltBehavior : Behavior<UIElement>
{
    private PlaneProjection projection_;

<pre><code>public TiltBehavior()
{
    this.Depth = 30.0;
    this.Tracking = true;
}

public double Depth
{
    get;
    set;
}

public bool Tracking
{
    get;
    set;
}

protected override void OnAttached()
{
    this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
    }
}

protected override void OnDetaching()
{
    this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
    }
}

private static void Apply(Size size, Point point, PlaneProjection projection, double depth)
{
    // コントロールのサイズからノーマライズした割合を得る
    var normalizePoint = new Point(
        point.X / size.Width,
        point.Y / size.Height);

    // 0~1の範囲外を切り捨てる
    var satulatePoint = new Point(
        (normalizePoint.X &amp;gt; 1.0) ? 1.0 : ((normalizePoint.X &amp;lt; 0.0) ? 0.0 : normalizePoint.X),
        (normalizePoint.Y &amp;gt; 1.0) ? 1.0 : ((normalizePoint.Y &amp;lt; 0.0) ? 0.0 : normalizePoint.Y));

    // 中心位置からの割合を得る
    var originPoint = new Point(
        satulatePoint.X * 2.0 - 1.0,
        satulatePoint.Y * 2.0 - 1.0);

    // 絶対位置
    var absolutePoint = new Point(
        Math.Abs(originPoint.X),
        Math.Abs(originPoint.Y));

    // 中心からの位置関係
    var directionX = originPoint.X &amp;gt;= 0.0;
    var directionY = originPoint.Y &amp;gt;= 0.0;

    // タップされた位置に応じて、回転軸位置を固定する(0又は1)
    projection.CenterOfRotationX = directionX ? 0.0 : 1.0;
    projection.CenterOfRotationY = directionY ? 0.0 : 1.0;

    // 辺ではなく、中心をタップした場合にも、フィードバックを得る
    // (辺をタップした場合は0に近づく事で影響を避ける)
    var distance = (absolutePoint.X &amp;gt; absolutePoint.Y) ? absolutePoint.X : absolutePoint.Y;
        projection.GlobalOffsetZ =
        (1.0 - distance) *
        0.5 *       // 中心位置でのZ座標
        (-depth);

    // Rotationは角度なので、計算して算出
    projection.RotationY =
        Math.Atan2(depth * (0.0 - originPoint.X) * 0.5, size.Width) /       // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
    projection.RotationX =
        Math.Atan2(depth * originPoint.Y * 0.5, size.Height) /      // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
}

private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (this.AssociatedObject.Projection == null)
    {
        this.AssociatedObject.CaptureMouse();

        projection_ = new PlaneProjection();
        this.AssociatedObject.Projection = projection_;

        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
{
    if (projection_ != null)
    {
        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void Uncapture()
{
    if (object.ReferenceEquals(this.AssociatedObject.Projection, projection_) == true)
    {
        this.AssociatedObject.Projection = null;
        projection_ = null;

        this.AssociatedObject.ReleaseMouseCapture();
    }
}

private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    Uncapture();
}

private void AssociatedObject_LostMouseCapture(object sender, MouseEventArgs e)
{
    Uncapture();
}
</code></pre>

}

何しろWPFは完全に初心者なので、変なことをやっていたり、思想から外れる設計なのかもしれないのであしからず。これはUIElementクラスに適用できるビヘイビアクラスで、プロジェクトに入れておいて、ページのXAMLで以下のような感じで使う。

&lt;ListBox x:Name=&quot;MainListBox&quot; Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot;&gt;
    &lt;ListBox.ItemTemplate&gt;
        &lt;DataTemplate&gt;
            &lt;StackPanel Margin=&quot;8,0,8,8&quot;&gt;
                &lt;TextBlock Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Name}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;TextBlock Margin=&quot;8,8,0,8&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Description}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;i:Interaction.Behaviors&gt;   &lt;!-- ココ --&gt;
                    &lt;Behaviors:TiltBehavior /&gt;
                &lt;/i:Interaction.Behaviors&gt;
            &lt;/StackPanel&gt;
        &lt;/DataTemplate&gt;
    &lt;/ListBox.ItemTemplate&gt;
&lt;/ListBox&gt;

ListBoxにコレクションをバインディングし、その要素毎にStackPanelで表示する。そのStackPanelにビヘイビアの指定を行うと、対応するクラスのビヘイビアが呼び出される。名前空間「i」は、System.Windows.Interactivityで、これはWindows Phone以外ではアセンブリが違うかもしれないが存在すると思う。最初にxmlns:iで宣言しておくこと。

なお、Buttonに適用するとうまく動かない。何故かは、これから悩むところ (^^;; StackPanelのクリックを検出する方向で逃げたほうがいいのか、Buttonで真面目にやるほうがいいのか、それすら分からないのが困ったものだ…