Condividi tramite


Creare un layout personalizzato in Xamarin.Forms

Xamarin.Forms definisce cinque classi di layout: StackLayout, AbsoluteLayout, RelativeLayout, Grid e FlexLayout e ognuno dispone i relativi elementi figlio in modo diverso. Tuttavia, a volte è necessario organizzare il contenuto della pagina usando un layout non fornito da Xamarin.Forms. Questo articolo illustra come scrivere una classe di layout personalizzata e illustra una classe WrapLayout sensibile all'orientamento che dispone gli elementi figlio orizzontalmente nella pagina e quindi esegue il wrapping della visualizzazione degli elementi figlio successivi in righe aggiuntive.

In Xamarin.Formstutte le classi di layout derivano dalla Layout<T> classe e vincolano il tipo generico a e i View relativi tipi derivati. A sua volta, la Layout<T> classe deriva dalla Layout classe , che fornisce il meccanismo per posizionare e ridimensionare gli elementi figlio.

Ogni elemento visivo è responsabile della determinazione delle proprie dimensioni preferite, note come dimensioni richieste . PageI tipi derivati , Layoute Layout<View> sono responsabili della determinazione della posizione e delle dimensioni del figlio o dei figli rispetto a se stessi. Pertanto, il layout implica una relazione padre-figlio, in cui l'elemento padre determina le dimensioni dei relativi elementi figlio, ma tenterà di adattare le dimensioni richieste dell'elemento figlio.

Per creare un layout personalizzato, è necessaria una conoscenza approfondita dei Xamarin.Forms cicli di layout e invalidazione. Questi cicli verranno ora discussi.

Layout

Il layout inizia nella parte superiore della struttura ad albero visuale con una pagina e procede attraverso tutti i rami della struttura ad albero visuale per includere ogni elemento visivo in una pagina. Gli elementi padre di altri elementi sono responsabili del dimensionamento e del posizionamento dei figli rispetto a se stessi.

La VisualElement classe definisce un metodo che misura un Measure elemento per le operazioni di layout e un Layout metodo che specifica l'area rettangolare in cui verrà eseguito il rendering dell'elemento. All'avvio di un'applicazione e viene visualizzata la prima pagina, un ciclo di layout costituito prima di Measure chiamate e quindi Layout chiama, inizia sull'oggetto Page :

  1. Durante il ciclo di layout, ogni elemento padre è responsabile della chiamata del Measure metodo sui relativi elementi figlio.
  2. Dopo aver misurato gli elementi figlio, ogni elemento padre è responsabile della chiamata del Layout metodo sui relativi elementi figlio.

Questo ciclo garantisce che ogni elemento visivo nella pagina riceva chiamate ai Measure metodi e Layout . Il processo è illustrato nel diagramma seguente:

Xamarin.Forms Ciclo di layout

Nota

Si noti che i cicli di layout possono verificarsi anche in un subset della struttura ad albero visuale se si apportano modifiche al layout. Sono inclusi gli elementi aggiunti o rimossi da una raccolta, ad esempio in un StackLayoutoggetto , una modifica nella IsVisible proprietà di un elemento o una modifica delle dimensioni di un elemento.

Ogni Xamarin.Forms classe con una Content proprietà o dispone di Children un metodo sostituibile LayoutChildren . Le classi di layout personalizzate che derivano da Layout<View> devono eseguire l'override di questo metodo e assicurarsi che i Measure metodi e Layout vengano chiamati su tutti gli elementi figlio dell'elemento, per fornire il layout personalizzato desiderato.

Inoltre, ogni classe che deriva da Layout o Layout<View> deve eseguire l'override del OnMeasure metodo , dove una classe di layout determina le dimensioni che deve essere effettuando chiamate ai Measure metodi dei relativi elementi figlio.

Nota

Gli elementi determinano le dimensioni in base ai vincoli, che indicano la quantità di spazio disponibile per un elemento all'interno dell'elemento padre dell'elemento. I vincoli passati ai Measure metodi e OnMeasure possono variare da 0 a Double.PositiveInfinity. Un elemento è vincolato o completamente vincolato quando riceve una chiamata al relativo Measure metodo con argomenti non infiniti. L'elemento è vincolato a una determinata dimensione. Un elemento non è vincolato o parzialmente vincolato, quando riceve una chiamata al relativo Measure metodo con almeno un argomento uguale a Double.PositiveInfinity . Il vincolo infinito può essere considerato come indicare il ridimensionamento automatico.

Invalidazione

