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:

  1. Partie 1 : Création du contrôle Carousel pour Windows 8.0 (une traduction en français est disponible sur Développez.com)
  2. 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 Sourire

https://github.com/Mimetis/LightStone

image

 

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é:

  1. ArrangeOverride : Appellé lors du premier rendu et à la suite de chaque animation / manipulation
  2. 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.
  3. 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:

  1. Changer d’index et déclencher l’animation pour compléter la manipulation
  2. Pas assez de mouvement pour déclencher une animation complète
  3. 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 :

  1. On change d’index (via un Tap ou par code)
  2. On termine une manipulation, pour placer les éléments au bon endroit.
  3. 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