RSS

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

17 7月

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_;

	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 > 1.0) ? 1.0 : ((normalizePoint.X < 0.0) ? 0.0 : normalizePoint.X),
			(normalizePoint.Y > 1.0) ? 1.0 : ((normalizePoint.Y < 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 >= 0.0;
		var directionY = originPoint.Y >= 0.0;

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

		// 辺ではなく、中心をタップした場合にも、フィードバックを得る
		// (辺をタップした場合は0に近づく事で影響を避ける)
		var distance = (absolutePoint.X > 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) > 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) > 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();
	}
}

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

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

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

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

 
1件のコメント

投稿者: : 2013/07/17 投稿先 .NET, Phone, WPF

 

Metro UIのボタンエフェクトをWindows Phoneに適用する」への1件のフィードバック

  1. kekyo

    2013/07/18 at 08:46

    ちょっと式が間違っていたので修正しました。見た目的には殆どわかりませんがw

     

kekyo への返信 コメントをキャンセル