Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

[WP7] Créer une vue panoramique

English version here

Vous avez sans doute déjà vu des vidéos de présentation de Windows Phone 7 montrant des effets panoramiques, où chaque écran apparaît comme un morceau de l’image globale. Le tout avec un aperçu du reste de l’image sur le bord de l’écran comme teaser pour inciter l’utilisateur à explorer le reste.

 

img_203332_win_mob_7_1

 

Cet effet est fort sympathique, mais un léger problème se pose toutefois pour nous autres développeurs : la version du SDK de WP7 publiée actuellement ne contient pas de contrôles pré-fabriqués pour créer des interfaces de ce style. Cet “oubli” sera vraisemblablement corrigé d’ici la version finale, mais ceci constitue un excellent exercice pour se familiariser avec Silverlight.

 

Organisation de la fenêtre

 

Etape numéro 1 : organiser la page principale. La stratégie que nous allons adopter ici est la suivante :

 

Layout

 

Pour des raisons évidentes de performance (c’est un téléphone, ne l’oublions pas), nous n’allons pas charger l’intégralité du panorama, bien que cela aurait été plus simple. Nous allons ici essayer de nous débrouiller avec une grille prenant approximativement trois fois la largeur de l’écran, et composée de trois colonnes. Chaque colonne servira à afficher une page de l’application, et doit être légèrement moins grande que l’écran pour que l’on puisse voir un bout de la suivante sur le bord. La largeur de l’écran standard d’un téléphone WP7 étant de 480 pixels, nous allons ici prendre 400 pixels.

Le code de notre grille ressemble pour le moment à :

   1: <Grid x:Name="PanoramicGrid" Grid.Row="1" Grid.Column="0" >
   2:             <Grid.RenderTransform>
   3:                 <TranslateTransform x:Name="PanoramaContentTranslate" X="-400" Y="0" />
   4:             </Grid.RenderTransform>
   5:             <Grid.RowDefinitions>
   6:                 <RowDefinition />
   7:             </Grid.RowDefinitions>
   8:  
   9:             <Grid.ColumnDefinitions>
  10:                 <ColumnDefinition Width="400" />
  11:                 <ColumnDefinition Width="400" />
  12:                 <ColumnDefinition Width="400" />
  13:             </Grid.ColumnDefinitions>
  14:  
  15:         </Grid>

Nous utiliserons ici un TranslateTransform pour centrer la grille plutôt que des marges. Ce sera en effet plus simple pour faire “glisser” l’interface par la suite.

On ajoute également à la solution une liste de UserControl, de 400 pixels de largeur, chacun correspondant à un morceau du panorama. Pour cet article j’en ai ajouté trois, appelés WindowsPhoneControl1, WindowsPhoneControl2, et WindowsPhoneControl3 (original non ?).

 

Chargement des contrôles

Maintenant que la grille est créée, il faut encore y placer le contenu. Cette fois c’est au niveau du code-behind que ça va se passer. On commence d’abord par rajouter quelques propriétés et instancier les contrôles dans le constructeur :

   1: /// <summary>
   2: /// Size of the pages
   3: /// </summary>
   4: public const int PageWidth = 400;
   5:  
   6: public MainPage()
   7: {
   8:     this.PageList = new List<UserControl>() 
   9:         { 
  10:             new WindowsPhoneControl1() { IsEnabled = false }, 
  11:             new WindowsPhoneControl2() { IsEnabled = false }, 
  12:             new WindowsPhoneControl3() { IsEnabled = false } 
  13:         };
  14:  
  15:     this.CurrentPageIndex = 0;
  16:  
  17:     InitializeComponent();
  18:  
  19:     SupportedOrientations = SupportedPageOrientation.Portrait;
  20: }
  21:  
  22: /// <summary>
  23: /// Ordered list of the panorama pages
  24: /// </summary>
  25: protected List<UserControl> PageList { get; set; }
  26:  
  27: /// <summary>
  28: /// Index of the page currently displayed
  29: /// </summary>
  30: protected int CurrentPageIndex { get; set; }

PageWidth est une constante contenant la taille des colonnes de la grille. Cela permettra de le modifier en cas de besoin en ayant un impact minimal sur le code. 
PageList contient la liste des usercontrol composant le panorama, dans l’ordre d’affichage de gauche à droite. Par mesure de précaution, on pensera à mettre la propriété “IsEnabled” de chacun des contrôles à False.
Ne pas oublier de ne laisser que “Portrait” dans la liste des orientations supportées, cet exemple ne gérant pas l’affichage du téléphone en mode paysage.

