Carousel user control for Windows 8.1 and Windows Phone 8.1 : ManipulationDelta, ArrangeOverride and Animation

Hi;

Today, here is the last part of the carousel user control creation for Windows 8.1 and Windows Phone 8.1

For recap; here are the first parts of this series :

  1. Part 1 : Create a custom user control using Xaml and C# for Windows 8.
  2. Part 2 : Upgrading a Windows 8.0 component to Windows 8.1 and Windows Phone 8.1 (WinRT)

 

Introduction

In this last part, we will again upgrading our component to add some gestures. Here are two videos showing before and after adding gestures features

As you can see in the first video there is no gesture handled when you move your finger. the only event raised is the index change when you made a translation

Here is the carousel with then new gesture features. As you can see, the carousel is smoother :

And of course, the Windows Phone 8.1 version as well :

Github

Now that the control is terminated, you can get the full source code from Github : https://github.com/Mimetis/LightStone

image

 

Transformation : PlaneProjection

To handle our new gesture feature, we need to know, for all items where they must be placed.
As you know, we handle position with a PlaneProjection (because we move on X,Y and Z axis)

The GetProjection method was created to return a Tuple of X,Y and Z values. It tooks an item index and the movement delta from its original position :

  1 private Tuple<Double, Double, Double, Double> GetProjection(int i, Double deltaX)
 2 {
 3 
 4     var isLeftToRight = deltaX > 0;
 5     var isRightToLeft = !isLeftToRight;
 6 
 7      Double newDepth = -this.Depth;
 8 
 9     Double initialRotation = (i == this.SelectedIndex) ? 0d : ((i < this.SelectedIndex) ? Rotation : -Rotation);
10     Double newRotation = 0d;
11 
12     Double offsetX = (i == this.SelectedIndex) ? 0 : (i - this.SelectedIndex) * desiredWidth;
13     Double translateX = (i == this.SelectedIndex) ? 0 : ((i < this.SelectedIndex) ? -TranslateX : TranslateX);
14     Double initialOffsetX = offsetX + translateX;
15     Double newOffsetX = 0d;
16 
17     var translateY = TranslateY;
18 
19 
20     if (i == this.SelectedIndex)
21     {
22         // rotation is from -Rotation to Rotation
23         // We get the proportional of deltaX by desiredWidth
24         newRotation = initialRotation - Rotation * deltaX / desiredWidth;
25         
26         // the offset max is the Sum(TranslateX + desiredWidth)
27         // We get the proportional too
28         newOffsetX = deltaX * (TranslateX + desiredWidth) / desiredWidth;
29     }
30     // only the first item on the left or right is moving on x, z, and d
31     else if ((i == this.SelectedIndex - 1 && isLeftToRight) || (i == this.SelectedIndex + 1 && isRightToLeft))
32     {
33         // We get the rotation (proportional from delta to desiredwidth, always)
34         // by far the initial position is Rotation, so we made a subsraction
35         newRotation = initialRotation - Rotation * deltaX / desiredWidth;
36 
37         // The Translation is decreasing to 0
38         newOffsetX = initialOffsetX - initialOffsetX * Math.Abs(deltaX) / desiredWidth;
39     }
40 
41     // Other items just moved on x
42     else
43     {
44         newOffsetX = initialOffsetX + deltaX;
45 
46         newRotation = initialRotation;
47     }
48 
49     return new Tuple<Double, Double, Double, Double>(newOffsetX, translateY, newDepth, newRotation);
50 
51 
52 }

ManipulationDelta, ArrangeOverride, Animation

Here are the 3 most important methods in the animation of our control. To summarize :

  1. ArrangeOverride : called every time a render is needed (initial load for example) . Called after any manipulation or animation
  2. ManipulationDelta : called during any gesture (in conjonction with ManipulationEnd) This method is called when we will “shift” an item on right or left
  3. Animation : (UpdatePosition method) Called at the end of a manipulation to move the items to their final position

 

ArrangeOverride

To keep it simple, this method is called on every render, except when any manipulation / animation occured. It’s called for example during the first rendering of our control. ArrangeOverride is responsible of the initial position of each item.

As you can imagine, ArrangeOverride called the GetProjection method with a value of 0 for the delta parameter

