Partager via


Créer une disposition personnalisée dans Xamarin.Forms

Xamarin.Forms définit cinq classes de disposition : StackLayout, AbsoluteLayout, RelativeLayout, Grid et FlexLayout, et chacune organise ses enfants d’une manière différente. Toutefois, il est parfois nécessaire d’organiser le contenu de la page à l’aide d’une disposition non fournie par Xamarin.Forms. Cet article explique comment écrire une classe de disposition personnalisée et montre une classe WrapLayout sensible à l’orientation qui organise ses enfants horizontalement sur la page, puis encapsule l’affichage des enfants suivants sur des lignes supplémentaires.

Dans Xamarin.Forms, toutes les classes de disposition dérivent de la Layout<T> classe et limitent le type générique vers View et ses types dérivés. À son tour, la Layout<T> classe dérive de la Layout classe, qui fournit le mécanisme de positionnement et de dimensionnement des éléments enfants.

Chaque élément visuel est chargé de déterminer sa propre taille préférée, appelée taille demandée . Page, Layoutet Layout<View> les types dérivés sont responsables de la détermination de l’emplacement et de la taille de leur enfant, ou enfants, par rapport à eux-mêmes. Par conséquent, la disposition implique une relation parent-enfant, où le parent détermine la taille de ses enfants, mais tente de prendre en charge la taille demandée de l’enfant.

Une compréhension approfondie des cycles de Xamarin.Forms disposition et d’invalidation est nécessaire pour créer une disposition personnalisée. Ces cycles seront maintenant abordés.

Disposition

La disposition commence en haut de l’arborescence visuelle avec une page et passe par toutes les branches de l’arborescence visuelle pour englober chaque élément visuel d’une page. Les éléments parents d’autres éléments sont responsables du dimensionnement et du positionnement de leurs enfants par rapport à eux-mêmes.

La VisualElement classe définit une Measure méthode qui mesure un élément pour les opérations de disposition et une Layout méthode qui spécifie la zone rectangulaire dans laquelle l’élément sera rendu. Lorsqu’une application démarre et que la première page s’affiche, un cycle de disposition composé en premier des Measure appels, puis Layout des appels, démarre sur l’objet Page :

  1. Pendant le cycle de disposition, chaque élément parent est responsable de l’appel de la Measure méthode sur ses enfants.
  2. Une fois les enfants mesurés, chaque élément parent est responsable de l’appel de la Layout méthode sur ses enfants.

Ce cycle garantit que chaque élément visuel de la page reçoit des appels aux méthodes et Layout aux Measure méthodes. Le processus est illustré dans le diagramme suivant :

Xamarin.Forms Cycle de disposition

Remarque

Notez que les cycles de disposition peuvent également se produire sur un sous-ensemble de l’arborescence visuelle si quelque chose change pour affecter la disposition. Cela inclut les éléments ajoutés ou supprimés d’une collection comme dans un StackLayout, une modification de la IsVisible propriété d’un élément ou une modification de la taille d’un élément.

Chaque Xamarin.Forms classe qui a une Content ou une Children propriété a une méthode substituable LayoutChildren . Les classes de disposition personnalisées qui dérivent de Layout<View> doivent remplacer cette méthode et s’assurer que les méthodes et Layout les Measure méthodes sont appelées sur tous les enfants de l’élément pour fournir la disposition personnalisée souhaitée.

En outre, chaque classe qui dérive de Layout la méthode ou Layout<View> doit remplacer la OnMeasure méthode, c’est-à-dire où une classe de disposition détermine la taille à laquelle elle doit être en effectuant des appels aux Measure méthodes de ses enfants.

Remarque

Les éléments déterminent leur taille en fonction des contraintes, ce qui indique la quantité d’espace disponible pour un élément dans le parent de l’élément. Les contraintes passées aux Measure OnMeasure méthodes peuvent aller de 0 à Double.PositiveInfinity. Un élément est contraint, ou entièrement contraint, lorsqu’il reçoit un appel à sa Measure méthode avec des arguments non infinis , l’élément est limité à une taille particulière. Un élément n’est pas contraint, ou partiellement contraint, lorsqu’il reçoit un appel à sa Measure méthode avec au moins un argument égal à Double.PositiveInfinity : la contrainte infinie peut être considérée comme indiquant le redimensionnement automatique.

Invalidation

L’invalidation est le processus par lequel une modification dans un élément d’une page déclenche un nouveau cycle de disposition. Les éléments sont considérés comme non valides lorsqu’ils n’ont plus la taille ou la position correctes. Par exemple, si la FontSize propriété d’une Button modification est considérée comme non valide, Button car elle n’aura plus la taille correcte. Redimensionner le Button peut alors avoir un effet d’onde sur les modifications apportées à la mise en page par le reste d’une page.