On rajoute maintenant quelques initialisations au chargement de l’application :

   1: private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
   2: {
   3:     var frame = (PhoneApplicationFrame)Application.Current.RootVisual;
   4:     frame.Width = PageWidth * 3;
   5:  
   6:     this.LoadPages();
   7: }

 

L’idée ici est d’agrandir la taille du conteneur PhoneApplicationFrame, sans quoi les zones en dehors de l’écran ne seront pas dessinées (ce qui gênera lors des effets de transition).

Enfin la méthode LoadPage, point central de l’application, s’occupe de placer les usercontrol devant être visibles dans la grille :

   1: private void LoadPages()
   2: {
   3:     this.PanoramicGrid.Children.Clear();
   4:  
   5:     var currentPage = this.PageList[this.CurrentPageIndex];
   6:     currentPage.IsEnabled = true;
   7:  
   8:     this.PanoramicGrid.Children.Add(currentPage);
   9:     Grid.SetColumn(currentPage, 1);
  10:     Grid.SetRow(currentPage, 1);
  11:  
  12:     if (this.PageList.Count > this.CurrentPageIndex + 1)
  13:     {
  14:         var nextPage = this.PageList[this.CurrentPageIndex + 1];
  15:         nextPage.IsEnabled = false;
  16:  
  17:         this.PanoramicGrid.Children.Add(nextPage);
  18:  
  19:         Grid.SetColumn(nextPage, 2);
  20:         Grid.SetRow(nextPage, 1);
  21:     }
  22:  
  23:     if (this.CurrentPageIndex > 0)
  24:     {
  25:         var previousPage = this.PageList[this.CurrentPageIndex - 1];
  26:         previousPage.IsEnabled = false;
  27:  
  28:         this.PanoramicGrid.Children.Add(previousPage);
  29:  
  30:         Grid.SetColumn(previousPage, 0);
  31:         Grid.SetRow(previousPage, 1);
  32:     }
  33: }

La méthode retire tous les contrôles de la grille, et re-détermine ceux qui doivent être affichés. La colonne centrale contiendra toujours le usercontrol correspondant à la page active. Les colonnes de gauche et de droite contiennent respectivement la précédente page et la suivante, si elles existent.

Si on lance l’application à ce point, la première page s’affiche, et on aperçoit un morceau de la second page à droite de l’écran. Il n’y a cependant pas la moindre interaction.

sample1

 

Changements de page et transitions

Définissons maintenant le comportement que doit adopter l’application : si l’utilisateur fait glisser son doigt de droite vers gauche, l’interface doit glisser en suivant le mouvement et en révélant la page suivante. Quand l’utilisateur retire son doigt de l’écran, si le mouvement de glisse a été trop faible on remet l’interface à sa position initiale, si le mouvement a été suffisamment ample on termine pour l’utilisateur la transition vers la page suivante.
Evidemment, l’application aura le même comportement si l’utilisateur fait glisser son doigt de gauche vers droite, mais avec la page précédente.

Retour dans le XAML, pour ajouter l’animation de transition, et attacher à la grille les évènements de manipulation qui serviront à détecter les mouvements de doigts de l’utilisateur :

   1: <UserControl.Resources>
   5:         <Storyboard x:Name="PageChangeAnimation">
   6:             <DoubleAnimation To="-400.0" SpeedRatio="4" Storyboard.TargetName="PanoramaContentTranslate" Storyboard.TargetProperty="X" />
   8:         </Storyboard>
   9:     </UserControl.Resources>
  10:         </StackPanel.RenderTransform>
  11:     </StackPanel>
  12:     
  13:     <Grid x:Name="PanoramicGrid" Grid.Row="1" Grid.Column="0"
  14:                     ManipulationDelta="PhoneApplicationPage_ManipulationDelta"
  15:                     ManipulationCompleted="PhoneApplicationPage_ManipulationCompleted" >
  16:         <Grid.RenderTransform>
  17:             <TranslateTransform x:Name="PanoramaContentTranslate" X="-400" Y="0" />
  18:         </Grid.RenderTransform>
  19:         <Grid.RowDefinitions>
  20:             <RowDefinition />
  21:         </Grid.RowDefinitions>
  22:  
  23:         <Grid.ColumnDefinitions>
  24:             <ColumnDefinition Width="400" />
  25:             <ColumnDefinition Width="400" />
  26:             <ColumnDefinition Width="400" />
  27:         </Grid.ColumnDefinitions>
  28:  
  29:     </Grid>

 