Here is a simplified version of the ArrangeOverride method:

  1 protected override Size ArrangeOverride(Size finalSize)
 2 {
 3     Double centerLeft = 0;
 4     Double centerTop = 0;
 5 
 6     this.Clip = new RectangleGeometry 
 7     { Rect = new Rect(0, 0, finalSize.Width, finalSize.Height) };
 8 
 9     for (int i = 0; i < this.internalList.Count; i++)
10     {
11         UIElement container = internalList[i];
12 
13         Size desiredSize = container.DesiredSize;
14         if (double.IsNaN(desiredSize.Width) || 
15         double.IsNaN(desiredSize.Height)) continue;
16 
17         // get the good center and top position
18         if (centerLeft == 0 && centerTop == 0 
19         && desiredSize.Width > 0 && desiredSize.Height > 0)
20         {
21             desiredWidth = desiredSize.Width;
22             desiredHeight = desiredSize.Height;
23 
24             centerLeft = (finalSize.Width / 2) - (desiredWidth / 2);
25             centerTop = (finalSize.Height - desiredHeight) / 2;
26         }
27 
28         // Get position from SelectedIndex
29         var deltaFromSelectedIndex = Math.Abs(this.SelectedIndex - i);
30 
31         // Get rect position
32         var rect = new Rect(centerLeft, centerTop, desiredWidth, 
33         desiredHeight);
34 
35         container.Arrange(rect);
36         Canvas.SetLeft(container, centerLeft);
37         Canvas.SetTop(container, centerTop);
38 
39         // Apply Transform
40         PlaneProjection planeProjection = container.Projection 
41         as PlaneProjection;
42 
43         if (planeProjection == null)
44             continue;
45 
46         // Get an initial projection (without move)
47         var props = GetProjection(i, 0d);
48 
49         planeProjection.LocalOffsetX = props.Item1;
50         planeProjection.GlobalOffsetY = props.Item2;
51         planeProjection.GlobalOffsetZ = props.Item3;
52         planeProjection.RotationY = props.Item4;
53 
54     }
55 
56      return finalSize;
57 }
58 

ManipulationDelta

This method is called for every manipulation engaged. We dont create animations, we just modify the PlaneProjection transformation within each item.

We will handle the ManipulationCompleted event too, to :

  1. Change the selected index and run the animation.
  2. Reset a maniupulation due to lack of movement
  3. Handle some situations like the first of the last item of the Carousel list

 

  1 private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
 2 {
 3     var deltaX = e.Cumulative.Translation.X;
 4 
 5     if (deltaX > desiredWidth)
 6         deltaX = desiredWidth;
 7 
 8     if (deltaX < -desiredWidth)
 9         deltaX = -desiredWidth;
10 
11     // Dont animate all items
12     var inf = this.SelectedIndex - (MaxVisibleItems * 2);
13     var sup = this.SelectedIndex + (MaxVisibleItems * 2);
14 
15     for (int i = 0; i < this.internalList.Count; i++)
16     {
17         // Dont animate all items
18         if (i < inf || i > sup)
19             continue;
20 
21         var item = internalList[i];
22 
23         PlaneProjection planeProjection = item.Projection as PlaneProjection;
24 
25         if (planeProjection == null)
26             continue;
27 
28         // Get the new projection for the current item
29         var props = GetProjection(i, deltaX);
30         planeProjection.LocalOffsetX = props.Item1;
31         planeProjection.GlobalOffsetY = props.Item2;
32         planeProjection.GlobalOffsetZ = props.Item3;
33         planeProjection.RotationY = props.Item4;
34 
35     }
36 }

Animation

The UpdatePosition method is called when we have to move all the items to a new position. This method is used when:

  1. We change the selected index  (when Tap or with code)
  2. We stop a maniuplation and we need to move the items to their last position.
  3. We add / remove items

 

  1 private void UpdatePosition()
 2 {
 3     storyboard = new Storyboard();
 4 
 5     for (int i = 0; i < this.internalList.Count; i++)
 6     {
 7         // Do not animate all items
 8         var inf = this.SelectedIndex - (MaxVisibleItems * 2);
 9         var sup = this.SelectedIndex + (MaxVisibleItems * 2);
10 
11         if (i < inf || i > sup)
12             continue;
13 
14         var item = internalList[i];
15 
16         PlaneProjection planeProjection = item.Projection as PlaneProjection;
17 
18         if (planeProjection == null)
19             continue;
20 
21         // Get target projection
22         var props = GetProjection(i, 0d);
23 
24         storyboard.AddAnimation(item, TransitionDuration, props.Item1, "(UIElement.Projection).(PlaneProjection.LocalOffsetX)", this.EasingFunction);
25         storyboard.AddAnimation(item, TransitionDuration, props.Item2, "(UIElement.Projection).(PlaneProjection.GlobalOffsetY)", this.EasingFunction);
26         storyboard.AddAnimation(item, TransitionDuration, props.Item3, "(UIElement.Projection).(PlaneProjection.GlobalOffsetZ)", this.EasingFunction);
27         storyboard.AddAnimation(item, TransitionDuration, props.Item4, "(UIElement.Projection).(PlaneProjection.RotationY)", this.EasingFunction);
28         storyboard.AddAnimation(item, TransitionDuration, opacity, "Opacity", this.EasingFunction);
29 
30     }
31 
32     // When storyboard completed, Invalidate
33     storyboard.Completed += (sender, o) =>
34     {
35         this.isUpdatingPosition = false;
36         this.InvalidateArrange();
37     };
38 
39     storyboard.Begin();
40 }

/Seb