Invalidazione è il processo in base al quale una modifica di un elemento in una pagina attiva un nuovo ciclo di layout. Gli elementi vengono considerati non validi quando non hanno più le dimensioni o la posizione corrette. Ad esempio, se la FontSize proprietà di una Button modifica, l'oggetto Button viene detto non valido perché non avrà più le dimensioni corrette. Il ridimensionamento Button di può quindi avere un effetto increspante delle modifiche nel layout attraverso il resto di una pagina.

Gli elementi si invalidano richiamando il InvalidateMeasure metodo, in genere quando una proprietà dell'elemento cambia che potrebbe comportare una nuova dimensione dell'elemento. Questo metodo attiva l'evento MeasureInvalidated , gestito dall'elemento padre per attivare un nuovo ciclo di layout.

La Layout classe imposta un gestore per l'evento MeasureInvalidated in ogni elemento figlio aggiunto alla relativa Content proprietà o Children raccolta e scollega il gestore quando l'elemento figlio viene rimosso. Di conseguenza, ogni elemento nella struttura ad albero visuale con elementi figlio viene avvisato ogni volta che uno dei relativi elementi figlio cambia le dimensioni. Il diagramma seguente illustra come una modifica delle dimensioni di un elemento nella struttura ad albero visuale può causare modifiche che si increspano nell'albero:

Invalidazione nella struttura ad albero visuale

Tuttavia, la Layout classe tenta di limitare l'impatto di una modifica delle dimensioni di un figlio nel layout di una pagina. Se il layout è vincolato, una modifica delle dimensioni figlio non influisce su alcun valore superiore al layout padre nella struttura ad albero visuale. Tuttavia, in genere una modifica delle dimensioni di un layout influisce sulla disposizione dei relativi elementi figlio. Pertanto, qualsiasi modifica delle dimensioni di un layout avvierà un ciclo di layout per il layout e il layout riceverà chiamate ai relativi OnMeasure metodi e LayoutChildren .

La Layout classe definisce anche un InvalidateLayout metodo che ha uno scopo simile al InvalidateMeasure metodo . Il InvalidateLayout metodo deve essere richiamato ogni volta che viene apportata una modifica che influisce sul modo in cui il layout posiziona e ridimensiona i relativi elementi figlio. Ad esempio, la Layout classe richiama il InvalidateLayout metodo ogni volta che un elemento figlio viene aggiunto o rimosso da un layout.

Può InvalidateLayout essere sottoposto a override per implementare una cache per ridurre al minimo le chiamate ripetitive dei Measure metodi degli elementi figlio del layout. L'override del InvalidateLayout metodo fornirà una notifica di quando gli elementi figlio vengono aggiunti o rimossi dal layout. Analogamente, è possibile eseguire l'override del OnChildMeasureInvalidated metodo per fornire una notifica quando una delle dimensioni figlio del layout cambia. Per entrambi gli override del metodo, un layout personalizzato deve rispondere cancellando la cache. Per altre informazioni, vedere Calculate and Cache Layout Data.For more information, see Calculate and Cache Layout Data.

Creare un layout personalizzato

Il processo di creazione di un layout personalizzato è il seguente:

  1. Creare una classe che derivi dalla classe Layout<View>. Per altre informazioni, vedere Creare un wrapLayout.

  2. [facoltativo] Aggiungere proprietà, supportate da proprietà associabili, per tutti i parametri che devono essere impostati nella classe di layout. Per altre informazioni, vedere Aggiungere proprietà supportate da proprietà associabili.

  3. Eseguire l'override del OnMeasure metodo per richiamare il Measure metodo su tutti gli elementi figlio del layout e restituire una dimensione richiesta per il layout. Per altre informazioni, vedere Eseguire l'override del metodo OnMeasure.

  4. Eseguire l'override del LayoutChildren metodo per richiamare il Layout metodo su tutti gli elementi figlio del layout. Se non si richiama il Layout metodo su ogni elemento figlio in un layout, l'elemento figlio non riceverà mai una dimensione o una posizione corretta e pertanto l'elemento figlio non diventerà visibile nella pagina. Per altre informazioni, vedere Eseguire l'override del metodo LayoutChildren.

    Nota

    Durante l'enumerazione degli elementi figlio nell'oggetto e LayoutChildren viene eseguito l'overrideOnMeasure, ignorare qualsiasi elemento figlio la cui IsVisible proprietà sia impostata su false. In questo modo, il layout personalizzato non lascerà spazio per gli elementi figlio invisibili.

  5. [facoltativo] Eseguire l'override del InvalidateLayout metodo per ricevere una notifica quando gli elementi figlio vengono aggiunti o rimossi dal layout. Per altre informazioni, vedere Eseguire l'override del metodo InvalidateLayout.

  6. [facoltativo] Eseguire l'override del OnChildMeasureInvalidated metodo per ricevere una notifica quando una delle dimensioni figlio del layout cambia. Per altre informazioni, vedere Eseguire l'override del metodo OnChildMeasureInvalidated.

