// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. // --- // Important Workaround Note for developers using the BETA: // There is a workaround in code that removes any CacheMode from the content of // the control. It works around a platform bug that is slated to be fixed for // release. // // If you are using the beta tools, remove the comment below: // #define WORKAROUND_BITMAP_CACHE_BUG // --- using System; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; namespace Microsoft.Phone.Controls.Unofficial { /// /// A content control designed to wrap anything in Silverlight with a user /// experience concept called 'tilt', applying a transformation during /// manipulation by a user. /// public class TiltContentControl : ContentControl { #region Constants /// /// Maximum angle for the tilt effect, defined in Radians. /// private const double MaxAngle = 0.3; /// /// The maximum depression for the tilt effect, given in pixel units. /// private const double MaxDepression = 25; /// /// The number of seconds for a tilt revert to take. /// private static readonly Duration TiltUpAnimationDuration = new Duration(TimeSpan.FromSeconds(.5)); /// /// A single logarithmic ease instance. /// private static readonly IEasingFunction LogEase = new LogarithmicEase(); #endregion #region Static property instances /// /// Single instance of the Rotation X property. /// private static readonly PropertyPath RotationXProperty = new PropertyPath(PlaneProjection.RotationXProperty); /// /// Single instance of the Rotation Y property. /// private static readonly PropertyPath RotationYProperty = new PropertyPath(PlaneProjection.RotationYProperty); /// /// Single instance of the Global Offset Z property. /// private static readonly PropertyPath GlobalOffsetZProperty = new PropertyPath(PlaneProjection.GlobalOffsetZProperty); #endregion /// /// The content element instance. /// private ContentPresenter _presenter; /// /// The original width of the control. /// private double _width; /// /// The original height of the control. /// private double _height; /// /// The storyboard used for the tilt up effect. /// private Storyboard _tiltUpStoryboard; /// /// The plane projection used to show the tilt effect. /// private PlaneProjection _planeProjection; /// /// Overrides the method called when apply template is called. We assume /// that the implementation root is the content presenter. /// public override void OnApplyTemplate() { base.OnApplyTemplate(); _presenter = GetImplementationRoot(this) as ContentPresenter; } /// /// Overrides the maniupulation started event. /// /// The manipulation event arguments. protected override void OnManipulationStarted(ManipulationStartedEventArgs e) { base.OnManipulationStarted(e); if (_presenter != null) { #if WORKAROUND_BITMAP_CACHE_BUG // WORKAROUND NOTE: // This is a workaround for a platform bug related to cache mode // that should be fixed before final release of the platform. UIElement elementContent = _contentElement.Content as UIElement; if (elementContent != null && elementContent.CacheMode != null) { elementContent.CacheMode = null; } #endif _planeProjection = new PlaneProjection(); _presenter.Projection = _planeProjection; _tiltUpStoryboard = new Storyboard(); _tiltUpStoryboard.Completed += TiltUpCompleted; DoubleAnimation tiltUpRotateXAnimation = new DoubleAnimation(); Storyboard.SetTarget(tiltUpRotateXAnimation, _planeProjection); Storyboard.SetTargetProperty(tiltUpRotateXAnimation, RotationXProperty); tiltUpRotateXAnimation.To = 0; tiltUpRotateXAnimation.EasingFunction = LogEase; tiltUpRotateXAnimation.Duration = TiltUpAnimationDuration; DoubleAnimation tiltUpRotateYAnimation = new DoubleAnimation(); Storyboard.SetTarget(tiltUpRotateYAnimation, _planeProjection); Storyboard.SetTargetProperty(tiltUpRotateYAnimation, RotationYProperty); tiltUpRotateYAnimation.To = 0; tiltUpRotateYAnimation.EasingFunction = LogEase; tiltUpRotateYAnimation.Duration = TiltUpAnimationDuration; DoubleAnimation tiltUpOffsetZAnimation = new DoubleAnimation(); Storyboard.SetTarget(tiltUpOffsetZAnimation, _planeProjection); Storyboard.SetTargetProperty(tiltUpOffsetZAnimation, GlobalOffsetZProperty); tiltUpOffsetZAnimation.To = 0; tiltUpOffsetZAnimation.EasingFunction = LogEase; tiltUpOffsetZAnimation.Duration = TiltUpAnimationDuration; _tiltUpStoryboard.Children.Add(tiltUpRotateXAnimation); _tiltUpStoryboard.Children.Add(tiltUpRotateYAnimation); _tiltUpStoryboard.Children.Add(tiltUpOffsetZAnimation); } if (_planeProjection != null) { _width = ActualWidth; _height = ActualHeight; if (_tiltUpStoryboard != null) { _tiltUpStoryboard.Stop(); } DepressAndTilt(e.ManipulationOrigin, e.ManipulationContainer); } } /// /// Handles the manipulation delta event. /// /// The manipulation event arguments. protected override void OnManipulationDelta(ManipulationDeltaEventArgs e) { base.OnManipulationDelta(e); // Depress and tilt regardless of whether the event was handled. if (_planeProjection != null) { DepressAndTilt(e.ManipulationOrigin, e.ManipulationContainer); } } /// /// Handles the manipulation completed event. /// /// The manipulation event arguments. protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e) { base.OnManipulationCompleted(e); if (_planeProjection != null) { if (_tiltUpStoryboard != null) { _tiltUpStoryboard.Begin(); } else { _planeProjection.RotationY = 0; _planeProjection.RotationX = 0; _planeProjection.GlobalOffsetZ = 0; } } } /// /// Updates the depression and tilt based on position of the /// manipulation relative to the original origin from input. /// /// The origin of manipulation. /// The container instance. private void DepressAndTilt(Point manipulationOrigin, UIElement manipulationContainer) { GeneralTransform transform = manipulationContainer.TransformToVisual(this); Point transformedOrigin = transform.Transform(manipulationOrigin); Point normalizedPoint = new Point( Math.Min(Math.Max(transformedOrigin.X / _width, 0), 1), Math.Min(Math.Max(transformedOrigin.Y / _height, 0), 1)); 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, the // two must be swapped. So 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.RotationY = angle * xAngleContribution * xDirection; _planeProjection.RotationX = angle * (1 - xAngleContribution) * yDirection; _planeProjection.GlobalOffsetZ = -depression; } /// /// Handles the tilt up completed event. /// /// The source object. /// The event arguments. private void TiltUpCompleted(object sender, EventArgs e) { if (_tiltUpStoryboard != null) { _tiltUpStoryboard.Stop(); } _tiltUpStoryboard = null; _planeProjection = null; _presenter.Projection = null; } /// /// An easing function of ln(t+1)/ln(2). /// private class LogarithmicEase : EasingFunctionBase { /// /// Constant value of ln(2) used in the easing function. /// private const double NaturalLog2 = 0.693147181; /// /// Overrides the EaseInCore method to provide the logic portion of /// an ease in. /// /// Normalized time (progress) of the /// animation, which is a value from 0 through 1. /// A double that represents the transformed progress. protected override double EaseInCore(double normalizedTime) { return Math.Log(normalizedTime + 1) / NaturalLog2; } } /// /// Gets the implementation root of the Control. /// /// The DependencyObject. /// /// Implements Silverlight's corresponding internal property on Control. /// /// Returns the implementation root or null. public static FrameworkElement GetImplementationRoot(DependencyObject dependencyObject) { Debug.Assert(dependencyObject != null, "DependencyObject should not be null."); return (1 == VisualTreeHelper.GetChildrenCount(dependencyObject)) ? VisualTreeHelper.GetChild(dependencyObject, 0) as FrameworkElement : null; } } }