L’évènement ManipulationDelta est déclenché tout au long du mouvement de doigt de l’utilisateur, et va nous permettre de faire “coller” l’interface au doigt. L’évènement ManipulationCompleted est déclenché lorsque l’utilisateur retire son doigt de la surface de l’écran.

 

Remarque :

J’ai découvert un comportement assez génant en voulant utiliser les évènements de manipulation : ceux-ci sont déclanchés même si la manipulation s’est faite au niveau d’un contrôle enfant. Ainsi, si l’utilisateur passe son doit sur un Slider, l’évènement sera quand même déclenché au niveau de la grille, déclenchant à son tour le code pour faire glisser l’interface. L’utilisateur se retrouve donc dans l’incapacité de manipuler le Slider, vu que l’interface bouge en même temps que ces gestes !
La seule solution de contournement que j’ai trouvé pour le moment est de vérifier la source de l’évènement, et de le traiter que s’il provient d’un conteneur (contrôle héritant du type Panel).

 

Commençons par nous occuper de ManipulationDelta :

   1: private void PhoneApplicationPage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
   2:         {
   3:             if (e.OriginalSource is Panel)
   4:             {
   5:                 this.PanoramaContentTranslate.X = e.CumulativeManipulation.Translation.X - PageWidth;
   8:             }
   9:         }

e.CumulativeManipulation.Translation contient l’ampleur totale du mouvement de l’utilisateur. L’idée est donc tout simplement de décaler la grille de cette valeur, à l’aide du TranslateTransform que nous avons pris soin de mettre précédemment. Ne pas oublier de soustraire PageWidth qui correspond à la valeur initiale du TranslateTransform, que nous avions défini pour centrer la grille.

Maintenant l’évènement ManipulationCompleted :

   1: private void PhoneApplicationPage_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
   2:         {
   3:             if (e.OriginalSource is Panel)
   4:             {
   5:                 if (e.TotalManipulation.Translation.X < 0)
   6:                 {
   7:                     if (e.TotalManipulation.Translation.X > -180 || this.CurrentPageIndex >= this.PageList.Count - 1)
   8:                     {
   9:                         this.PageChangeAnimation.Begin();
  10:                     }
  11:                     else
  12:                     {
  13:                         this.ChangePage(1);
  14:                     }
  15:                 }
  16:                 else if (e.TotalManipulation.Translation.X > 0)
  17:                 {
  18:                     if (e.TotalManipulation.Translation.X < 180 || this.CurrentPageIndex <= 0)
  19:                     {
  20:                         this.PageChangeAnimation.Begin();
  21:                     }
  22:                     else
  23:                     {
  24:                         this.ChangePage(-1);
  25:                     }
  26:                 }
  27:             }
  28:         }

On commence par déterminer le sens du mouvement (droite ou gauche) à l’aide de e.TotalManipulation.Translation.X. A partir de là, on regarde si l’ampleur du mouvement est supérieure à 180 pixels (valeur fixée arbitrairement) : si oui, on appelle la fonction ChangePage qui va se charger de changer la page active, sinon on considère qu’il s’agit d’une erreur de manipulation et on déclenche l’animation PageChangeAnimation pour remettre l’interface en place. Ne pas oublier, avant de changer de page, de vérifier qu’il y a bien une page suivante ou précédente.

Intéressons-nous maintenant à la fonction ChangePage :

   1: private void ChangePage(int step)
   2: {
   3:     this.CurrentPageIndex += step;
   4:  
   5:     this.LoadPages();
   6:  
   7:     this.PanoramaContentTranslate.X += PageWidth * step;
   8:  
   9:     this.PageChangeAnimation.Begin();
  10: }