Les éléments s’invalident en appelant la InvalidateMeasure méthode, généralement lorsqu’une propriété de l’élément change, ce qui peut entraîner une nouvelle taille de l’élément. Cette méthode déclenche l’événement MeasureInvalidated , que le parent de l’élément gère pour déclencher un nouveau cycle de disposition.

La Layout classe définit un gestionnaire pour l’événement MeasureInvalidated sur chaque enfant ajouté à sa Content propriété ou Children collection et détache le gestionnaire lorsque l’enfant est supprimé. Par conséquent, chaque élément de l’arborescence visuelle qui a des enfants est alerté chaque fois qu’un de ses enfants change de taille. Le diagramme suivant montre comment une modification de la taille d’un élément dans l’arborescence visuelle peut entraîner des changements qui se propagent dans l’arborescence :

Invalidation dans l’arborescence des visuels

Toutefois, la Layout classe tente de limiter l’impact d’une modification de la taille d’un enfant sur la mise en page d’une page. Si la disposition est limitée, une modification de taille enfant n’affecte rien de plus élevé que la disposition parente dans l’arborescence visuelle. Toutefois, généralement, une modification de la taille d’une disposition affecte la façon dont la disposition organise ses enfants. Par conséquent, toute modification de la taille d’une disposition démarre un cycle de disposition pour la disposition, et la disposition reçoit des appels à ses OnMeasure méthodes.LayoutChildren

La Layout classe définit également une InvalidateLayout méthode qui a un objectif similaire à la InvalidateMeasure méthode. La InvalidateLayout méthode doit être appelée chaque fois qu’une modification est apportée qui affecte la façon dont la disposition positionne et dimensionne ses enfants. Par exemple, la Layout classe appelle la InvalidateLayout méthode chaque fois qu’un enfant est ajouté ou supprimé d’une disposition.

Il InvalidateLayout peut être substitué pour implémenter un cache afin de réduire les appels répétitifs des Measure méthodes des enfants de la disposition. La substitution de la InvalidateLayout méthode fournit une notification indiquant quand les enfants sont ajoutés ou supprimés de la disposition. De même, la OnChildMeasureInvalidated méthode peut être remplacée pour fournir une notification lorsque l’un des enfants de la disposition change de taille. Pour les deux remplacements de méthode, une disposition personnalisée doit répondre en désactivant le cache. Pour plus d’informations, consultez Calculer et mettre en cache les données de disposition.

Créer une disposition personnalisée

Le processus de création d’une disposition personnalisée est le suivant :

  1. Créez une classe qui dérive de la classe Layout<View>. Pour plus d’informations, consultez Créer un WrapLayout.

  2. [facultatif] Ajoutez des propriétés, sauvegardées par des propriétés pouvant être liées, pour tous les paramètres qui doivent être définis sur la classe de disposition. Pour plus d’informations, consultez Ajouter des propriétés sauvegardées par des propriétés pouvant être liées.

  3. Remplacez la OnMeasure méthode pour appeler la Measure méthode sur tous les enfants de la disposition et renvoyez une taille demandée pour la disposition. Pour plus d’informations, consultez Remplacer la méthode OnMeasure.

  4. Remplacez la LayoutChildren méthode pour appeler la Layout méthode sur tous les enfants de la disposition. Si vous ne parvenez pas à appeler la Layout méthode sur chaque enfant d’une disposition, l’enfant ne reçoit jamais une taille ou une position correctes. Par conséquent, l’enfant ne sera pas visible sur la page. Pour plus d’informations, consultez Remplacer la méthode LayoutChildren.

    Remarque

    Lors de l’énumération des enfants dans et OnMeasure LayoutChildren remplace, ignorez tout enfant dont IsVisible la propriété est définie falsesur . Cela garantit que la disposition personnalisée ne laissera pas l’espace pour les enfants invisibles.

  5. [facultatif] Remplacez la InvalidateLayout méthode à avertir lorsque les enfants sont ajoutés ou supprimés de la disposition. Pour plus d’informations, consultez Remplacer la méthode InvalidateLayout.

  6. [facultatif] Remplacez la OnChildMeasureInvalidated méthode à avertir quand l’un des enfants de la disposition change de taille. Pour plus d’informations, consultez Remplacer la méthode OnChildMeasureInvalidated.

Remarque

