using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; namespace SharpGIS.Metro.Controls { /// /// This code provides attached properties for adding a 'tilt' effect to all /// controls within a container. /// /// Preview [SuppressMessage("Microsoft.Design", "CA1052:StaticHolderTypesShouldBeSealed", Justification = "Cannot be static and derive from DependencyObject.")] public partial class TiltEffect : DependencyObject { /// /// Cache of previous cache modes. Not using weak references for now. /// private static Dictionary _originalCacheMode = new Dictionary(); /// /// Maximum amount of tilt, in radians. /// private const double MaxAngle = 0.3; /// /// Maximum amount of depression, in pixels /// private const double MaxDepression = 25; /// /// Delay between releasing an element and the tilt release animation /// playing. /// private static readonly TimeSpan TiltReturnAnimationDelay = TimeSpan.FromMilliseconds(200); /// /// Duration of tilt release animation. /// private static readonly TimeSpan TiltReturnAnimationDuration = TimeSpan.FromMilliseconds(100); /// /// The control that is currently being tilted. /// private static FrameworkElement currentTiltElement; /// /// The single instance of a storyboard used for all tilts. /// private static Storyboard tiltReturnStoryboard; /// /// The single instance of an X rotation used for all tilts. /// private static DoubleAnimation tiltReturnXAnimation; /// /// The single instance of a Y rotation used for all tilts. /// private static DoubleAnimation tiltReturnYAnimation; /// /// The single instance of a Z depression used for all tilts. /// private static DoubleAnimation tiltReturnZAnimation; /// /// The center of the tilt element. /// private static Point currentTiltElementCenter; /// /// Whether the animation just completed was for a 'pause' or not. /// private static bool wasPauseAnimation = false; #region Constructor and Static Constructor /// /// This is not a constructable class, but it cannot be static because /// it derives from DependencyObject. /// private TiltEffect() { } #endregion #region Dependency properties /// /// Whether the tilt effect is enabled on a container (and all its /// children). /// public static readonly DependencyProperty IsTiltEnabledProperty = DependencyProperty.RegisterAttached( "IsTiltEnabled", typeof(bool), typeof(TiltEffect), new PropertyMetadata(false, OnIsTiltEnabledChanged) ); /// /// Gets the IsTiltEnabled dependency property from an object. /// /// The object to get the property from. /// The property's value. [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); } /// /// Sets the IsTiltEnabled dependency property on an object. /// /// The object to set the property on. /// The value to set. [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); } /// /// Suppresses the tilt effect on a single control that would otherwise /// be tilted. /// public static readonly DependencyProperty SuppressTiltProperty = DependencyProperty.RegisterAttached( "SuppressTilt", typeof(bool), typeof(TiltEffect), null ); /// /// Gets the SuppressTilt dependency property from an object. /// /// The object to get the property from. /// The property's value. [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); } /// /// Sets the SuppressTilt dependency property from an object. /// /// The object to get the property from. /// The property's value. [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); } /// /// Property change handler for the IsTiltEnabled dependency property. /// /// The element that the property is atteched to. /// Event arguments. /// /// Adds or removes event handlers from the element that has been /// (un)registered for tilting. /// 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 /// /// Event handler for ManipulationStarted. /// /// sender of the event - this will be the tilt /// container (eg, entire page). /// Event arguments. private static void TiltEffect_PointerPressed(object sender, PointerEventArgs e) { TryStartTiltEffect(sender as FrameworkElement, e); } /// /// Event handler for ManipulationDelta /// /// sender of the event - this will be the tilting /// object (eg a button). /// Event arguments. private static void TiltEffect_PointerMoved(object sender, PointerEventArgs e) { ContinueTiltEffect(sender as FrameworkElement, e); } /// /// Event handler for ManipulationCompleted. /// /// sender of the event - this will be the tilting /// object (eg a button). /// Event arguments. private static void TiltEffect_PointerReleased(object sender, PointerEventArgs e) { EndTiltEffect(currentTiltElement); } #endregion #region Core tilt logic /// /// Checks if the manipulation should cause a tilt, and if so starts the /// tilt effect. /// /// The source of the manipulation (the tilt /// container, eg entire page). /// The args from the ManipulationStarted event. private static void TryStartTiltEffect(FrameworkElement source, PointerEventArgs 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; } /// /// Computes the delta between the centre of an element and its /// container. /// /// The element to compare. /// The element to compare against. /// A point that represents the delta between the two centers. 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); } /// /// Begins the tilt effect by preparing the control and doing the /// initial animation. /// /// The element to tilt. /// The touch point, in element coordinates. /// The center point of the element in element /// coordinates. /// The delta between the /// 's center and the container's center. 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); } /// /// Prepares a control to be tilted by setting up a plane projection and /// some event handlers. /// /// The control that is to be tilted. /// Delta between the element's center and the /// tilt container's. /// true if successful; false otherwise. /// /// This method is conservative; it will fail any attempt to tilt a /// control that already has a projection on it. /// 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; } /// /// Removes modifications made by PrepareControlForTilt. /// /// THe control to be un-prepared. /// /// This method is basic; it does not do anything to detect if the /// control being un-prepared was previously prepared. /// 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; } } /// /// Creates the tilt return storyboard (if not already created) and /// targets it to the projection. /// /// The framework element to prepare for /// projection. 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); } /// /// Continues a tilt effect that is currently applied to an element, /// presumably because the user moved their finger. /// /// The element being tilted. /// The manipulation event args. private static void ContinueTiltEffect(FrameworkElement element, PointerEventArgs 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); } } /// /// Ends the tilt effect by playing the animation. /// /// The element being tilted. 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(); } } /// /// Handler for the storyboard complete event. /// /// sender of the event. /// event args. private static void TiltReturnStoryboard_Completed(object sender, object e) { if (wasPauseAnimation) { ResetTiltEffect(currentTiltElement); } else { StopTiltReturnStoryboardAndCleanup(); } } /// /// Resets the tilt effect on the control, making it appear 'normal' /// again. /// /// The element to reset the tilt on. /// /// This method doesn't turn off the tilt effect or cancel any current /// manipulation; it just temporarily cancels the effect. /// private static void ResetTiltEffect(FrameworkElement element) { PlaneProjection projection = element.Projection as PlaneProjection; projection.RotationY = 0; projection.RotationX = 0; projection.GlobalOffsetZ = 0; } /// /// Stops the tilt effect and release resources applied to the currently /// tilted control. /// private static void StopTiltReturnStoryboardAndCleanup() { if (tiltReturnStoryboard != null) { tiltReturnStoryboard.Stop(); } RevertPrepareControlForTilt(currentTiltElement); } /// /// 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). /// private static void PauseTiltEffect() { if ((tiltReturnStoryboard != null) && !wasPauseAnimation) { tiltReturnStoryboard.Stop(); wasPauseAnimation = true; tiltReturnStoryboard.Begin(); } } /// /// Resets the storyboard to not running. /// private static void ResetTiltReturnStoryboard() { tiltReturnStoryboard.Stop(); wasPauseAnimation = false; } /// /// Applies the tilt effect to the control. /// /// the control to tilt. /// The touch point, in the container's /// coordinates. /// The center point of the container. 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 } /// /// Couple of simple helpers for walking the visual tree. /// static class TreeHelpers { /// /// Gets the ancestors of the element, up to the root. /// /// The element to start from. /// An enumerator of the ancestors. public static IEnumerable GetVisualAncestors(this FrameworkElement node) { FrameworkElement parent = node.GetVisualParent(); while (parent != null) { yield return parent; parent = parent.GetVisualParent(); } } /// /// Gets the visual parent of the element. /// /// The element to check. /// The visual parent. public static FrameworkElement GetVisualParent(this FrameworkElement node) { return VisualTreeHelper.GetParent(node) as FrameworkElement; } } }