PageRenderTime 37ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/TiltEffect.cs

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