PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/TiltEffect.cs

https://bitbucket.org/jeremejevs/milk-manager
C# | 634 lines | 312 code | 104 blank | 218 comment | 46 complexity | 14a6723eeac915d9649e4e182cf41197 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using System.Windows.Controls.Primitives;
  6. using System.Windows.Input;
  7. using System.Windows.Media;
  8. using System.Windows.Media.Animation;
  9. using Microsoft.Phone.Controls;
  10. namespace NotEdible
  11. {
  12. /// <summary>
  13. /// This code provides attached properties for adding a 'tilt' effect to all controls within a container.
  14. /// </summary>
  15. public class TiltEffect : DependencyObject
  16. {
  17. #region Constructor and Static Constructor
  18. /// <summary>
  19. /// This is not a constructable class, but it cannot be static because it derives from DependencyObject.
  20. /// </summary>
  21. private TiltEffect()
  22. {
  23. }
  24. /// <summary>
  25. /// Initialize the static properties
  26. /// </summary>
  27. static TiltEffect()
  28. {
  29. // The tiltable items list.
  30. TiltableItems = new List<Type>() { typeof(ButtonBase), typeof(ListBoxItem), };
  31. UseLogarithmicEase = false;
  32. }
  33. #endregion
  34. #region Fields and simple properties
  35. // These constants are the same as the built-in effects
  36. /// <summary>
  37. /// Maximum amount of tilt, in radians
  38. /// </summary>
  39. const double MaxAngle = 0.3;
  40. /// <summary>
  41. /// Maximum amount of depression, in pixels
  42. /// </summary>
  43. const double MaxDepression = 25;
  44. /// <summary>
  45. /// Delay between releasing an element and the tilt release animation playing
  46. /// </summary>
  47. static readonly TimeSpan TiltReturnAnimationDelay = TimeSpan.FromMilliseconds(200);
  48. /// <summary>
  49. /// Duration of tilt release animation
  50. /// </summary>
  51. static readonly TimeSpan TiltReturnAnimationDuration = TimeSpan.FromMilliseconds(100);
  52. /// <summary>
  53. /// The control that is currently being tilted
  54. /// </summary>
  55. static FrameworkElement currentTiltElement;
  56. /// <summary>
  57. /// The single instance of a storyboard used for all tilts
  58. /// </summary>
  59. static Storyboard tiltReturnStoryboard;
  60. /// <summary>
  61. /// The single instance of an X rotation used for all tilts
  62. /// </summary>
  63. static DoubleAnimation tiltReturnXAnimation;
  64. /// <summary>
  65. /// The single instance of a Y rotation used for all tilts
  66. /// </summary>
  67. static DoubleAnimation tiltReturnYAnimation;
  68. /// <summary>
  69. /// The single instance of a Z depression used for all tilts
  70. /// </summary>
  71. static DoubleAnimation tiltReturnZAnimation;
  72. /// <summary>
  73. /// The center of the tilt element
  74. /// </summary>
  75. static Point currentTiltElementCenter;
  76. /// <summary>
  77. /// Whether the animation just completed was for a 'pause' or not
  78. /// </summary>
  79. static bool wasPauseAnimation = false;
  80. /// <summary>
  81. /// Whether to use a slightly more accurate (but slightly slower) tilt animation easing function
  82. /// </summary>
  83. public static bool UseLogarithmicEase { get; set; }
  84. /// <summary>
  85. /// Default list of items that are tiltable
  86. /// </summary>
  87. public static List<Type> TiltableItems { get; private set; }
  88. #endregion
  89. #region Dependency properties
  90. /// <summary>
  91. /// Whether the tilt effect is enabled on a container (and all its children)
  92. /// </summary>
  93. public static readonly DependencyProperty IsTiltEnabledProperty = DependencyProperty.RegisterAttached(
  94. "IsTiltEnabled",
  95. typeof(bool),
  96. typeof(TiltEffect),
  97. new PropertyMetadata(OnIsTiltEnabledChanged)
  98. );
  99. /// <summary>
  100. /// Gets the IsTiltEnabled dependency property from an object
  101. /// </summary>
  102. /// <param name="source">The object to get the property from</param>
  103. /// <returns>The property's value</returns>
  104. public static bool GetIsTiltEnabled(DependencyObject source) { return (bool)source.GetValue(IsTiltEnabledProperty); }
  105. /// <summary>
  106. /// Sets the IsTiltEnabled dependency property on an object
  107. /// </summary>
  108. /// <param name="source">The object to set the property on</param>
  109. /// <param name="value">The value to set</param>
  110. public static void SetIsTiltEnabled(DependencyObject source, bool value) { source.SetValue(IsTiltEnabledProperty, value); }
  111. /// <summary>
  112. /// Suppresses the tilt effect on a single control that would otherwise be tilted
  113. /// </summary>
  114. public static readonly DependencyProperty SuppressTiltProperty = DependencyProperty.RegisterAttached(
  115. "SuppressTilt",
  116. typeof(bool),
  117. typeof(TiltEffect),
  118. null
  119. );
  120. /// <summary>
  121. /// Gets the SuppressTilt dependency property from an object
  122. /// </summary>
  123. /// <param name="source">The object to get the property from</param>
  124. /// <returns>The property's value</returns>
  125. public static bool GetSuppressTilt(DependencyObject source) { return (bool)source.GetValue(SuppressTiltProperty); }
  126. /// <summary>
  127. /// Sets the SuppressTilt dependency property from an object
  128. /// </summary>
  129. /// <param name="source">The object to get the property from</param>
  130. /// <returns>The property's value</returns>
  131. public static void SetSuppressTilt(DependencyObject source, bool value) { source.SetValue(SuppressTiltProperty, value); }
  132. /// <summary>
  133. /// Property change handler for the IsTiltEnabled dependency property
  134. /// </summary>
  135. /// <param name="target">The element that the property is atteched to</param>
  136. /// <param name="args">Event args</param>
  137. /// <remarks>
  138. /// Adds or removes event handlers from the element that has been (un)registered for tilting
  139. /// </remarks>
  140. static void OnIsTiltEnabledChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
  141. {
  142. if (target is FrameworkElement)
  143. {
  144. // Add / remove the event handler if necessary
  145. if ((bool)args.NewValue == true)
  146. {
  147. (target as FrameworkElement).ManipulationStarted += TiltEffect_ManipulationStarted;
  148. }
  149. else
  150. {
  151. (target as FrameworkElement).ManipulationStarted -= TiltEffect_ManipulationStarted;
  152. }
  153. }
  154. }
  155. #endregion
  156. #region Top-level manipulation event handlers
  157. /// <summary>
  158. /// Event handler for ManipulationStarted
  159. /// </summary>
  160. /// <param name="sender">sender of the event - this will be the tilt container (eg, entire page)</param>
  161. /// <param name="e">event args</param>
  162. static void TiltEffect_ManipulationStarted(object sender, ManipulationStartedEventArgs e)
  163. {
  164. TryStartTiltEffect(sender as FrameworkElement, e);
  165. }
  166. /// <summary>
  167. /// Event handler for ManipulationDelta
  168. /// </summary>
  169. /// <param name="sender">sender of the event - this will be the tilting object (eg a button)</param>
  170. /// <param name="e">event args</param>
  171. static void TiltEffect_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
  172. {
  173. ContinueTiltEffect(sender as FrameworkElement, e);
  174. }
  175. /// <summary>
  176. /// Event handler for ManipulationCompleted
  177. /// </summary>
  178. /// <param name="sender">sender of the event - this will be the tilting object (eg a button)</param>
  179. /// <param name="e">event args</param>
  180. static void TiltEffect_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
  181. {
  182. EndTiltEffect(currentTiltElement);
  183. }
  184. #endregion
  185. #region Core tilt logic
  186. /// <summary>
  187. /// Checks if the manipulation should cause a tilt, and if so starts the tilt effect
  188. /// </summary>
  189. /// <param name="source">The source of the manipulation (the tilt container, eg entire page)</param>
  190. /// <param name="e">The args from the ManipulationStarted event</param>
  191. static void TryStartTiltEffect(FrameworkElement source, ManipulationStartedEventArgs e)
  192. {
  193. foreach (FrameworkElement ancestor in (e.OriginalSource as FrameworkElement).GetVisualAncestors())
  194. {
  195. foreach (Type t in TiltableItems)
  196. {
  197. if (t.IsAssignableFrom(ancestor.GetType()))
  198. {
  199. if ((bool)ancestor.GetValue(SuppressTiltProperty) != true)
  200. {
  201. // Use first child of the control, so that you can add transforms and not
  202. // impact any transforms on the control itself
  203. FrameworkElement element = VisualTreeHelper.GetChild(ancestor, 0) as FrameworkElement;
  204. FrameworkElement container = e.ManipulationContainer as FrameworkElement;
  205. if (element == null || container == null)
  206. return;
  207. // Touch point relative to the element being tilted
  208. Point tiltTouchPoint = container.TransformToVisual(element).Transform(e.ManipulationOrigin);
  209. // Center of the element being tilted
  210. Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
  211. // Camera adjustment
  212. Point centerToCenterDelta = GetCenterToCenterDelta(element, source);
  213. BeginTiltEffect(element, tiltTouchPoint, elementCenter, centerToCenterDelta);
  214. return;
  215. }
  216. }
  217. }
  218. }
  219. }
  220. /// <summary>
  221. /// Computes the delta between the centre of an element and its container
  222. /// </summary>
  223. /// <param name="element">The element to compare</param>
  224. /// <param name="container">The element to compare against</param>
  225. /// <returns>A point that represents the delta between the two centers</returns>
  226. static Point GetCenterToCenterDelta(FrameworkElement element, FrameworkElement container)
  227. {
  228. Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
  229. Point containerCenter;
  230. #if WINDOWS_PHONE
  231. // Need to special-case the frame to handle different orientations
  232. if (container is PhoneApplicationFrame)
  233. {
  234. PhoneApplicationFrame frame = container as PhoneApplicationFrame;
  235. // Switch width and height in landscape mode
  236. if ((frame.Orientation & PageOrientation.Landscape) == PageOrientation.Landscape)
  237. {
  238. containerCenter = new Point(container.ActualHeight / 2, container.ActualWidth / 2);
  239. }
  240. else
  241. containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
  242. }
  243. else
  244. containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
  245. #else
  246. containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
  247. #endif
  248. Point transformedElementCenter = element.TransformToVisual(container).Transform(elementCenter);
  249. Point result = new Point(containerCenter.X - transformedElementCenter.X, containerCenter.Y - transformedElementCenter.Y);
  250. return result;
  251. }
  252. /// <summary>
  253. /// Begins the tilt effect by preparing the control and doing the initial animation
  254. /// </summary>
  255. /// <param name="element">The element to tilt </param>
  256. /// <param name="touchPoint">The touch point, in element coordinates</param>
  257. /// <param name="centerPoint">The center point of the element in element coordinates</param>
  258. /// <param name="centerDelta">The delta between the <paramref name="element"/>'s center and
  259. /// the container's center</param>
  260. static void BeginTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint, Point centerDelta)
  261. {
  262. if (tiltReturnStoryboard != null)
  263. StopTiltReturnStoryboardAndCleanup();
  264. if (PrepareControlForTilt(element, centerDelta) == false)
  265. return;
  266. currentTiltElement = element;
  267. currentTiltElementCenter = centerPoint;
  268. PrepareTiltReturnStoryboard(element);
  269. ApplyTiltEffect(currentTiltElement, touchPoint, currentTiltElementCenter);
  270. }
  271. /// <summary>
  272. /// Prepares a control to be tilted by setting up a plane projection and some event handlers
  273. /// </summary>
  274. /// <param name="element">The control that is to be tilted</param>
  275. /// <param name="centerDelta">Delta between the element's center and the tilt container's</param>
  276. /// <returns>true if successful; false otherwise</returns>
  277. /// <remarks>
  278. /// This method is conservative; it will fail any attempt to tilt a control that already
  279. /// has a projection on it
  280. /// </remarks>
  281. static bool PrepareControlForTilt(FrameworkElement element, Point centerDelta)
  282. {
  283. // Prevents interference with any existing transforms
  284. if (element.Projection != null || (element.RenderTransform != null && element.RenderTransform.GetType() != typeof(MatrixTransform)))
  285. return false;
  286. TranslateTransform transform = new TranslateTransform();
  287. transform.X = centerDelta.X;
  288. transform.Y = centerDelta.Y;
  289. element.RenderTransform = transform;
  290. PlaneProjection projection = new PlaneProjection();
  291. projection.GlobalOffsetX = -1 * centerDelta.X;
  292. projection.GlobalOffsetY = -1 * centerDelta.Y;
  293. element.Projection = projection;
  294. element.ManipulationDelta += TiltEffect_ManipulationDelta;
  295. element.ManipulationCompleted += TiltEffect_ManipulationCompleted;
  296. return true;
  297. }
  298. /// <summary>
  299. /// Removes modifications made by PrepareControlForTilt
  300. /// </summary>
  301. /// <param name="element">THe control to be un-prepared</param>
  302. /// <remarks>
  303. /// This method is basic; it does not do anything to detect if the control being un-prepared
  304. /// was previously prepared
  305. /// </remarks>
  306. static void RevertPrepareControlForTilt(FrameworkElement element)
  307. {
  308. element.ManipulationDelta -= TiltEffect_ManipulationDelta;
  309. element.ManipulationCompleted -= TiltEffect_ManipulationCompleted;
  310. element.Projection = null;
  311. element.RenderTransform = null;
  312. }
  313. /// <summary>
  314. /// Creates the tilt return storyboard (if not already created) and targets it to the projection
  315. /// </summary>
  316. /// <param name="projection">the projection that should be the target of the animation</param>
  317. static void PrepareTiltReturnStoryboard(FrameworkElement element)
  318. {
  319. if (tiltReturnStoryboard == null)
  320. {
  321. tiltReturnStoryboard = new Storyboard();
  322. tiltReturnStoryboard.Completed += TiltReturnStoryboard_Completed;
  323. tiltReturnXAnimation = new DoubleAnimation();
  324. Storyboard.SetTargetProperty(tiltReturnXAnimation, new PropertyPath(PlaneProjection.RotationXProperty));
  325. tiltReturnXAnimation.BeginTime = TiltReturnAnimationDelay;
  326. tiltReturnXAnimation.To = 0;
  327. tiltReturnXAnimation.Duration = TiltReturnAnimationDuration;
  328. tiltReturnYAnimation = new DoubleAnimation();
  329. Storyboard.SetTargetProperty(tiltReturnYAnimation, new PropertyPath(PlaneProjection.RotationYProperty));
  330. tiltReturnYAnimation.BeginTime = TiltReturnAnimationDelay;
  331. tiltReturnYAnimation.To = 0;
  332. tiltReturnYAnimation.Duration = TiltReturnAnimationDuration;
  333. tiltReturnZAnimation = new DoubleAnimation();
  334. Storyboard.SetTargetProperty(tiltReturnZAnimation, new PropertyPath(PlaneProjection.GlobalOffsetZProperty));
  335. tiltReturnZAnimation.BeginTime = TiltReturnAnimationDelay;
  336. tiltReturnZAnimation.To = 0;
  337. tiltReturnZAnimation.Duration = TiltReturnAnimationDuration;
  338. if (UseLogarithmicEase)
  339. {
  340. tiltReturnXAnimation.EasingFunction = new LogarithmicEase();
  341. tiltReturnYAnimation.EasingFunction = new LogarithmicEase();
  342. tiltReturnZAnimation.EasingFunction = new LogarithmicEase();
  343. }
  344. tiltReturnStoryboard.Children.Add(tiltReturnXAnimation);
  345. tiltReturnStoryboard.Children.Add(tiltReturnYAnimation);
  346. tiltReturnStoryboard.Children.Add(tiltReturnZAnimation);
  347. }
  348. Storyboard.SetTarget(tiltReturnXAnimation, element.Projection);
  349. Storyboard.SetTarget(tiltReturnYAnimation, element.Projection);
  350. Storyboard.SetTarget(tiltReturnZAnimation, element.Projection);
  351. }
  352. /// <summary>
  353. /// Continues a tilt effect that is currently applied to an element, presumably because
  354. /// the user moved their finger
  355. /// </summary>
  356. /// <param name="element">The element being tilted</param>
  357. /// <param name="e">The manipulation event args</param>
  358. static void ContinueTiltEffect(FrameworkElement element, ManipulationDeltaEventArgs e)
  359. {
  360. FrameworkElement container = e.ManipulationContainer as FrameworkElement;
  361. if (container == null || element == null)
  362. return;
  363. Point tiltTouchPoint = container.TransformToVisual(element).Transform(e.ManipulationOrigin);
  364. // If touch moved outside bounds of element, then pause the tilt (but don't cancel it)
  365. if (new Rect(0, 0, currentTiltElement.ActualWidth, currentTiltElement.ActualHeight).Contains(tiltTouchPoint) != true)
  366. {
  367. PauseTiltEffect();
  368. return;
  369. }
  370. // Apply the updated tilt effect
  371. ApplyTiltEffect(currentTiltElement, e.ManipulationOrigin, currentTiltElementCenter);
  372. }
  373. /// <summary>
  374. /// Ends the tilt effect by playing the animation
  375. /// </summary>
  376. /// <param name="element">The element being tilted</param>
  377. static void EndTiltEffect(FrameworkElement element)
  378. {
  379. if (element != null)
  380. {
  381. element.ManipulationCompleted -= TiltEffect_ManipulationCompleted;
  382. element.ManipulationDelta -= TiltEffect_ManipulationDelta;
  383. }
  384. if (tiltReturnStoryboard != null)
  385. {
  386. wasPauseAnimation = false;
  387. if (tiltReturnStoryboard.GetCurrentState() != ClockState.Active)
  388. tiltReturnStoryboard.Begin();
  389. }
  390. else
  391. StopTiltReturnStoryboardAndCleanup();
  392. }
  393. /// <summary>
  394. /// Handler for the storyboard complete event
  395. /// </summary>
  396. /// <param name="sender">sender of the event</param>
  397. /// <param name="e">event args</param>
  398. static void TiltReturnStoryboard_Completed(object sender, EventArgs e)
  399. {
  400. if (wasPauseAnimation)
  401. ResetTiltEffect(currentTiltElement);
  402. else
  403. StopTiltReturnStoryboardAndCleanup();
  404. }
  405. /// <summary>
  406. /// Resets the tilt effect on the control, making it appear 'normal' again
  407. /// </summary>
  408. /// <param name="element">The element to reset the tilt on</param>
  409. /// <remarks>
  410. /// This method doesn't turn off the tilt effect or cancel any current
  411. /// manipulation; it just temporarily cancels the effect
  412. /// </remarks>
  413. static void ResetTiltEffect(FrameworkElement element)
  414. {
  415. PlaneProjection projection = element.Projection as PlaneProjection;
  416. projection.RotationY = 0;
  417. projection.RotationX = 0;
  418. projection.GlobalOffsetZ = 0;
  419. }
  420. /// <summary>
  421. /// Stops the tilt effect and release resources applied to the currently-tilted control
  422. /// </summary>
  423. static void StopTiltReturnStoryboardAndCleanup()
  424. {
  425. if (tiltReturnStoryboard != null)
  426. tiltReturnStoryboard.Stop();
  427. RevertPrepareControlForTilt(currentTiltElement);
  428. }
  429. /// <summary>
  430. /// Pauses the tilt effect so that the control returns to the 'at rest' position, but doesn't
  431. /// stop the tilt effect (handlers are still attached, etc.)
  432. /// </summary>
  433. static void PauseTiltEffect()
  434. {
  435. if ((tiltReturnStoryboard != null) && !wasPauseAnimation)
  436. {
  437. tiltReturnStoryboard.Stop();
  438. wasPauseAnimation = true;
  439. tiltReturnStoryboard.Begin();
  440. }
  441. }
  442. /// <summary>
  443. /// Resets the storyboard to not running
  444. /// </summary>
  445. private static void ResetTiltReturnStoryboard()
  446. {
  447. tiltReturnStoryboard.Stop();
  448. wasPauseAnimation = false;
  449. }
  450. /// <summary>
  451. /// Applies the tilt effect to the control
  452. /// </summary>
  453. /// <param name="element">the control to tilt</param>
  454. /// <param name="touchPoint">The touch point, in the container's coordinates</param>
  455. /// <param name="centerPoint">The center point of the container</param>
  456. static void ApplyTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint)
  457. {
  458. // Stop any active animation
  459. ResetTiltReturnStoryboard();
  460. // Get relative point of the touch in percentage of container size
  461. Point normalizedPoint = new Point(
  462. Math.Min(Math.Max(touchPoint.X / (centerPoint.X * 2), 0), 1),
  463. Math.Min(Math.Max(touchPoint.Y / (centerPoint.Y * 2), 0), 1));
  464. // Shell values
  465. double xMagnitude = Math.Abs(normalizedPoint.X - 0.5);
  466. double yMagnitude = Math.Abs(normalizedPoint.Y - 0.5);
  467. double xDirection = -Math.Sign(normalizedPoint.X - 0.5);
  468. double yDirection = Math.Sign(normalizedPoint.Y - 0.5);
  469. double angleMagnitude = xMagnitude + yMagnitude;
  470. double xAngleContribution = xMagnitude + yMagnitude > 0 ? xMagnitude / (xMagnitude + yMagnitude) : 0;
  471. double angle = angleMagnitude * MaxAngle * 180 / Math.PI;
  472. double depression = (1 - angleMagnitude) * MaxDepression;
  473. // RotationX and RotationY are the angles of rotations about the x- or y-*axis*;
  474. // to achieve a rotation in the x- or y-*direction*, we need to swap the two.
  475. // That is, a rotation to the left about the y-axis is a rotation to the left in the x-direction,
  476. // and a rotation up about the x-axis is a rotation up in the y-direction.
  477. PlaneProjection projection = element.Projection as PlaneProjection;
  478. projection.RotationY = angle * xAngleContribution * xDirection;
  479. projection.RotationX = angle * (1 - xAngleContribution) * yDirection;
  480. projection.GlobalOffsetZ = -depression;
  481. }
  482. #endregion
  483. #region Custom easing function
  484. /// <summary>
  485. /// Provides an easing function for the tilt return
  486. /// </summary>
  487. private class LogarithmicEase : EasingFunctionBase
  488. {
  489. /// <summary>
  490. /// Computes the easing function
  491. /// </summary>
  492. /// <param name="normalizedTime">The time</param>
  493. /// <returns>The eased value</returns>
  494. protected override double EaseInCore(double normalizedTime)
  495. {
  496. return Math.Log(normalizedTime + 1) / 0.693147181; // ln(t + 1) / ln(2)
  497. }
  498. }
  499. #endregion
  500. }
  501. /// <summary>
  502. /// Couple of simple helpers for walking the visual tree
  503. /// </summary>
  504. static class TreeHelpers
  505. {
  506. /// <summary>
  507. /// Gets the ancestors of the element, up to the root
  508. /// </summary>
  509. /// <param name="node">The element to start from</param>
  510. /// <returns>An enumerator of the ancestors</returns>
  511. public static IEnumerable<FrameworkElement> GetVisualAncestors(this FrameworkElement node)
  512. {
  513. FrameworkElement parent = node.GetVisualParent();
  514. while (parent != null)
  515. {
  516. yield return parent;
  517. parent = parent.GetVisualParent();
  518. }
  519. }
  520. /// <summary>
  521. /// Gets the visual parent of the element
  522. /// </summary>
  523. /// <param name="node">The element to check</param>
  524. /// <returns>The visual parent</returns>
  525. public static FrameworkElement GetVisualParent(this FrameworkElement node)
  526. {
  527. return VisualTreeHelper.GetParent(node) as FrameworkElement;
  528. }
  529. }
  530. }