Notez que le OnMeasure remplacement ne sera pas appelé si la taille de la disposition est régie par son parent, plutôt que par ses enfants. Toutefois, le remplacement est appelé si une ou les deux contraintes sont infinies, ou si la classe de disposition a des valeurs non par défaut HorizontalOptions ou VerticalOptions de propriété. Pour cette raison, le LayoutChildren remplacement ne peut pas compter sur les tailles enfants obtenues pendant l’appel OnMeasure de méthode. Au lieu de cela, LayoutChildren vous devez appeler la Measure méthode sur les enfants de la disposition avant d’appeler la Layout méthode. Vous pouvez également mettre en cache la taille des enfants obtenus dans le OnMeasure remplacement pour éviter les appels ultérieurs Measure dans le LayoutChildren remplacement, mais la classe de disposition doit savoir quand les tailles doivent être obtenues à nouveau. Pour plus d’informations, consultez Calculer et mettre en cache les données de disposition.

La classe de disposition peut ensuite être consommée en l’ajoutant à un Page, et en ajoutant des enfants à la disposition. Pour plus d’informations, consultez Utiliser WrapLayout.

Créer un WrapLayout

L’exemple d’application illustre une classe sensible à WrapLayout l’orientation qui organise ses enfants horizontalement sur la page, puis encapsule l’affichage des enfants suivants sur des lignes supplémentaires.

La WrapLayout classe alloue la même quantité d’espace pour chaque enfant, appelée taille de cellule, en fonction de la taille maximale des enfants. Les enfants plus petits que la taille de cellule peuvent être positionnés dans la cellule en fonction de leurs valeurs de propriété et VerticalOptions de leurs HorizontalOptions valeurs.

La WrapLayout définition de classe est illustrée dans l’exemple de code suivant :

public class WrapLayout : Layout<View>
{
  Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
  ...
}

Calculer et mettre en cache les données de disposition

La LayoutData structure stocke des données sur une collection d’enfants dans un certain nombre de propriétés :

  • VisibleChildCount : nombre d’enfants visibles dans la disposition.
  • CellSize : taille maximale de tous les enfants, ajustée à la taille de la disposition.
  • Rows : nombre de lignes.
  • Columns : nombre de colonnes.

Le layoutDataCache champ est utilisé pour stocker plusieurs LayoutData valeurs. Au démarrage de l’application, deux LayoutData objets sont mis en cache dans le layoutDataCache dictionnaire pour l’orientation actuelle : un pour les arguments de contrainte pour le OnMeasure remplacement, et un pour les width arguments height et les arguments du LayoutChildren remplacement. Lors de la rotation de l’appareil en orientation paysage, le OnMeasure remplacement et le LayoutChildren remplacement sont à nouveau appelés, ce qui entraîne la mise en cache de deux autres LayoutData objets dans le dictionnaire. Toutefois, lors du retour de l’appareil à l’orientation portrait, aucun calcul supplémentaire n’est requis, car les layoutDataCache données requises sont déjà présentes.

L’exemple de code suivant montre la GetLayoutData méthode, qui calcule les propriétés de la LayoutData structure en fonction d’une taille particulière :

LayoutData GetLayoutData(double width, double height)
{
  Size size = new Size(width, height);

  // Check if cached information is available.
  if (layoutDataCache.ContainsKey(size))
  {
    return layoutDataCache[size];
  }

  int visibleChildCount = 0;
  Size maxChildSize = new Size();
  int rows = 0;
  int columns = 0;
  LayoutData layoutData = new LayoutData();

  // Enumerate through all the children.
  foreach (View child in Children)
  {
    // Skip invisible children.
    if (!child.IsVisible)
      continue;

    // Count the visible children.
    visibleChildCount++;

    // Get the child's requested size.
    SizeRequest childSizeRequest = child.Measure(Double.PositiveInfinity, Double.PositiveInfinity);

    // Accumulate the maximum child size.
    maxChildSize.Width = Math.Max(maxChildSize.Width, childSizeRequest.Request.Width);
    maxChildSize.Height = Math.Max(maxChildSize.Height, childSizeRequest.Request.Height);
  }

  if (visibleChildCount != 0)
  {
    // Calculate the number of rows and columns.
    if (Double.IsPositiveInfinity(width))
    {
      columns = visibleChildCount;
      rows = 1;
    }
    else
    {
      columns = (int)((width + ColumnSpacing) / (maxChildSize.Width + ColumnSpacing));
      columns = Math.Max(1, columns);
      rows = (visibleChildCount + columns - 1) / columns;
    }

    // Now maximize the cell size based on the layout size.
    Size cellSize = new Size();

    if (Double.IsPositiveInfinity(width))
      cellSize.Width = maxChildSize.Width;
    else
      cellSize.Width = (width - ColumnSpacing * (columns - 1)) / columns;

    if (Double.IsPositiveInfinity(height))
      cellSize.Height = maxChildSize.Height;
    else
      cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;

    layoutData = new LayoutData(visibleChildCount, cellSize, rows, columns);
  }

  layoutDataCache.Add(size, layoutData);
  return layoutData;
}

