UWP : retour d'expérience de la mise en place d'un design responsive - trucs et astuces !
Avoir un design responsive lorsque l'on développe une application UWP est presque un pré-requis de nos jours. Dans cet article, je donnerai quelques astuces que j'utilise et que j'ai découvertes en portant mon application Mobile/tablette vers UWP.
L'outil de base : AdaptiveTrigger
Teddy en a déjà parlé dans un précédent article que je vous invite à re-lire : il s'agit vraiment de l'outil de base qui vous évitera de détecter les changements de taille de fenêtre à la main via du code. Globalement, c'est assez simple, on utilise les VisualStateManager.
Il ne faut cependant pas oublier de redéfinir les setters d'une définition de taille "plus petite": pas moyen d'utiliser de l'héritage ou d'utiliser plusieurs states à la fois (car il ne peut y avoir qu'un VisualState par "VisualStateGroup") :
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="WindowStates"> <VisualState x:Name="WideWideState"> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="580" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="WideScreenAirDate.Visibility" Value="Visible" /> <Setter Target="AirDateLabelC3.Visibility" Value="Visible" /> </VisualState.Setters> </VisualState> <VisualState x:Name="WideState"> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="520" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="WideScreenAirDate.Visibility" Value="Visible" /> <Setter Target="AirDateLabelC3.Visibility" Value="Visible" /> </VisualState.Setters> </VisualState> <VisualState x:Name="WideState"> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="400" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="WideScreenAirDate.Visibility" Value="Colapsed" /> <Setter Target="AirDateLabelC3.Visibility" Value="Colapsed" /> </VisualState.Setters> </VisualState> <VisualStateManager.VisualStateGroups>
Utiliser des VisualState et l'AdaptiveTrigger dans un ItemTemplate
Si vous essayez d'utiliser les VisualStates dans un ItemTemplate, vous serez déçus de voir que cela ne fonctionne pas.
L'astuce consiste à placer votre contenu XAML dans un UserControl et de définir les différents états visuels sur le premier Panel en son sein :
<UserControl> <Border> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="WindowStates"> <VisualState x:Name="WideWideState"> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="580" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="WideScreenAirDateLabel.Visibility" Value="Visible" /> </VisualState.Setters> </VisualState> <VisualStateManager.VisualStateGroups> </Border> </UserControl>
Cela permet par exemple d'afficher plus d'informations dans les éléments de vos ListView :
Attention aux Transitions utilisées
L'ajout d'animations à l'aide de la propriété "Transitions" des FrameworkElement est un jeu d'enfant. Attention cependant à celles que vous utilisez sur vos pages. Si comme moi vous utilisiez "ContentThemeTransition", vous aurez cet affichage "qui saute" lors des redimensionnements:
La solution consite simplement à utiliser "NavigationThemeTransition" :
Transitions = new TransitionCollection { new NavigationThemeTransition() };
Densité de pixels et zoom appliqué par l'OS
En fonction de la densité de pixels de votre écran, Windows applique automatiquement un "zoom" sur votre application. Ce n'était pas le cas sur les versions antérieures du Framework. Techniquement, cela fait "croire" à votre application qu'elle a plus ou moins de place.
Voici un exemple du même écran mais avec des scales différents. Vous remarquerez que les AdapativeTrigger se sont déclenchés car avec un niveau de zoom moindre, l'écran apparaît plus large à votre application.
Il est donc très important de tester votre application avec les valeurs par défaut de l'OS et même avec un zoom supérieur pour prendre en compte les personnes malvoyantes.
Pensez au placeholder pour les images
Il n'y a rien de plus déconcertant qu'une interface où les éléments se déplacent. Un cas fréquent de ce phénomène est lors du chargement d'une image : on ajoute une image dans l'UI en lui laissant la place qu'elle souhaite afin de respecter son ratio et il y a un saut au moment où l'image est décodée. Elle passe d'une hauteur de 0 (car cette information n'est pas encore connue - l'image n'est pas décodée) à sa taille réelle : l'interface "saute".
La solution que j'utilise passe par un Behavior qui applique un ratio au container de l'image : ce container respectera toujours le ratio de l'image final est il n'y aura pas de saut.
Au niveau du XAML c'est très simple :
<--- Ce container prendra toute la largeur possible --> <Border HorizontalAlignment="Stretch" Behaviors:ForceRatioToFrameworkElement.Ratio="1.777777777777778" > <Image ... /> </Borderx
Le code consiste à créer un DependencyObject et une AttachedProperty :
public class ForceRatioToFrameworkElement : DependencyObject { #region Ratio /// <summary> /// Ratio Attached Dependency Property /// </summary> public static readonly DependencyProperty RatioProperty = DependencyProperty.RegisterAttached("Ratio", typeof(double), typeof(ForceRatioToFrameworkElement), new PropertyMetadata((double)-1, OnRatioChanged)); /// <summary> /// Gets the Ratio property. This dependency property /// indicates .... /// </summary> public static double GetRatio(DependencyObject d) { return (double)d.GetValue(RatioProperty); } /// <summary> /// Sets the Ratio property. This dependency property /// indicates .... /// </summary> public static void SetRatio(DependencyObject d, double value) { d.SetValue(RatioProperty, value); } /// <summary> /// Handles changes to the Ratio property. /// </summary> private static void OnRatioChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var target = d as FrameworkElement; if (target == null) { return; } ForceRationOnFrameworkElement(target); target.SizeChanged -= Target_SizeChanged; target.SizeChanged += Target_SizeChanged; } private static void Target_SizeChanged(object sender, SizeChangedEventArgs e) { var target = sender as FrameworkElement; if (target == null) { return; } ForceRationOnFrameworkElement(target); } private static void ForceRationOnFrameworkElement(FrameworkElement target) { var ratio = GetRatio(target); var targetActualWidth = target.ActualWidth; var targetActualHeight = target.ActualWidth; if (!double.IsNaN(targetActualWidth) && targetActualWidth > 0) { target.Height = targetActualWidth / ratio; } else if (!double.IsNaN(targetActualHeight) && targetActualHeight > 0) { target.Width = targetActualHeight * ratio; } } #endregion }
Et voici le résultat :
N'ayez pas peur de passer par le code...
Il s'agit d'une remarque un peu générique mais il faut le dire quand même : n'ayez pas peur du code et encore moins du code-behind. Lorsque l'on utilise massivement MVVM, nous avons tendance à se dire "le code-behind", c'est le mal... mais il ne faut pas tomber dans le travers de ne jamais en faire. Parfois il s'agit en effet de la seule solution possible... :)
Happy coding !
Commentaires