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 :
- Part 1 : Create a custom user control using Xaml and C# for Windows 8.
- 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
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 :
- ArrangeOverride : called every time a render is needed (initial load for example) . Called after any manipulation or animation
- ManipulationDelta : called during any gesture (in conjonction with ManipulationEnd) This method is called when we will “shift” an item on right or left
- 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 :
- Change the selected index and run the animation.
- Reset a maniupulation due to lack of movement
- 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:
- We change the selected index (when Tap or with code)
- We stop a maniuplation and we need to move the items to their last position.
- 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
Comments
Anonymous
June 15, 2014
This is a cool representation of an other type of carousel control. Great job SergeAnonymous
June 30, 2014
Thanks for the wonderful control this was really helpful :) It is possible to make the carousel rotating automatically? Waiting for your reply.Anonymous
July 01, 2014
@Serge : Thx a lot ! @Suresh : I didn't test, but I think a timer binded on the selectedIndex should be a good starting solution :)Anonymous
July 01, 2014
@Suresh : Here is a code that works fine for me. DispatcherTimer timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromSeconds(1d); timer.Tick += (s, e) => { timer.Stop(); if (LightStoneElement.SelectedIndex == ((IList)LightStoneElement.ItemsSource).Count - 1) LightStoneElement.SelectedIndex = 0; else LightStoneElement.SelectedIndex += 1; timer.Start(); }; timer.Start();Anonymous
July 08, 2014
Great work man, it's really cool. I have two requests:
- How can I eliminate the reflection effect at the the base of every image?
- How could I make the carousel completely circular? Thanks in advance for the help.
- Anonymous
July 09, 2014
Very good solution for carousel, but i have a question:
- why when changing color background, half of the screen changes color, the other half is black? I can not find a solution to this problem. Thanks in advance for the help.