La GetLayoutData méthode effectue les opérations suivantes :

  • Elle détermine si une valeur calculée LayoutData est déjà dans le cache et la retourne si elle est disponible.
  • Sinon, il énumère tous les enfants, appelant la Measure méthode sur chaque enfant avec une largeur et une hauteur infinies, et détermine la taille maximale de l’enfant.
  • À condition qu’il y ait au moins un enfant visible, il calcule le nombre de lignes et de colonnes requises, puis calcule une taille de cellule pour les enfants en fonction des dimensions du WrapLayout. Notez que la taille de cellule est généralement légèrement plus large que la taille maximale de l’enfant, mais qu’elle peut également être plus petite si elle WrapLayout n’est pas suffisamment large pour l’enfant le plus large ou assez grand pour l’enfant le plus grand.
  • Il stocke la nouvelle LayoutData valeur dans le cache.

Ajouter des propriétés sauvegardées par des propriétés pouvant être liées

La WrapLayout classe définit ColumnSpacing et RowSpacing propriétés, dont les valeurs sont utilisées pour séparer les lignes et les colonnes de la disposition, et qui sont sauvegardées par des propriétés pouvant être liées. Les propriétés pouvant être liées sont présentées dans l’exemple de code suivant :

public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(
  "ColumnSpacing",
  typeof(double),
  typeof(WrapLayout),
  5.0,
  propertyChanged: (bindable, oldvalue, newvalue) =>
  {
    ((WrapLayout)bindable).InvalidateLayout();
  });

public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(
  "RowSpacing",
  typeof(double),
  typeof(WrapLayout),
  5.0,
  propertyChanged: (bindable, oldvalue, newvalue) =>
  {
    ((WrapLayout)bindable).InvalidateLayout();
  });

Le gestionnaire modifié par propriété de chaque propriété pouvant être liée appelle le remplacement de la InvalidateLayout méthode pour déclencher un nouveau passage de disposition sur le WrapLayout. Pour plus d’informations, consultez Remplacer la méthode InvalidateLayout et Remplacer la méthode OnChildMeasureInvalidated.

Remplacer la méthode OnMeasure

Le OnMeasure remplacement est illustré dans l’exemple de code suivant :

protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
  LayoutData layoutData = GetLayoutData(widthConstraint, heightConstraint);
  if (layoutData.VisibleChildCount == 0)
  {
    return new SizeRequest();
  }

  Size totalSize = new Size(layoutData.CellSize.Width * layoutData.Columns + ColumnSpacing * (layoutData.Columns - 1),
                layoutData.CellSize.Height * layoutData.Rows + RowSpacing * (layoutData.Rows - 1));
  return new SizeRequest(totalSize);
}

Le remplacement appelle la GetLayoutData méthode et construit un SizeRequest objet à partir des données retournées, tout en tenant compte des valeurs des propriétés et ColumnSpacing des RowSpacing valeurs. Pour plus d’informations sur la GetLayoutData méthode, consultez Calculate et Cache Layout Data.

Important

Les Measure méthodes et OnMeasure ne doivent jamais demander une dimension infinie en retournant une SizeRequest valeur avec une propriété définie sur Double.PositiveInfinity. Toutefois, au moins l’un des arguments de contrainte à OnMeasure utiliser peut être Double.PositiveInfinity.

Remplacer la méthode LayoutChildren

Le LayoutChildren remplacement est illustré dans l’exemple de code suivant :

protected override void LayoutChildren(double x, double y, double width, double height)
{
  LayoutData layoutData = GetLayoutData(width, height);

  if (layoutData.VisibleChildCount == 0)
  {
    return;
  }

  double xChild = x;
  double yChild = y;
  int row = 0;
  int column = 0;

  foreach (View child in Children)
  {
    if (!child.IsVisible)
    {
      continue;
    }

    LayoutChildIntoBoundingRegion(child, new Rectangle(new Point(xChild, yChild), layoutData.CellSize));
    if (++column == layoutData.Columns)
    {
      column = 0;
      row++;
      xChild = x;
      yChild += RowSpacing + layoutData.CellSize.Height;
    }
    else
    {
      xChild += ColumnSpacing + layoutData.CellSize.Width;
    }
  }
}