Nota

Si noti che l'override OnMeasure non verrà richiamato se le dimensioni del layout sono regolate dal relativo elemento padre, anziché dai relativi elementi figlio. Tuttavia, l'override verrà richiamato se uno o entrambi i vincoli sono infiniti o se la classe di layout ha valori di proprietà o VerticalOptions non predefinitiHorizontalOptions. Per questo motivo, l'override LayoutChildren non può basarsi sulle dimensioni figlio ottenute durante la chiamata al OnMeasure metodo. È invece LayoutChildren necessario richiamare il Measure metodo sugli elementi figlio del layout, prima di richiamare il Layout metodo . In alternativa, le dimensioni degli elementi figlio ottenuti nell'override OnMeasure possono essere memorizzate nella cache per evitare chiamate successive Measure nell'override LayoutChildren , ma la classe di layout dovrà sapere quando è necessario ottenere nuovamente le dimensioni. Per altre informazioni, vedere Calculate and Cache Layout Data.For more information, see Calculate and Cache Layout Data.

La classe di layout può quindi essere utilizzata aggiungendola a un Pageoggetto e aggiungendo elementi figlio al layout. Per altre informazioni, vedere Utilizzare WrapLayout.

Creare un WrapLayout

L'applicazione di esempio illustra una classe sensibile all'orientamento WrapLayout che dispone orizzontalmente i relativi elementi figlio nella pagina e quindi esegue il wrapping della visualizzazione degli elementi figlio successivi in righe aggiuntive.

La WrapLayout classe alloca la stessa quantità di spazio per ogni figlio, nota come dimensione della cella, in base alle dimensioni massime degli elementi figlio. I figli minori delle dimensioni della cella possono essere posizionati all'interno della cella in base ai HorizontalOptions valori delle proprietà e VerticalOptions .

La definizione della WrapLayout classe è illustrata nell'esempio di codice seguente:

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

Calcolare e memorizzare nella cache i dati di layout

La LayoutData struttura archivia i dati relativi a una raccolta di elementi figlio in una serie di proprietà:

  • VisibleChildCount : numero di elementi figlio visibili nel layout.
  • CellSize : dimensioni massime di tutti gli elementi figlio, regolate in base alle dimensioni del layout.
  • Rows : numero di righe.
  • Columns : numero di colonne.

Il layoutDataCache campo viene usato per archiviare più LayoutData valori. All'avvio dell'applicazione, due LayoutData oggetti verranno memorizzati nella cache nel layoutDataCache dizionario per l'orientamento corrente, uno per gli argomenti vincolo dell'override OnMeasure e uno per gli width argomenti e height per l'override LayoutChildren . Quando si ruota il dispositivo in orientamento orizzontale, l'override OnMeasure e l'override LayoutChildren verranno richiamati di nuovo, il che comporterà la memorizzazione di altri due LayoutData oggetti nella cache nel dizionario. Tuttavia, quando si restituisce il dispositivo all'orientamento verticale, non sono necessari altri calcoli perché i layoutDataCache dati necessari sono già presenti.

Nell'esempio di codice seguente viene illustrato il GetLayoutData metodo , che calcola le proprietà dell'oggetto LayoutData strutturato in base a una dimensione specifica:

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;
}

Il GetLayoutData metodo esegue le operazioni seguenti:

  • Determina se un valore calcolato LayoutData è già presente nella cache e lo restituisce, se disponibile.
  • In caso contrario, enumera tutti gli elementi figlio, richiamando il Measure metodo su ogni figlio con larghezza e altezza infinite e determina la dimensione massima figlio.
  • A condizione che sia presente almeno un elemento figlio visibile, calcola il numero di righe e colonne necessarie e quindi calcola le dimensioni di una cella per gli elementi figlio in base alle dimensioni dell'oggetto WrapLayout. Si noti che la dimensione della cella è in genere leggermente più ampia rispetto alla dimensione massima figlio, ma che potrebbe anche essere più piccola se non WrapLayout è abbastanza grande per il bambino più largo o abbastanza alto per il bambino più alto.
  • Archivia il nuovo LayoutData valore nella cache.

Aggiungere proprietà supportate dalle proprietà associabili

La WrapLayout classe definisce ColumnSpacing le proprietà e RowSpacing , i cui valori vengono utilizzati per separare le righe e le colonne nel layout e supportate da proprietà associabili. Le proprietà associabili sono illustrate nell'esempio di codice seguente:

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();
  });

