MetroInMotion.cs (17077B)
1 using System; 2 using System.Net; 3 using System.Linq; 4 using System.Windows; 5 using System.Windows.Controls; 6 using System.Windows.Documents; 7 using System.Windows.Ink; 8 using System.Windows.Input; 9 using System.Windows.Media; 10 using System.Windows.Media.Animation; 11 using System.Windows.Shapes; 12 using Microsoft.Phone.Controls; 13 using LinqToVisualTree; 14 using System.Diagnostics; 15 using System.Collections.Generic; 16 using System.Windows.Controls.Primitives; 17 using System.Windows.Media.Imaging; 18 19 namespace MetroInMotionUtils 20 { 21 public static class MetroInMotion 22 { 23 #region AnimationLevel 24 25 public static int GetAnimationLevel(DependencyObject obj) 26 { 27 return (int)obj.GetValue(AnimationLevelProperty); 28 } 29 30 public static void SetAnimationLevel(DependencyObject obj, int value) 31 { 32 obj.SetValue(AnimationLevelProperty, value); 33 } 34 35 36 public static readonly DependencyProperty AnimationLevelProperty = 37 DependencyProperty.RegisterAttached("AnimationLevel", typeof(int), 38 typeof(MetroInMotion), new PropertyMetadata(-1)); 39 40 #endregion 41 42 #region Tilt 43 44 public static double GetTilt(DependencyObject obj) 45 { 46 return (double)obj.GetValue(TiltProperty); 47 } 48 49 public static void SetTilt(DependencyObject obj, double value) 50 { 51 obj.SetValue(TiltProperty, value); 52 } 53 54 55 public static readonly DependencyProperty TiltProperty = 56 DependencyProperty.RegisterAttached("Tilt", typeof(double), 57 typeof(MetroInMotion), new PropertyMetadata(2.0, OnTiltChanged)); 58 59 /// <summary> 60 /// The extent of the tilt action, the larger the number, the bigger the tilt 61 /// </summary> 62 private static double TiltAngleFactor = 4; 63 64 /// <summary> 65 /// The extent of the scaling action, the smaller the number, the greater the scaling. 66 /// </summary> 67 private static double ScaleFactor = 100; 68 69 private static void OnTiltChanged(DependencyObject d, 70 DependencyPropertyChangedEventArgs args) 71 { 72 FrameworkElement targetElement = d as FrameworkElement; 73 74 double tiltFactor = GetTilt(d); 75 76 // create the required transformations 77 var projection = new PlaneProjection(); 78 var scale = new ScaleTransform(); 79 var translate = new TranslateTransform(); 80 81 var transGroup = new TransformGroup(); 82 transGroup.Children.Add(scale); 83 transGroup.Children.Add(translate); 84 85 // associate with the target element 86 targetElement.Projection = projection; 87 targetElement.RenderTransform = transGroup; 88 targetElement.RenderTransformOrigin = new Point(0.5, 0.5); 89 90 targetElement.MouseLeftButtonDown += (s, e) => 91 { 92 var clickPosition = e.GetPosition(targetElement); 93 94 // find the maximum of width / height 95 double maxDimension = Math.Max(targetElement.ActualWidth, targetElement.ActualHeight); 96 97 // compute the normalised horizontal distance from the centre 98 double distanceFromCenterX = targetElement.ActualWidth / 2 - clickPosition.X; 99 double normalisedDistanceX = 2 * distanceFromCenterX / maxDimension; 100 101 // rotate around the Y axis 102 projection.RotationY = normalisedDistanceX * TiltAngleFactor * tiltFactor; 103 104 // compute the normalised vertical distance from the centre 105 double distanceFromCenterY = targetElement.ActualHeight / 2 - clickPosition.Y; 106 double normalisedDistanceY = 2 * distanceFromCenterY / maxDimension; 107 108 // rotate around the X axis, 109 projection.RotationX = -normalisedDistanceY * TiltAngleFactor * tiltFactor; 110 111 // find the distance to centre 112 double distanceToCentre = Math.Sqrt(normalisedDistanceX * normalisedDistanceX 113 + normalisedDistanceY * normalisedDistanceY); 114 115 // scale accordingly 116 double scaleVal = tiltFactor * (1 - distanceToCentre) / ScaleFactor; 117 scale.ScaleX = 1 - scaleVal; 118 scale.ScaleY = 1 - scaleVal; 119 120 // offset the plane transform 121 var rootElement = Application.Current.RootVisual as FrameworkElement; 122 var relativeToCentre = (targetElement.GetRelativePosition(rootElement).Y - rootElement.ActualHeight / 2) / 2; 123 translate.Y = -relativeToCentre; 124 projection.LocalOffsetY = +relativeToCentre; 125 126 }; 127 128 targetElement.ManipulationCompleted += (s, e) => 129 { 130 var sb = new Storyboard(); 131 sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationY", projection)); 132 sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationX", projection)); 133 sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleX", scale)); 134 sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleY", scale)); 135 sb.Begin(); 136 137 translate.Y = 0; 138 projection.LocalOffsetY = 0; 139 }; 140 141 } 142 143 144 #endregion 145 146 #region IsPivotAnimated 147 148 public static bool GetIsPivotAnimated(DependencyObject obj) 149 { 150 return (bool)obj.GetValue(IsPivotAnimatedProperty); 151 } 152 153 public static void SetIsPivotAnimated(DependencyObject obj, bool value) 154 { 155 obj.SetValue(IsPivotAnimatedProperty, value); 156 } 157 158 public static readonly DependencyProperty IsPivotAnimatedProperty = 159 DependencyProperty.RegisterAttached("IsPivotAnimated", typeof(bool), 160 typeof(MetroInMotion), new PropertyMetadata(false, OnIsPivotAnimatedChanged)); 161 162 private static void OnIsPivotAnimatedChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) 163 { 164 ItemsControl list = d as ItemsControl; 165 166 list.Loaded += (s2, e2) => 167 { 168 // locate the pivot control that this list is within 169 Pivot pivot = list.Ancestors<Pivot>().Single() as Pivot; 170 171 // and its index within the pivot 172 int pivotIndex = pivot.Items.IndexOf(list.Ancestors<PivotItem>().Single()); 173 174 bool selectionChanged = false; 175 176 pivot.SelectionChanged += (s3, e3) => 177 { 178 selectionChanged = true; 179 }; 180 181 // handle manipulation events which occur when the user 182 // moves between pivot items 183 pivot.ManipulationCompleted += (s, e) => 184 { 185 if (!selectionChanged) 186 return; 187 188 selectionChanged = false; 189 190 if (pivotIndex != pivot.SelectedIndex) 191 return; 192 193 // determine which direction this tab will be scrolling in from 194 bool fromRight = e.TotalManipulation.Translation.X <= 0; 195 196 197 // iterate over each of the items in view 198 var items = list.GetItemsInView().ToList(); 199 for (int index = 0; index < items.Count; index++ ) 200 { 201 var lbi = items[index]; 202 203 list.Dispatcher.BeginInvoke(() => 204 { 205 var animationTargets = lbi.Descendants() 206 .Where(p => MetroInMotion.GetAnimationLevel(p) > -1); 207 foreach (FrameworkElement target in animationTargets) 208 { 209 // trigger the required animation 210 GetSlideAnimation(target, fromRight).Begin(); 211 } 212 }); 213 }; 214 215 }; 216 }; 217 } 218 219 220 #endregion 221 222 /// <summary> 223 /// Animates each element in order, creating a 'peel' effect. The supplied action 224 /// is invoked when the animation ends. 225 /// </summary> 226 public static void Peel(this IEnumerable<FrameworkElement> elements, Action endAction) 227 { 228 var elementList = elements.ToList(); 229 var lastElement = elementList.Last(); 230 231 // iterate over all the elements, animating each of them 232 double delay = 0; 233 foreach (FrameworkElement element in elementList) 234 { 235 var sb = GetPeelAnimation(element, delay); 236 237 // add a Completed event handler to the last element 238 if (element.Equals(lastElement)) 239 { 240 sb.Completed += (s, e) => 241 { 242 endAction(); 243 }; 244 } 245 246 sb.Begin(); 247 delay += 50; 248 } 249 } 250 251 252 /// <summary> 253 /// Enumerates all the items that are currently visible in am ItemsControl. This implementation assumes 254 /// that a VirtualizingStackPanel is being used as the ItemsPanel. 255 /// </summary> 256 public static IEnumerable<FrameworkElement> GetItemsInView(this ItemsControl itemsControl) 257 { 258 // locate the stack panel that hosts the items 259 VirtualizingStackPanel vsp = itemsControl.Descendants<VirtualizingStackPanel>().First() as VirtualizingStackPanel; 260 261 // iterate over each of the items in view 262 int firstVisibleItem = (int)vsp.VerticalOffset; 263 int visibleItemCount = (int)vsp.ViewportHeight; 264 for (int index = firstVisibleItem; index <= firstVisibleItem + visibleItemCount + 1; index++) 265 { 266 var item = itemsControl.ItemContainerGenerator.ContainerFromIndex(index) as FrameworkElement; 267 if (item == null) 268 continue; 269 270 yield return item; 271 } 272 } 273 274 /// <summary> 275 /// Creates a PlaneProjection and associates it with the given element, returning 276 /// a Storyboard which will animate the PlaneProjection to 'peel' the item 277 /// from the screen. 278 /// </summary> 279 private static Storyboard GetPeelAnimation(FrameworkElement element, double delay) 280 { 281 Storyboard sb; 282 283 var projection = new PlaneProjection() 284 { 285 CenterOfRotationX = -0.1 286 }; 287 element.Projection = projection; 288 289 // compute the angle of rotation required to make this element appear 290 // at a 90 degree angle at the edge of the screen. 291 var width = element.ActualWidth; 292 var targetAngle = Math.Atan(1000 / (width / 2)); 293 targetAngle = targetAngle * 180 / Math.PI; 294 295 // animate the projection 296 sb = new Storyboard(); 297 sb.BeginTime = TimeSpan.FromMilliseconds(delay); 298 sb.Children.Add(CreateAnimation(0, -(180 - targetAngle), 0.3, "RotationY", projection)); 299 sb.Children.Add(CreateAnimation(0, 23, 0.3, "RotationZ", projection)); 300 sb.Children.Add(CreateAnimation(0, -23, 0.3, "GlobalOffsetZ", projection)); 301 return sb; 302 } 303 304 private static DoubleAnimation CreateAnimation(double? from, double? to, double duration, 305 string targetProperty, DependencyObject target) 306 { 307 var db = new DoubleAnimation(); 308 db.To = to; 309 db.From = from; 310 db.EasingFunction = new SineEase(); 311 db.Duration = TimeSpan.FromSeconds(duration); 312 Storyboard.SetTarget(db, target); 313 Storyboard.SetTargetProperty(db, new PropertyPath(targetProperty)); 314 return db; 315 } 316 317 /// <summary> 318 /// Creates a TranslateTransform and associates it with the given element, returning 319 /// a Storyboard which will animate the TranslateTransform with a SineEase function 320 /// </summary> 321 private static Storyboard GetSlideAnimation(FrameworkElement element, bool fromRight) 322 { 323 double from = fromRight ? 80 : -80; 324 325 Storyboard sb; 326 double delay = (MetroInMotion.GetAnimationLevel(element)) * 0.1 + 0.1; 327 328 TranslateTransform trans = new TranslateTransform() { X = from }; 329 element.RenderTransform = trans; 330 331 sb = new Storyboard(); 332 sb.BeginTime = TimeSpan.FromSeconds(delay); 333 sb.Children.Add(CreateAnimation(from, 0, 0.8, "X", trans)); 334 return sb; 335 } 336 337 } 338 339 public static class ExtensionMethods 340 { 341 public static Point GetRelativePosition(this UIElement element, UIElement other) 342 { 343 return element.TransformToVisual(other) 344 .Transform(new Point(0, 0)); 345 } 346 } 347 348 /// <summary> 349 /// Animates an element so that it flies out and flies in! 350 /// </summary> 351 public class ItemFlyInAndOutAnimations 352 { 353 private Popup _popup; 354 355 private Canvas _popupCanvas; 356 357 private FrameworkElement _targetElement; 358 359 private Point _targetElementPosition; 360 361 private Image _targetElementClone; 362 363 private Rectangle _backgroundMask; 364 365 private static TimeSpan _flyInSpeed = TimeSpan.FromMilliseconds(200); 366 367 private static TimeSpan _flyOutSpeed = TimeSpan.FromMilliseconds(300); 368 369 public ItemFlyInAndOutAnimations() 370 { 371 // construct a popup, with a Canvas as its child 372 _popup = new Popup(); 373 _popupCanvas = new Canvas(); 374 _popup.Child = _popupCanvas; 375 } 376 377 public static void TitleFlyIn(FrameworkElement title) 378 { 379 TranslateTransform trans = new TranslateTransform(); 380 trans.X = 300; 381 trans.Y = -50; 382 title.RenderTransform = trans; 383 384 var sb = new Storyboard(); 385 386 // animate the X position 387 var db = CreateDoubleAnimation(300, 0, 388 new SineEase(), trans, TranslateTransform.XProperty, _flyInSpeed); 389 sb.Children.Add(db); 390 391 // animate the Y position 392 db = CreateDoubleAnimation(-100, 0, 393 new SineEase(), trans, TranslateTransform.YProperty, _flyInSpeed); 394 sb.Children.Add(db); 395 396 sb.Begin(); 397 } 398 399 /// <summary> 400 /// Animate the previously 'flown-out' element back to its original location. 401 /// </summary> 402 public void ItemFlyIn() 403 { 404 if (_popupCanvas.Children.Count != 2) 405 return; 406 407 _popup.IsOpen = true; 408 _backgroundMask.Opacity = 0.0; 409 410 Image animatedImage = _popupCanvas.Children[1] as Image; 411 412 var sb = new Storyboard(); 413 414 // animate the X position 415 var db = CreateDoubleAnimation(_targetElementPosition.X - 100, _targetElementPosition.X, 416 new SineEase(), 417 _targetElementClone, Canvas.LeftProperty, _flyInSpeed); 418 sb.Children.Add(db); 419 420 // animate the Y position 421 db = CreateDoubleAnimation(_targetElementPosition.Y - 50, _targetElementPosition.Y, 422 new SineEase(), 423 _targetElementClone, Canvas.TopProperty, _flyInSpeed); 424 sb.Children.Add(db); 425 426 sb.Completed += (s, e) => 427 { 428 // when the animation has finished, hide the popup once more 429 _popup.IsOpen = false; 430 431 // restore the element we have animated 432 _targetElement.Opacity = 1.0; 433 434 // and get rid of our clone 435 _popupCanvas.Children.Clear(); 436 }; 437 438 sb.Begin(); 439 } 440 441 442 /// <summary> 443 /// Animate the given element so that it flies off screen, fading 444 /// everything else that is on screen. 445 /// </summary> 446 public void ItemFlyOut(FrameworkElement element, Action action) 447 { 448 _targetElement = element; 449 var rootElement = Application.Current.RootVisual as FrameworkElement; 450 451 _backgroundMask = new Rectangle() 452 { 453 Fill = new SolidColorBrush(Colors.Black), 454 Opacity = 0.0, 455 Width = rootElement.ActualWidth, 456 Height = rootElement.ActualHeight 457 }; 458 _popupCanvas.Children.Add(_backgroundMask); 459 460 _targetElementClone = new Image() 461 { 462 Source = new WriteableBitmap(element, null) 463 }; 464 _popupCanvas.Children.Add(_targetElementClone); 465 466 _targetElementPosition = element.GetRelativePosition(rootElement); 467 Canvas.SetTop(_targetElementClone, _targetElementPosition.Y); 468 Canvas.SetLeft(_targetElementClone, _targetElementPosition.X); 469 470 var sb = new Storyboard(); 471 472 // animate the X position 473 var db = CreateDoubleAnimation(_targetElementPosition.X, _targetElementPosition.X + 500, 474 new SineEase() { EasingMode = EasingMode.EaseIn }, 475 _targetElementClone, Canvas.LeftProperty, _flyOutSpeed); 476 sb.Children.Add(db); 477 478 // animate the Y position 479 db = CreateDoubleAnimation(_targetElementPosition.Y, _targetElementPosition.Y + 50, 480 new SineEase() { EasingMode = EasingMode.EaseOut }, 481 _targetElementClone, Canvas.TopProperty, _flyOutSpeed); 482 sb.Children.Add(db); 483 484 // fade out the other elements 485 db = CreateDoubleAnimation(0, 1, 486 null, _backgroundMask, UIElement.OpacityProperty, _flyOutSpeed); 487 sb.Children.Add(db); 488 489 sb.Completed += (s, e2) => 490 { 491 action(); 492 493 // hide the popup, by placing a task on the dispatcher queue, this 494 // should be executed after the navigation has occurred 495 element.Dispatcher.BeginInvoke(() => 496 { 497 _popup.IsOpen = false; 498 }); 499 }; 500 501 // hide the element we have 'cloned' into the popup 502 element.Opacity = 0.0; 503 504 // open the popup 505 _popup.IsOpen = true; 506 507 // begin the animation 508 sb.Begin(); 509 } 510 511 public static DoubleAnimation CreateDoubleAnimation(double from, double to, IEasingFunction easing, 512 DependencyObject target, object propertyPath, TimeSpan duration) 513 { 514 var db = new DoubleAnimation(); 515 db.To = to; 516 db.From = from; 517 db.EasingFunction = easing; 518 db.Duration = duration; 519 Storyboard.SetTarget(db, target); 520 Storyboard.SetTargetProperty(db, new PropertyPath(propertyPath)); 521 return db; 522 } 523 } 524 }