Cette fonction est un peu plus subtile. La grille ne faisant que trois colonnes, il faut que la page active soit systématiquement dans la colonne centrale (pour permettre d’effectuer la transition vers la page suivante ou la page précédente). Il faut donc à un moment ou un autre prendre la page suivante et la mettre dans la colonne centrale, pour qu’elle devienne à son tour la page active.
Pour cela, nous commençons par changer le numéro de la page active et appeler la méthode LoadPages. Celle-ci, écrite précédemment, va mettre la nouvelle page active dans la colonne centrale, et les pages appropriées dans les autres colonnes. Dans le cas d’une transition vers la page suivante par exemple, l’ancienne page active se retrouve dans la colonne de gauche, la nouvelle page active dans la colonne centrale, et la nouvelle page suivante dans la colonne de droite. Problème : vu que la grille est décalée, suite au mouvement de l’utilisateur, il va se retrouver en train de glisser vers la nouvelle page suivante ! Pour empêcher cela, nous décalons la grille de la taille d’une colonne dans la direction appropriée, pour que l’utilisateur ne se rende pas compte de ce qu’il vient de se passer. Il n’y a plus qu’à déclencher l’animation PageChangeAnimation pour terminer le mouvement.

A ce point là, l’application est utilisable. Il manque cependant un dernier détail qu’on peut apercevoir dans les vidéos de présentation de WP7.

Ajout du titre

En effet, dans les présentations, on peut apercevoir un titre qui s’étend sur plusieurs pages, et qui défile à une vitesse inférieure de celle des pages. Nous allons ajouter cela à notre application.
Tout d’abord, petit changement de présentation. Nous gardons notre grille avec ses trois colonnes, mais nous la plaçons dans une grille plus grande, laquelle contient également une ligne s’étendant sur toute la largeur pour y placer le titre :

Layout2

Ce qui donne, au niveau du XAML :

   1: <UserControl.Resources>
   2:     <Storyboard x:Name="PageChangeAnimation">
   3:         <DoubleAnimation To="-400.0" SpeedRatio="4" Storyboard.TargetName="PanoramaContentTranslate" Storyboard.TargetProperty="X" />
   4:         <DoubleAnimation x:Name="SlideTitleDoubleAnimation" SpeedRatio="4" Storyboard.TargetName="TitleTranslate" Storyboard.TargetProperty="X" />
   5:     </Storyboard>
   6: </UserControl.Resources>
   7:  
   8: <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneBackgroundBrush}">
   9:     <Grid.RowDefinitions>
  10:         <RowDefinition Height="140" />
  11:         <RowDefinition Height="*"/>
  12:     </Grid.RowDefinitions>         
  13:     
  14:     <StackPanel Grid.Row="0" Grid.Column="0" x:Name="TitlePanel">
  15:         <StackPanel.RenderTransform>
  16:             <TranslateTransform x:Name="TitleTranslate" />
  17:         </StackPanel.RenderTransform>
  18:     </StackPanel>
  19:     
  20:     <Grid x:Name="PanoramicGrid" Grid.Row="1" Grid.Column="0"
  21:                     ManipulationDelta="PhoneApplicationPage_ManipulationDelta"
  22:       ManipulationCompleted="PhoneApplicationPage_ManipulationCompleted" >
  23:         <Grid.RenderTransform>
  24:             <TranslateTransform x:Name="PanoramaContentTranslate" X="-400" Y="0" />
  25:         </Grid.RenderTransform>
  26:         <Grid.RowDefinitions>
  27:             <RowDefinition />
  28:         </Grid.RowDefinitions>
  29:  
  30:         <Grid.ColumnDefinitions>
  31:             <ColumnDefinition Width="400" />
  32:             <ColumnDefinition Width="400" />
  33:             <ColumnDefinition Width="400" />
  34:         </Grid.ColumnDefinitions>
  35:  
  36:     </Grid>
  37: </Grid>

Un panel est placé dans la ligne de la grille prévue à cet effet, pour recevoir le titre. Nous plaçons également un TranslateTransform  et nous ajoutons un DoubleAnimation au Storyboard de changement de page pour pouvoir le déplacer le titre.

Nous ajoutons également à la solution un UserControl nommé “PanoramicTitle”, de la largeur souhaitée, qui contiendra le titre proprement dit.

Nous initialisons le tout au chargement de l’application :

   1: private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
   2: {
   3:     var frame = (PhoneApplicationFrame)Application.Current.RootVisual;
   4:     frame.Width = PageWidth * 3;
   5:  
   6:     var title = new PanoramicTitle();
   7:  
   8:     this.TitlePanel.Children.Add(title);
   9:  
  10:     this.LoadPages();
  11: }