Il gestore delle proprietà modificate di ogni proprietà associabile richiama l'override del InvalidateLayout metodo per attivare un nuovo passaggio di layout su WrapLayout. Per altre informazioni, vedere Eseguire l'override del metodo InvalidateLayout ed eseguire l'override del metodo OnChildMeasureInvalidated.

Eseguire l'override del metodo OnMeasure

L'override OnMeasure è illustrato nell'esempio di codice seguente:

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);
}

L'override richiama il GetLayoutData metodo e costruisce un SizeRequest oggetto dai dati restituiti, tenendo conto anche dei valori delle RowSpacing proprietà e ColumnSpacing . Per altre informazioni sul metodo, vedere Calculate and Cache Layout Data.For more information about the GetLayoutData method, see Calculate and Cache Layout Data.

Importante

I Measure metodi e OnMeasure non devono mai richiedere una dimensione infinita restituendo un SizeRequest valore con una proprietà impostata su Double.PositiveInfinity. Tuttavia, almeno uno degli argomenti del vincolo da OnMeasure può essere Double.PositiveInfinity.

Eseguire l'override del metodo LayoutChildren

L'override LayoutChildren è illustrato nell'esempio di codice seguente:

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;
    }
  }
}

L'override inizia con una chiamata al GetLayoutData metodo e quindi enumera tutti gli elementi figlio per ridimensionarli e posizionarli all'interno della cella di ogni figlio. Questo risultato viene ottenuto richiamando il LayoutChildIntoBoundingRegion metodo , che viene usato per posizionare un elemento figlio all'interno di un rettangolo in base ai HorizontalOptions relativi valori di proprietà e VerticalOptions . Equivale a eseguire una chiamata al metodo dell'elemento Layout figlio.

Nota

Si noti che il rettangolo passato al LayoutChildIntoBoundingRegion metodo include l'intera area in cui può risiedere l'elemento figlio.

Per altre informazioni sul metodo, vedere Calculate and Cache Layout Data.For more information about the GetLayoutData method, see Calculate and Cache Layout Data.

Eseguire l'override del metodo InvalidateLayout

L'override InvalidateLayout viene richiamato quando gli elementi figlio vengono aggiunti o rimossi dal layout o quando una delle WrapLayout proprietà cambia valore, come illustrato nell'esempio di codice seguente:

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

L'override invalida il layout e rimuove tutte le informazioni sul layout memorizzate nella cache.

Nota

Per arrestare la classe richiamando il InvalidateLayout metodo ogni volta che un elemento figlio viene aggiunto o rimosso da un layout, eseguire l'override Layout dei ShouldInvalidateOnChildAdded metodi e ShouldInvalidateOnChildRemoved e restituire false. La classe di layout può quindi implementare un processo personalizzato quando gli elementi figlio vengono aggiunti o rimossi.

Eseguire l'override del metodo OnChildMeasureInvalidated

L'override OnChildMeasureInvalidated viene richiamato quando uno degli elementi figlio del layout cambia le dimensioni e viene illustrato nell'esempio di codice seguente:

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

L'override invalida il layout figlio e rimuove tutte le informazioni sul layout memorizzate nella cache.

Utilizzare WrapLayout

La WrapLayout classe può essere utilizzata inserendola su un Page tipo derivato, come illustrato nell'esempio di codice XAML seguente:

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

Il codice C# equivalente è illustrato di seguito:

public class ImageWrapLayoutPageCS : ContentPage
{
  WrapLayout wrapLayout;

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

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

Gli elementi figlio possono quindi essere aggiunti all'oggetto WrapLayout in base alle esigenze. Nell'esempio di codice seguente vengono illustrati Image gli elementi aggiunti a 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;
}

Quando viene visualizzata la pagina contenente WrapLayout , l'applicazione di esempio accede in modo asincrono a un file JSON remoto contenente un elenco di foto, crea un Image elemento per ogni foto e lo aggiunge a WrapLayout. Il risultato è l'aspetto illustrato negli screenshot seguenti:

Screenshot verticale dell'applicazione di esempio

Gli screenshot seguenti mostrano l'oggetto WrapLayout dopo che è stato ruotato sull'orientamento orizzontale:

Screenshot del panorama dell'applicazione iOS di esempioScreenshot orizzontale dell'applicazione Android di esempioScreenshot del panorama dell'applicazione UWP di esempio

Il numero di colonne in ogni riga dipende dalle dimensioni della foto, dalla larghezza dello schermo e dal numero di pixel per unità indipendente dal dispositivo. Gli Image elementi caricano in modo asincrono le foto e quindi la WrapLayout classe riceverà chiamate frequenti al relativo LayoutChildren metodo perché ogni Image elemento riceve una nuova dimensione in base alla foto caricata.