La substitution commence par un appel à la GetLayoutData méthode, puis énumère tous les enfants pour les dimensionner et les positionner dans la cellule de chaque enfant. Pour ce faire, appelez la LayoutChildIntoBoundingRegion méthode, utilisée pour positionner un enfant dans un rectangle en fonction de ses valeurs de propriété et VerticalOptions de ses HorizontalOptions valeurs. Cela équivaut à appeler la méthode de Layout l’enfant.

Remarque

Notez que le rectangle passé à la LayoutChildIntoBoundingRegion méthode inclut la zone entière dans laquelle l’enfant peut résider.

Pour plus d’informations sur la GetLayoutData méthode, consultez Calculate et Cache Layout Data.

Remplacer la méthode InvalidateLayout

Le InvalidateLayout remplacement est appelé lorsque les enfants sont ajoutés ou supprimés de la disposition, ou lorsque l’une des WrapLayout propriétés change de valeur, comme indiqué dans l’exemple de code suivant :

protected override void InvalidateLayout()
{
  base.InvalidateLayout();
  layoutInfoCache.Clear();
}

La substitution invalide la disposition et ignore toutes les informations de disposition mises en cache.

Remarque

Pour arrêter la classe appelant la Layout InvalidateLayout méthode chaque fois qu’un enfant est ajouté ou supprimé d’une disposition, remplacez les ShouldInvalidateOnChildAdded méthodes et ShouldInvalidateOnChildRemoved les méthodes et retournez false. La classe layout peut ensuite implémenter un processus personnalisé lorsque les enfants sont ajoutés ou supprimés.

Remplacer la méthode OnChildMeasureInvalidated

Le OnChildMeasureInvalidated remplacement est appelé lorsque l’un des enfants de la disposition change de taille et est illustré dans l’exemple de code suivant :

protected override void OnChildMeasureInvalidated()
{
  base.OnChildMeasureInvalidated();
  layoutInfoCache.Clear();
}

La substitution invalide la disposition enfant et ignore toutes les informations de disposition mises en cache.

Utiliser wrapLayout

La WrapLayout classe peut être consommée en la plaçant sur un Page type dérivé, comme illustré dans l’exemple de code XAML suivant :

<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
    <ScrollView Margin="0,20,0,20">
        <local:WrapLayout x:Name="wrapLayout" />
    </ScrollView>
</ContentPage>

Le code C# équivalent est illustré ci-dessous :

public class ImageWrapLayoutPageCS : ContentPage
{
  WrapLayout wrapLayout;

  public ImageWrapLayoutPageCS()
  {
    wrapLayout = new WrapLayout();

    Content = new ScrollView
    {
      Margin = new Thickness(0, 20, 0, 20),
      Content = wrapLayout
    };
  }
  ...
}

Les enfants peuvent ensuite être ajoutés au WrapLayout besoin. L’exemple de code suivant montre Image les éléments ajoutés au WrapLayout:

protected override async void OnAppearing()
{
    base.OnAppearing();

    var images = await GetImageListAsync();
    if (images != null)
    {
        foreach (var photo in images.Photos)
        {
            var image = new Image
            {
                Source = ImageSource.FromUri(new Uri(photo))
            };
            wrapLayout.Children.Add(image);
        }
    }
}

async Task<ImageList> GetImageListAsync()
{
    try
    {
        string requestUri = "https://raw.githubusercontent.com/xamarin/docs-archive/master/Images/stock/small/stock.json";
        string result = await _client.GetStringAsync(requestUri);
        return JsonConvert.DeserializeObject<ImageList>(result);
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"\tERROR: {ex.Message}");
    }

    return null;
}

Lorsque la page contenant l’affichage WrapLayout s’affiche, l’exemple d’application accède de façon asynchrone à un fichier JSON distant contenant une liste de photos, crée un Image élément pour chaque photo et l’ajoute au WrapLayoutfichier . Cela donne l’affichage illustré dans les captures d’écran suivantes :

Exemples de captures d’écran portrait d’application

Les captures d’écran suivantes montrent l’après WrapLayout avoir été pivotées vers l’orientation paysage :

Exemple de capture d’écran paysage de l’application iOSExemple de capture d’écran paysage d’application AndroidExemple de capture d’écran paysage de l’application UWP

Le nombre de colonnes de chaque ligne dépend de la taille de la photo, de la largeur de l’écran et du nombre de pixels par unité indépendante de l’appareil. Les Image éléments chargent de façon asynchrone les photos, et par conséquent, la WrapLayout classe reçoit des appels fréquents à sa LayoutChildren méthode, car chaque Image élément reçoit une nouvelle taille en fonction de la photo chargée.