// (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;
}
}
}