Skip to content

Instantly share code, notes, and snippets.

@winkel
Created July 13, 2013 10:14
Show Gist options
  • Save winkel/5990239 to your computer and use it in GitHub Desktop.
Save winkel/5990239 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
namespace SharpGIS.Metro.Controls
{
/// <summary>
/// This code provides attached properties for adding a 'tilt' effect to all
/// controls within a container.
/// </summary>
/// <QualityBand>Preview</QualityBand>
[SuppressMessage("Microsoft.Design", "CA1052:StaticHolderTypesShouldBeSealed", Justification = "Cannot be static and derive from DependencyObject.")]
public partial class TiltEffect : DependencyObject
{
/// <summary>
/// Cache of previous cache modes. Not using weak references for now.
/// </summary>
private static Dictionary<DependencyObject, CacheMode> _originalCacheMode = new Dictionary<DependencyObject, CacheMode>();
/// <summary>
/// Maximum amount of tilt, in radians.
/// </summary>
private const double MaxAngle = 0.3;
/// <summary>
/// Maximum amount of depression, in pixels
/// </summary>
private const double MaxDepression = 25;
/// <summary>
/// Delay between releasing an element and the tilt release animation
/// playing.
/// </summary>
private static readonly TimeSpan TiltReturnAnimationDelay = TimeSpan.FromMilliseconds(200);
/// <summary>
/// Duration of tilt release animation.
/// </summary>
private static readonly TimeSpan TiltReturnAnimationDuration = TimeSpan.FromMilliseconds(100);
/// <summary>
/// The control that is currently being tilted.
/// </summary>
private static FrameworkElement currentTiltElement;
/// <summary>
/// The single instance of a storyboard used for all tilts.
/// </summary>
private static Storyboard tiltReturnStoryboard;
/// <summary>
/// The single instance of an X rotation used for all tilts.
/// </summary>
private static DoubleAnimation tiltReturnXAnimation;
/// <summary>
/// The single instance of a Y rotation used for all tilts.
/// </summary>
private static DoubleAnimation tiltReturnYAnimation;
/// <summary>
/// The single instance of a Z depression used for all tilts.
/// </summary>
private static DoubleAnimation tiltReturnZAnimation;
/// <summary>
/// The center of the tilt element.
/// </summary>
private static Point currentTiltElementCenter;
/// <summary>
/// Whether the animation just completed was for a 'pause' or not.
/// </summary>
private static bool wasPauseAnimation = false;
#region Constructor and Static Constructor
/// <summary>
/// This is not a constructable class, but it cannot be static because
/// it derives from DependencyObject.
/// </summary>
private TiltEffect()
{
}
#endregion
#region Dependency properties
/// <summary>
/// Whether the tilt effect is enabled on a container (and all its
/// children).
/// </summary>
public static readonly DependencyProperty IsTiltEnabledProperty = DependencyProperty.RegisterAttached(
"IsTiltEnabled",
typeof(bool),
typeof(TiltEffect),
new PropertyMetadata(false, OnIsTiltEnabledChanged)
);
/// <summary>
/// Gets the IsTiltEnabled dependency property from an object.
/// </summary>
/// <param name="source">The object to get the property from.</param>
/// <returns>The property's value.</returns>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
public static bool GetIsTiltEnabled(DependencyObject source)
{
return (bool)source.GetValue(IsTiltEnabledProperty);
}
/// <summary>
/// Sets the IsTiltEnabled dependency property on an object.
/// </summary>
/// <param name="source">The object to set the property on.</param>
/// <param name="value">The value to set.</param>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
public static void SetIsTiltEnabled(DependencyObject source, bool value)
{
source.SetValue(IsTiltEnabledProperty, value);
}
/// <summary>
/// Suppresses the tilt effect on a single control that would otherwise
/// be tilted.
/// </summary>
public static readonly DependencyProperty SuppressTiltProperty = DependencyProperty.RegisterAttached(
"SuppressTilt",
typeof(bool),
typeof(TiltEffect),
null
);
/// <summary>
/// Gets the SuppressTilt dependency property from an object.
/// </summary>
/// <param name="source">The object to get the property from.</param>
/// <returns>The property's value.</returns>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
public static bool GetSuppressTilt(DependencyObject source)
{
return (bool)source.GetValue(SuppressTiltProperty);
}
/// <summary>
/// Sets the SuppressTilt dependency property from an object.
/// </summary>
/// <param name="source">The object to get the property from.</param>
/// <param name="value">The property's value.</param>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
public static void SetSuppressTilt(DependencyObject source, bool value)
{
source.SetValue(SuppressTiltProperty, value);
}
/// <summary>
/// Property change handler for the IsTiltEnabled dependency property.
/// </summary>
/// <param name="target">The element that the property is atteched to.</param>
/// <param name="args">Event arguments.</param>
/// <remarks>
/// Adds or removes event handlers from the element that has been
/// (un)registered for tilting.
/// </remarks>
private static void OnIsTiltEnabledChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
FrameworkElement fe = target as FrameworkElement;
if (fe != null)
{
// Add / remove the event handler if necessary
if ((bool)args.NewValue == true)
{
fe.PointerPressed += TiltEffect_PointerPressed;
}
else
{
fe.PointerPressed -= TiltEffect_PointerPressed;
}
}
}
#endregion
#region Top-level manipulation event handlers
/// <summary>
/// Event handler for ManipulationStarted.
/// </summary>
/// <param name="sender">sender of the event - this will be the tilt
/// container (eg, entire page).</param>
/// <param name="e">Event arguments.</param>
private static void TiltEffect_PointerPressed(object sender, PointerRoutedEventArgs e)
{
TryStartTiltEffect(sender as FrameworkElement, e);
}
/// <summary>
/// Event handler for ManipulationDelta
/// </summary>
/// <param name="sender">sender of the event - this will be the tilting
/// object (eg a button).</param>
/// <param name="e">Event arguments.</param>
private static void TiltEffect_PointerMoved(object sender, PointerRoutedEventArgs e)
{
ContinueTiltEffect(sender as FrameworkElement, e);
}
/// <summary>
/// Event handler for ManipulationCompleted.
/// </summary>
/// <param name="sender">sender of the event - this will be the tilting
/// object (eg a button).</param>
/// <param name="e">Event arguments.</param>
private static void TiltEffect_PointerReleased(object sender, PointerRoutedEventArgs e)
{
EndTiltEffect(currentTiltElement);
}
#endregion
#region Core tilt logic
/// <summary>
/// Checks if the manipulation should cause a tilt, and if so starts the
/// tilt effect.
/// </summary>
/// <param name="source">The source of the manipulation (the tilt
/// container, eg entire page).</param>
/// <param name="e">The args from the ManipulationStarted event.</param>
private static void TryStartTiltEffect(FrameworkElement source, PointerRoutedEventArgs e)
{
FrameworkElement element = source; // VisualTreeHelper.GetChild(ancestor, 0) as FrameworkElement;
FrameworkElement container = source;// e.Container as FrameworkElement;
if (element == null || container == null)
return;
// Touch point relative to the element being tilted.
Point tiltTouchPoint = e.GetCurrentPoint(element).Position; // container.TransformToVisual(element).TransformPoint(e.GetCurrentPoint(element));
// Center of the element being tilted.
Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
// Camera adjustment.
Point centerToCenterDelta = GetCenterToCenterDelta(element, source);
BeginTiltEffect(element, tiltTouchPoint, elementCenter, centerToCenterDelta, e.Pointer);
return;
}
/// <summary>
/// Computes the delta between the centre of an element and its
/// container.
/// </summary>
/// <param name="element">The element to compare.</param>
/// <param name="container">The element to compare against.</param>
/// <returns>A point that represents the delta between the two centers.</returns>
private static Point GetCenterToCenterDelta(FrameworkElement element, FrameworkElement container)
{
Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
Point containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
Point transformedElementCenter = element.TransformToVisual(container).TransformPoint(elementCenter);
return new Point(containerCenter.X - transformedElementCenter.X, containerCenter.Y - transformedElementCenter.Y);
}
/// <summary>
/// Begins the tilt effect by preparing the control and doing the
/// initial animation.
/// </summary>
/// <param name="element">The element to tilt.</param>
/// <param name="touchPoint">The touch point, in element coordinates.</param>
/// <param name="centerPoint">The center point of the element in element
/// coordinates.</param>
/// <param name="centerDelta">The delta between the
/// <paramref name="element"/>'s center and the container's center.</param>
private static void BeginTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint, Point centerDelta, Pointer p)
{
if (tiltReturnStoryboard != null)
{
StopTiltReturnStoryboardAndCleanup();
}
if (PrepareControlForTilt(element, centerDelta, p) == false)
{
return;
}
currentTiltElement = element;
currentTiltElementCenter = centerPoint;
PrepareTiltReturnStoryboard(element);
ApplyTiltEffect(currentTiltElement, touchPoint, currentTiltElementCenter);
}
/// <summary>
/// Prepares a control to be tilted by setting up a plane projection and
/// some event handlers.
/// </summary>
/// <param name="element">The control that is to be tilted.</param>
/// <param name="centerDelta">Delta between the element's center and the
/// tilt container's.</param>
/// <returns>true if successful; false otherwise.</returns>
/// <remarks>
/// This method is conservative; it will fail any attempt to tilt a
/// control that already has a projection on it.
/// </remarks>
private static bool PrepareControlForTilt(FrameworkElement element, Point centerDelta, Pointer p)
{
// Prevents interference with any existing transforms
if (element.Projection != null || (element.RenderTransform != null && element.RenderTransform.GetType() != typeof(MatrixTransform)))
{
return false;
}
_originalCacheMode[element] = element.CacheMode;
element.CacheMode = new BitmapCache();
TranslateTransform transform = new TranslateTransform();
transform.X = centerDelta.X;
transform.Y = centerDelta.Y;
element.RenderTransform = transform;
PlaneProjection projection = new PlaneProjection();
projection.GlobalOffsetX = -1 * centerDelta.X;
projection.GlobalOffsetY = -1 * centerDelta.Y;
element.Projection = projection;
element.PointerMoved += TiltEffect_PointerMoved;
element.PointerReleased += TiltEffect_PointerReleased;
element.CapturePointer(p);
return true;
}
/// <summary>
/// Removes modifications made by PrepareControlForTilt.
/// </summary>
/// <param name="element">THe control to be un-prepared.</param>
/// <remarks>
/// This method is basic; it does not do anything to detect if the
/// control being un-prepared was previously prepared.
/// </remarks>
private static void RevertPrepareControlForTilt(FrameworkElement element)
{
element.PointerMoved -= TiltEffect_PointerMoved;
element.PointerReleased -= TiltEffect_PointerReleased;
element.Projection = null;
element.RenderTransform = null;
CacheMode original;
if (_originalCacheMode.TryGetValue(element, out original))
{
element.CacheMode = original;
_originalCacheMode.Remove(element);
}
else
{
element.CacheMode = null;
}
}
/// <summary>
/// Creates the tilt return storyboard (if not already created) and
/// targets it to the projection.
/// </summary>
/// <param name="element">The framework element to prepare for
/// projection.</param>
private static void PrepareTiltReturnStoryboard(FrameworkElement element)
{
if (tiltReturnStoryboard == null)
{
tiltReturnStoryboard = new Storyboard();
tiltReturnStoryboard.Completed += TiltReturnStoryboard_Completed;
tiltReturnXAnimation = new DoubleAnimation();
Storyboard.SetTargetProperty(tiltReturnXAnimation, "RotationX");
tiltReturnXAnimation.BeginTime = TiltReturnAnimationDelay;
tiltReturnXAnimation.To = 0;
tiltReturnXAnimation.Duration = TiltReturnAnimationDuration;
tiltReturnYAnimation = new DoubleAnimation();
Storyboard.SetTargetProperty(tiltReturnYAnimation, "RotationY");
tiltReturnYAnimation.BeginTime = TiltReturnAnimationDelay;
tiltReturnYAnimation.To = 0;
tiltReturnYAnimation.Duration = TiltReturnAnimationDuration;
tiltReturnZAnimation = new DoubleAnimation();
Storyboard.SetTargetProperty(tiltReturnZAnimation, "GlobalOffsetZ");
tiltReturnZAnimation.BeginTime = TiltReturnAnimationDelay;
tiltReturnZAnimation.To = 0;
tiltReturnZAnimation.Duration = TiltReturnAnimationDuration;
tiltReturnStoryboard.Children.Add(tiltReturnXAnimation);
tiltReturnStoryboard.Children.Add(tiltReturnYAnimation);
tiltReturnStoryboard.Children.Add(tiltReturnZAnimation);
}
Storyboard.SetTarget(tiltReturnXAnimation, element.Projection);
Storyboard.SetTarget(tiltReturnYAnimation, element.Projection);
Storyboard.SetTarget(tiltReturnZAnimation, element.Projection);
}
/// <summary>
/// Continues a tilt effect that is currently applied to an element,
/// presumably because the user moved their finger.
/// </summary>
/// <param name="element">The element being tilted.</param>
/// <param name="e">The manipulation event args.</param>
private static void ContinueTiltEffect(FrameworkElement element, PointerRoutedEventArgs e)
{
FrameworkElement container = element;
if (container == null || element == null)
{
return;
}
Point tiltTouchPoint = e.GetCurrentPoint(element).Position;
// If touch moved outside bounds of element, then pause the tilt
// (but don't cancel it)
if (new Rect(0, 0, currentTiltElement.ActualWidth, currentTiltElement.ActualHeight).Contains(tiltTouchPoint) != true)
{
PauseTiltEffect();
}
else
{
// Apply the updated tilt effect
ApplyTiltEffect(currentTiltElement, tiltTouchPoint, currentTiltElementCenter);
}
}
/// <summary>
/// Ends the tilt effect by playing the animation.
/// </summary>
/// <param name="element">The element being tilted.</param>
private static void EndTiltEffect(FrameworkElement element)
{
if (element != null)
{
element.PointerReleased -= TiltEffect_PointerPressed;
element.PointerMoved -= TiltEffect_PointerMoved;
}
if (tiltReturnStoryboard != null)
{
wasPauseAnimation = false;
if (tiltReturnStoryboard.GetCurrentState() != ClockState.Active)
{
tiltReturnStoryboard.Begin();
}
}
else
{
StopTiltReturnStoryboardAndCleanup();
}
}
/// <summary>
/// Handler for the storyboard complete event.
/// </summary>
/// <param name="sender">sender of the event.</param>
/// <param name="e">event args.</param>
private static void TiltReturnStoryboard_Completed(object sender, object e)
{
if (wasPauseAnimation)
{
ResetTiltEffect(currentTiltElement);
}
else
{
StopTiltReturnStoryboardAndCleanup();
}
}
/// <summary>
/// Resets the tilt effect on the control, making it appear 'normal'
/// again.
/// </summary>
/// <param name="element">The element to reset the tilt on.</param>
/// <remarks>
/// This method doesn't turn off the tilt effect or cancel any current
/// manipulation; it just temporarily cancels the effect.
/// </remarks>
private static void ResetTiltEffect(FrameworkElement element)
{
PlaneProjection projection = element.Projection as PlaneProjection;
projection.RotationY = 0;
projection.RotationX = 0;
projection.GlobalOffsetZ = 0;
}
/// <summary>
/// Stops the tilt effect and release resources applied to the currently
/// tilted control.
/// </summary>
private static void StopTiltReturnStoryboardAndCleanup()
{
if (tiltReturnStoryboard != null)
{
tiltReturnStoryboard.Stop();
}
RevertPrepareControlForTilt(currentTiltElement);
}
/// <summary>
/// Pauses the tilt effect so that the control returns to the 'at rest'
/// position, but doesn't stop the tilt effect (handlers are still
/// attached).
/// </summary>
private static void PauseTiltEffect()
{
if ((tiltReturnStoryboard != null) && !wasPauseAnimation)
{
tiltReturnStoryboard.Stop();
wasPauseAnimation = true;
tiltReturnStoryboard.Begin();
}
}
/// <summary>
/// Resets the storyboard to not running.
/// </summary>
private static void ResetTiltReturnStoryboard()
{
tiltReturnStoryboard.Stop();
wasPauseAnimation = false;
}
/// <summary>
/// Applies the tilt effect to the control.
/// </summary>
/// <param name="element">the control to tilt.</param>
/// <param name="touchPoint">The touch point, in the container's
/// coordinates.</param>
/// <param name="centerPoint">The center point of the container.</param>
private static void ApplyTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint)
{
// Stop any active animation
ResetTiltReturnStoryboard();
// Get relative point of the touch in percentage of container size
Point normalizedPoint = new Point(
Math.Min(Math.Max(touchPoint.X / (centerPoint.X * 2), 0), 1),
Math.Min(Math.Max(touchPoint.Y / (centerPoint.Y * 2), 0), 1));
if (double.IsNaN(normalizedPoint.X) || double.IsNaN(normalizedPoint.Y))
{
return;
}
// Shell values
double xMagnitude = Math.Abs(normalizedPoint.X - 0.5);
double yMagnitude = Math.Abs(normalizedPoint.Y - 0.5);
double xDirection = -Math.Sign(normalizedPoint.X - 0.5);
double yDirection = Math.Sign(normalizedPoint.Y - 0.5);
double angleMagnitude = xMagnitude + yMagnitude;
double xAngleContribution = xMagnitude + yMagnitude > 0 ? xMagnitude / (xMagnitude + yMagnitude) : 0;
double angle = angleMagnitude * MaxAngle * 180 / Math.PI;
double depression = (1 - angleMagnitude) * MaxDepression;
// RotationX and RotationY are the angles of rotations about the x-
// or y-*axis*; to achieve a rotation in the x- or y-*direction*, we
// need to swap the two. That is, a rotation to the left about the
// y-axis is a rotation to the left in the x-direction, and a
// rotation up about the x-axis is a rotation up in the y-direction.
PlaneProjection projection = element.Projection as PlaneProjection;
projection.RotationY = angle * xAngleContribution * xDirection;
projection.RotationX = angle * (1 - xAngleContribution) * yDirection;
projection.GlobalOffsetZ = -depression;
}
#endregion
}
/// <summary>
/// Couple of simple helpers for walking the visual tree.
/// </summary>
static class TreeHelpers
{
/// <summary>
/// Gets the ancestors of the element, up to the root.
/// </summary>
/// <param name="node">The element to start from.</param>
/// <returns>An enumerator of the ancestors.</returns>
public static IEnumerable<FrameworkElement> GetVisualAncestors(this FrameworkElement node)
{
FrameworkElement parent = node.GetVisualParent();
while (parent != null)
{
yield return parent;
parent = parent.GetVisualParent();
}
}
/// <summary>
/// Gets the visual parent of the element.
/// </summary>
/// <param name="node">The element to check.</param>
/// <returns>The visual parent.</returns>
public static FrameworkElement GetVisualParent(this FrameworkElement node)
{
return VisualTreeHelper.GetParent(node) as FrameworkElement;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment