Écrire du code commun pour les applications multiplateformes ne signifie pas refuser toute adaptation au support. Par exemple, les techniques de conception adaptative, qui peuvent être mises en œuvre dans une application MAUI, sont nombreuses et variées.
L’adaptive design
Parfois confondu avec le Reactive Design, l'adaptive design vise à adapter l'interface utilisateur (UI) au facteur de forme utilisé, que ce soit un petit smartphone, un écran de télévision ou un PC.
Le Reactive Design, en revanche, revêt une connotation plus technique, se concentrant sur le codage de manière à ce que l'application demeure réactive, ou "responsive", en toutes circonstances. De fait, on entend également parler de Responsive Design.
Hélas, comme c'est souvent le cas avec ces expressions et termes récents, il n'y a pas de consensus sur leur signification exacte. Ainsi, on utilise parfois les termes adaptive design, reactive design ou responsive design de manière interchangeable pour désigner la capacité d'une UI à s'adapter au facteur de forme. Bien que cette utilisation ne soit pas correcte, elle est néanmoins courante.
L’Adaptive Design avec MAUI
Ce billet ne sera pas théorique, mais résolument pratique ! Ainsi, après cette introduction qui pose le décor, nous allons plonger directement dans des cas concrets de mise en œuvre de l’adaptive design avec MAUI.
S’adapter à la taille des écrans
S’il existe un point central dans le domaine concret de l’Adaptive Design, c’est bien la connaissance de la taille de l’écran. C'est même la première chose à prendre en considération. On n'affiche pas les mêmes informations sur un écran de 320x480 pixels (comme celui de l'iPhone 3GS) que sur un écran de 5120x2880 pixels (comme celui de l’iPad 27” retina 5k).
Étant donné qu'il n'existe pas de standard unique (ah, les bons vieux jours du CGA, EGA ou VGA ! - des références que les plus jeunes auront du mal à saisir...), on trouve une grande variété de résolutions intermédiaires. À cela s’ajoute la densité en pixels, qui modifie également la façon d’interpréter la taille de l’écran en pixels.
Il se trouve que MAUI, tout comme le faisait les Xamarin.forms, fonctionne de manière similaire au pixel effectif de UWP. Par défaut, MAUI utilise des unités indépendantes des dispositifs (Device Independent Units - DIU). Cela simplifie notre tâche car nous n’avons pas à prendre en compte la densité de pixels, les valeurs retournées par MAUI étant déjà adaptées (vous noterez que ces aspects de tailles, de pixels et de fontes sont abordés dans mon livre sur MAUI, disponible en format Kindle sur Amazon).
A savoir sur les DIU .NET MAUI utilise les Device Independent Units (DIU) pour les mesures d’écran. Un DIU (également appelé Device Independent Pixel ou DIP) est basé sur les pouces plutôt que sur des pixels spécifiques au matériel. Plus précisément, un DIU est défini comme 1/96 de pouce (plus petit que le point, qui est défini comme 1/72 de pouce). Pour un moniteur standard de 96 pixels par pouce, 96 DIU équivalent à 96 pixels. Lorsque la mesure ne correspond pas exactement à un nombre rond (ce qui est souvent le cas), .NET MAUI utilise automatiquement l’anti-aliasing ou vous avez la possibilité de “sauter” vers le pixel le plus proche si vous ne souhaitez pas d’éléments “flous” pour vos boutons, etc
Pour s’adapter à la taille de l’écran, il faut donc obtenir la mesure en pixels, donnée en DIUs par MAUI.
Le moyen le plus simple, au sein d’une Page, est d’enregistrer un gestionnaire pour l’événement SizeChanged :
C’est un procédé simple et efficace était déjà utilisable sous les Xamarin.Forms et permet d’adapter la taille d’une image, comme illustré dans l’exemple ci-dessus. Toutefois, il est important de noter qu’une interrogation au niveau de chaque page peut sembler peu économe en ressources, car la taille de l’écran ne risque pas de changer en cours d’exécution. Il peut donc être plus efficace et logique d’obtenir cette information une fois pour toutes dans App.xaml.cs, par exemple.
De plus sous MAUI il existe d'autres façon de recevoir les informations détaillées de la device.
Cependant, il reste la question de la gestion de l’orientation de l’appareil, qui modifie la taille de l’écran retournée par l’événement indiqué. Pour garantir une adaptation à la fois à la taille de l’appareil et aux rotations qui modifient artificiellement cette taille (la hauteur et la largeur étant simplement inversées), l’adaptation au niveau de la Page peut conserver son intérêt.
Ainsi, en gérant l'événement SizeChanged au niveau de chaque Page, on assure une réactivité optimale face aux changements d'orientation, garantissant une expérience utilisateur fluide et adaptée à chaque contexte d'utilisation.
Répondre aux changements d’orientation
Lorsque la device est tournée du mode paysage au mode portrait, ou inversement il est très rare qu’un écran conçu pour le mode portrait (le plus fréquent sur un smartphone) passe sagement en mode paysage sans que cela ne pose quelques soucis de mise en page !
On peut s’en tenir à l’idée précédente qui consiste à “écouter” les changements de taille de l’écran. Mais on peut aussi détecter directement le changement d’orientation de la device ce qui est parfois plus pertinent. L’un n’interdisant pas l’autre (connaître l’orientation et la taille écran en pixels DIU est plutôt complémentaire).
Les petits futés auront remarqué que sur une device donnée on pouvait d’ailleurs utiliser le même évènement ! Il suffit de regarder si la largeur est plus grande que la hauteur et on sait de façon sûre dans quelle orientation se trouve la device…
Dans l’exemple ci-dessus on modifie une variable booléenne quand la taille écran est changée. Selon les proportions hauteur / largeur on peut ainsi créer un flag isPortrait exploitable par le reste de la page.
Le code présenté jusqu’ici doit se comprendre comme du code d’UI, donc il se trouve dans le code-behind de la page concernée et non dans le ViewModel.
Une fois l’orientation de l’appareil connue, il est possible, depuis le code-behind, de modifier la taille et la disposition des éléments à afficher, voire de masquer ou de montrer certains d'entre eux. Par exemple, dans l’illustration ci-dessous, une partie de l’affichage qui se trouvait au-dessus en mode portrait est déplacée sur le côté gauche en mode paysage. Ce changement transforme radicalement l’apparence de l’application, mais de manière si naturelle et intuitive que l’utilisateur ne le remarquera même pas (alors que sans cette adaptation, il serait certainement mécontent et à juste titre !) :
La chose pourrait s'écrire de cette façon :
void OnSizeChanged(object sender, EventArgs e)
{
var page = sender as ContentPage;
var width = page.Width;
var height = page.Height;
if (width > height)
{
// Mode paysage
// Adapter la taille et la disposition des éléments pour le mode paysage
image.HorizontalOptions = LayoutOptions.Start;
stackLayout.Orientation = StackOrientation.Horizontal;
}
else
{
// Mode portrait
// Adapter la taille et la disposition des éléments pour le mode portrait
image.HorizontalOptions = LayoutOptions.Center;
stackLayout.Orientation = StackOrientation.Vertical;
}
}
Dans cet exemple, la disposition des éléments change en fonction de l'orientation de l'appareil, garantissant une expérience utilisateur cohérente et agréable, quelle que soit la manière dont il tient son appareil. Grâce à ces ajustements, l’application reste utilisable et esthétique dans toutes les configurations possibles.
MAUI va plus loin
Si la façon de répondre au changement d'orientation pour adapter l'UI reste fondamentalement la même, la façon de détecter le changement d'orientation ou de connaître les informations précises de la device sont un peu plus sophistiquées sous MAUI. La façon classique exposée plus haut reste valable et très utilisée, mais il est possible d'utiliser les services de IDeviceDisplay (voir la documentation Microsoft).
Par exemple, il est possible de prendre connaissance de l'orientation en cours et d'autres informations pertinentes sur l'affichage :
private void ReadDeviceDisplay()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.AppendLine($"Pixel width: {DeviceDisplay.Current.MainDisplayInfo.Width} / Pixel Height: {DeviceDisplay.Current.MainDisplayInfo.Height}");
sb.AppendLine($"Density: {DeviceDisplay.Current.MainDisplayInfo.Density}");
sb.AppendLine($"Orientation: {DeviceDisplay.Current.MainDisplayInfo.Orientation}");
sb.AppendLine($"Rotation: {DeviceDisplay.Current.MainDisplayInfo.Rotation}");
sb.AppendLine($"Refresh Rate: {DeviceDisplay.Current.MainDisplayInfo.RefreshRate}");
DisplayDetailsLabel.Text = sb.ToString();
}
L'interface donne même accès à un évènement (MainDisplayInfoChanged) qui permet de répondre immédiatement aux changements d'orientation.
Prendre en compte le type de device
Dans la panoplie de base de l’Adaptive Design. il y a la taille de l’écran, son orientation mais aussi le type de la device !
On n'affiche pas les mêmes contenus sur l'écran d'un smartphone et sur celui d'une tablette ou d'un Mac, indépendamment de l'orientation ou de la taille en pixels, en raison de la taille physique de l'appareil et de la distance à laquelle il se trouve des yeux de l'utilisateur. Un smartphone est tenu très près, une tablette sera posée sur une table comme un écran d'ordinateur portable, un écran de PC se trouvera à environ 30-50 cm, tandis qu'un écran de télévision sera beaucoup plus éloigné.
MAUI nous offre un moyen simple de déterminer le type de l'appareil grâce à la classe Device et aux informations qu'elle retourne. Voici un exemple illustrant comment utiliser cette fonctionnalité :
using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
AdjustLayoutForDevice();
}
private void AdjustLayoutForDevice()
{
switch (DeviceInfo.Idiom)
{
case DeviceIdiom.Phone:
// Configuration pour smartphone
ConfigureForPhone();
break;
case DeviceIdiom.Tablet:
// Configuration pour tablette
ConfigureForTablet();
break;
case DeviceIdiom.Desktop:
// Configuration pour PC
ConfigureForDesktop();
break;
case DeviceIdiom.TV:
// Configuration pour TV
ConfigureForTV();
break;
default:
// Configuration par défaut
ConfigureForDefault();
break;
}
}
private void ConfigureForPhone()
{
// Adapter l'UI pour un smartphone
label.FontSize = 14;
image.WidthRequest = 100;
image.HeightRequest = 100;
}
private void ConfigureForTablet()
{
// Adapter l'UI pour une tablette
label.FontSize = 18;
image.WidthRequest = 200;
image.HeightRequest = 200;
}
private void ConfigureForDesktop()
{
// Adapter l'UI pour un PC
label.FontSize = 20;
image.WidthRequest = 300;
image.HeightRequest = 300;
}
private void ConfigureForTV()
{
// Adapter l'UI pour une TV
label.FontSize = 24;
image.WidthRequest = 400;
image.HeightRequest = 400;
}
private void ConfigureForDefault()
{
// Configuration par défaut
label.FontSize = 16;
image.WidthRequest = 150;
image.HeightRequest = 150;
}
}
A noter : Le Device.Idiom utilisé sous les Xamarin.Forms est deprecated, il faut désormais utiliser DeviceInfo.Idiom comme le montre code ci-dessus.
On peut utiliser l’Idiom pour changer la taille des objets, de la fonte, pour modifier la mise en page, voire même pour appeler une page différente au moment de la navigation ce qui s'avère parfois indispensable lorsque les différences de mise en page sont trop importantes. Rappelez-vous qu'une page XAML bourrée de code sera plus longue à chargée qu'une page adaptée à un type d'affichage donnée. Vouloir tout faire en un seul code peut parfois allez à l'encontre d'un autre but : la maintenabilité. Et cette dernière a toujours raison quand il s'agit de trancher !
Il est même judicieux parfois de prévoir la substitution d’éléments d’UI par d’autres. Ainsi par exemple une application de dessin proposant un Color Picker pourra gérer différents types de contrôles selon que la device est de type purement tactile (smartphone, phablet, tablette…) ou qu’il s’agit d’une machine disposant d’un outil de pointage plus précis (PC ou Mac avec souris). Le fait de créer des pages totalement adaptées quand ce type de différence devient flagrant allège le code XAML à charger et permet de maintenir chaque page plus facilement. Mais on peut tout faire en une seule page. Le bon sens vous dictera le meilleur choix selon les circonstrances j'en suis convaincu.
Utilisation du ContentView en Adaptive Design
Pour s’adapter correctement à l’appareil, à sa taille, à son orientation, etc., il est souvent plus efficace, plutôt que de faire des contorsions en XAML, de concevoir son interface utilisateur par blocs interchangeables. Dans ce cas, le contrôle le plus pratique est le ContentView
.
En reprenant l’exemple du sélecteur de couleurs (Color Picker), on peut ainsi concevoir une version simplifiée pour smartphone et une version plus complète pour les écrans plus grands. Charger l’une ou l’autre de ces versions selon le contexte est très simple, comme nous l’avons vu dans les sections précédentes.
Voici un exemple illustrant comment utiliser le ContentView
pour créer des interfaces adaptatives :
Définir les ContentViews :
- Créer une version simplifiée pour les smartphones (
ColorPickerPhone.xaml
).
- Créer une version complète pour les écrans plus grands (
ColorPickerDesktop.xaml
).
<!-- ColorPickerPhone.xaml -->
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="YourNamespace.ColorPickerPhone">
<StackLayout>
<Label Text="Choose a Color" FontSize="Medium" />
<Picker x:Name="colorPicker">
<!-- Ajouter des options de couleur -->
</Picker>
</StackLayout>
</ContentView>
<!-- ColorPickerDesktop.xaml -->
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="YourNamespace.ColorPickerDesktop">
<StackLayout>
<Label Text="Choose a Color" FontSize="Large" />
<Picker x:Name="colorPicker">
<!-- Ajouter des options de couleur -->
</Picker>
<!-- Ajouter des fonctionnalités supplémentaires pour les grands écrans -->
<Button Text="Apply Color" />
<Label x:Name="selectedColorLabel" />
</StackLayout>
</ContentView>
Charger le ContentView approprié :
using Microsoft.Maui.Controls;
using Microsoft.Maui.Devices;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
LoadAppropriateColorPicker();
}
private void LoadAppropriateColorPicker()
{
ContentView colorPickerView;
switch (DeviceInfo.Idiom)
{
case DeviceIdiom.Phone:
colorPickerView = new ColorPickerPhone();
break;
case DeviceIdiom.Tablet:
case DeviceIdiom.Desktop:
case DeviceIdiom.TV:
colorPickerView = new ColorPickerDesktop();
break;
default:
colorPickerView = new ColorPickerPhone(); // Configuration par défaut
break;
}
this.Content = colorPickerView;
}
}
Dans cet exemple, deux versions de l'interface utilisateur du sélecteur de couleurs sont créées : une version simplifiée pour les smartphones et une version plus complète pour les écrans plus grands. La méthode LoadAppropriateColorPicker
charge la version appropriée en fonction du type d’appareil.
En utilisant des ContentView
interchangeables, il devient simple d'adapter l’interface utilisateur aux différentes tailles d’écran et orientations, garantissant ainsi une expérience utilisateur optimale sur tous les appareils.
Les pages personnalisées
En dernier recours, et comme je le proposais déjà plus haut, il est tout à fait légitime de prévoir des pages totalement différentes selon les contextes. Il est préférable d'avoir une page dédiée pour le mode portrait et une autre pour le mode paysage plutôt qu'une seule page XAML complexe, accompagnée d'un code-behind alambiqué et difficile à maintenir. Pensez toujours à la maintenance avant la concision ou la "beauté" de certaines astuces... Ce n'est pas un simple conseil, c'est une supplique. Vous me remercierez un jour.
Le modèle MVVM s'accommode parfaitement de cette situation puisque, par définition, le ViewModel n’a pas besoin de connaître la View qui l’utilise.
Développer un exemple serait ici trop long car il faudrait en plus choisir une boîte à outils MVVM ce qui compliquerait tout. Si vous avez compris le principe pour le chargement partiel de blocs avec des ContentView, vous voyez certainement l'esprit assez prochez d'un chargement de ContentPage différents.
Conclusion
De base, MAUI nous fournit tout ce qu’il faut pour réaliser un design adaptatif. Il suffit simplement de les utiliser à bon escient !
Il existe de nombreuses autres techniques et astuces. Par exemple, certains contrôles comme les grilles (Grid
) ou les grilles relatives (RelativeLayout
) sont de précieux alliés pour structurer l’interface utilisateur de manière flexible. Toutefois, ces contrôles peuvent souvent complexifier la logique de mise en page et rendre l’ensemble moins maintenable que des ContentViews
différenciées ou même des pages totalement distinctes.
Il est donc crucial de bien choisir vos outils en fonction du contexte et des besoins spécifiques de votre application.
Stay Tuned !