Pour créer l’effet de scolling différentiel, nous allons faire glisser le titre à une vitesse deux fois moindre que les pages de l’application. A partir de là il n’y a plus qu’à compléter le code pour gérer ce glissement :

   1: private void PhoneApplicationPage_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
   2: {
   3:     if (e.OriginalSource is Panel)
   4:     {
   5:         if (e.TotalManipulation.Translation.X < 0)
   6:         {
   7:             if (e.TotalManipulation.Translation.X > -180 || this.CurrentPageIndex >= this.PageList.Count - 1)
   8:             {
   9:                 this.SlideTitleDoubleAnimation.To = this.CurrentPageIndex * PageWidth / 2 * -1;
  10:                 this.PageChangeAnimation.Begin();
  11:             }
  12:             else
  13:             {
  14:                 this.ChangePage(1);
  15:             }
  16:         }
  17:         else if (e.TotalManipulation.Translation.X > 0)
  18:         {
  19:             if (e.TotalManipulation.Translation.X < 180 || this.CurrentPageIndex <= 0)
  20:             {
  21:                 this.SlideTitleDoubleAnimation.To = this.CurrentPageIndex * PageWidth / 2 * -1;
  22:                 this.PageChangeAnimation.Begin();
  23:             }
  24:             else
  25:             {
  26:                 this.ChangePage(-1);
  27:             }
  28:         }
  29:     }
  30: }
  31:  
  32: private void ChangePage(int step)
  33: {
  34:     this.CurrentPageIndex += step;
  35:  
  36:     this.LoadPages();
  37:  
  38:     this.PanoramaContentTranslate.X += PageWidth * step;
  39:  
  40:     this.SlideTitleDoubleAnimation.To = this.CurrentPageIndex * PageWidth / 2 * -1;
  41:  
  42:     this.PageChangeAnimation.Begin();
  43: }
  44:  
  45: private void PhoneApplicationPage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
  46: {
  47:     if (e.OriginalSource is Panel)
  48:     {
  49:         this.PanoramaContentTranslate.X = e.CumulativeManipulation.Translation.X - PageWidth;
  50:  
  51:         this.TitleTranslate.X = e.CumulativeManipulation.Translation.X /2 - (this.CurrentPageIndex * PageWidth / 2);
  52:     }
  53: }

Compilez, exécutez, et nous voilà avec le résultat final :

 

Le code complet est téléchargeable en suivant ce lien.

Publié lundi 22 mars 2010 22:59 par KooKiz
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

# re: [WP7] Créer une vue panoramique

Pas mal du tout ton article :) surtout pour le scrolling !

C'est plus sympa de lire ton article que la doc de windows mobile 7 pour développeur ;)

______

Nk54

mardi 6 avril 2010 17:09 by Kikuts
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- L’application des MiniDrones Parrot est aussi disponible pour Windows 8.1 par Blog de Jérémy Jeanson le 10-28-2014, 15:01

- L’application des MiniDrones Parrot est enfin disponible pour Windows Phone par Blog de Jérémy Jeanson le 10-27-2014, 09:49

- Mise à jour Samsung 840 EVO sur core server par Blog de Jérémy Jeanson le 10-27-2014, 05:59

- MVP Award 2014 ;) par Blog de Jérémy Jeanson le 10-27-2014, 05:42

- « Naviguer vers le haut » dans une librairie SharePoint par Blog de Jérémy Jeanson le 10-07-2014, 13:21

- PowerShell: Comment mixer NAGIOS et PowerShell pour le monitoring applicatif par Blog Technique de Romelard Fabrice le 10-07-2014, 11:43

- ReBUILD 2014 : les présentations par Le blog de Patrick [MVP Office 365] le 10-06-2014, 09:15

- II6 Management Compatibility présente dans Windows Server Technical Preview avec IIS8 par Blog de Jérémy Jeanson le 10-05-2014, 17:37

- Soft Restart sur Windows Server Technical Preview par Blog de Jérémy Jeanson le 10-03-2014, 19:43

- Non, le certificat public du CA n’est pas un certificat client !!! par Blog de Jérémy Jeanson le 10-03-2014, 00:08