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