Création d’un contrôle Carousel Windows 8.1 Windows Phone 8.1 : ManipulationDelta, ArrangeOverride et Animation
Bonjour à tous;
Aujourd’hui dernière partie autour de la création d’un contrôle Carousel Windows 8.1 / Windows Phone 8.1.
Pour rappel; voici les précédentes parties:
- Partie 1 : Création du contrôle Carousel pour Windows 8.0 (une traduction en français est disponible sur Développez.com)
- Partie 2 : Migration du contrôle Carousel vers Windows 8.1 et Windows Phone 8.1 (Version Française ici)
Introduction
Dans cette dernière partie, nous allons améliorer le comportement du Carousel. Voici une vidéo du Carousel au départ. Notez que la gesture est binaire : Dès qu’on remarque une manipulation, on déclenche une animation complète:
Dans la nouvelle version, nous allons contrôler le Carousel pour avoir un comportement plus fluide et gérer la manipulation, comme on peut le voir sur cette vidéo:
Et la version Windows Phone 8.1 bien sûr :
Github
Parce que je me suis bien régalé à créer ce composant de A à Z, je l’ai mis sur Github, voilà si ça peut servir
https://github.com/Mimetis/LightStone
Transformation : PlaneProjection
Il faut bien comprendre les 3 méthodes nécessaires pour obtenir un rendu et une manipulation fluide.
Tout d’abord pour que l’accélération soit matérielle, il nous faut utiliser une Transformation. Dans notre cas, nous allons animer sur les axes X,Y et Z, donc nous utilisons une PlaneProjection
Je vous invite à lire le premier article sur le sujet.
Pour gérer la manipulation nous avons besoin d’une méthode pour connaître à tout moment, suivant la manipulation, quelle est la position de chaque élément.
Pour cela, nous avons donc créer une nouvelle méthode GetProjection, qui prend en argument l’index de l’élément en question, et le delta qui le sépare de sa position d’origine :
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
Voici les 3 méthodes les plus importantes dans la construction du contrôle:
Pour résumé:
- ArrangeOverride : Appellé lors du premier rendu et à la suite de chaque animation / manipulation
- ManipulationDelta : Appelé à chaque manipulation (en cohérence avec ManipulationEnd). Cette méthode va être appelée dès que nous allons “faire bouger” le carousel.
- Animation : (Méthode UpdatePosition) Appelé lors d’une fin de manipulation pour replacer les éléments dans une bonne position
ArrangeOverride
Pour faire simple, cette méthode est appelée à chaque rendu complet, hors manipulation est animation. Elle est appelée lors du premier rendu notamment. Mais aussi après une manipulation ou après une animation. Elle doit donc pouvoir positionner chaque élément précisément.
Elle appelle notamment la méthode GetProjection en appliquant un delta de 0.
Voici une version simplifiée de la méthode ArrangeOverride :
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
Cette méthode est appellé pour chaque manipulation. Ici on ne crée pas d’animations, on ne fait que modifier la PlaneProjection. Dans notre exemple, je me base sur un écart (delta) mais vous pouvez tout aussi bien jouer avec les coordonnées absolues.
Je joue aussi sur le Zinde pour faire passer les éléments les uns derrières les autres (J’avoue, on peut faire mieux sur ce point là)
On va aussi s’abonner à la méthode ManipulationCompleted pour gérer certains évènements:
- Changer d’index et déclencher l’animation pour compléter la manipulation
- Pas assez de mouvement pour déclencher une animation complète
- Gérer les effets de bords quand on est positionné au premier élément ou au dernier élément
Ici, le déroulement est assez simple : On récupère le delta entre la denière position enregistrée et la nouvelle position et on applique une nouvelle projection pour chaque élément:
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
La méthode UpdatePosition déclenche une animation complète de tous les éléments pour se repositionner sur le bon index.
Cette méthode est utilisée quand :
- On change d’index (via un Tap ou par code)
- On termine une manipulation, pour placer les éléments au bon endroit.
- On ajoute ou enlève des éléments (manière de replacer les autres éléments)
Voici le code simplifié (le code complet en ressource de l’article), où l’on voit la récupération de la projection correcte et la construction du storyboard pour chaque élément :
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 }
Voilà, j’espère que ce petit contrôle vous aura permis de voir comment créer un petit composant sympa pour vos application Windows 8.1 et (ou) Windows Phone 8.1
/Seb