Layout personalizzati
L'interfaccia utente dell'app multipiattaforma .NET (.NET MAUI) definisce più classi di layout che dispongono dei relativi elementi figlio in modo diverso. Un layout può essere considerato come un elenco di visualizzazioni con regole e proprietà che definiscono come disporre tali visualizzazioni all'interno del layout. Esempi di layout includono Grid, AbsoluteLayoute VerticalStackLayout.
Le classi di layout MAUI .NET derivano dalla classe astratta Layout . Questa classe delega il layout e la misurazione multipiattaforma a una classe di gestione layout. La Layout classe contiene anche un metodo sostituibile CreateLayoutManager() che i layout derivati possono usare per specificare la gestione layout.
Ogni classe di gestione layout implementa l'interfaccia ILayoutManager , che specifica che Measure e ArrangeChildren le implementazioni devono essere fornite:
- L'implementazione Measure chiama IView.Measure in ogni visualizzazione del layout e restituisce le dimensioni totali del layout in base ai vincoli.
- L'implementazione ArrangeChildren determina la posizione di ogni visualizzazione all'interno dei limiti del layout e le chiamate Arrange a ogni visualizzazione con i limiti appropriati. Il valore restituito è la dimensione effettiva del layout.
I layout di .NET MAUI hanno gestori di layout predefiniti per gestire il layout. Tuttavia, a volte è necessario organizzare il contenuto della pagina usando un layout non fornito da .NET MAUI. A tale scopo, è possibile produrre un layout personalizzato, che richiede una conoscenza del funzionamento del processo di layout multipiattaforma di .NET MAUI.
Processo di layout
Il processo di layout multipiattaforma di .NET MAUI si basa sul processo di layout nativo in ogni piattaforma. In genere, il processo di layout viene avviato dal sistema di layout nativo. Il processo multipiattaforma viene eseguito quando un controllo layout o contenuto lo avvia in seguito alla misurazione o alla disposizione da parte del sistema di layout nativo.
Nota
Ogni piattaforma gestisce il layout leggermente diverso. Tuttavia, il processo di layout multipiattaforma di .NET MAUI mira a essere il più indipendente dalla piattaforma possibile.
Il diagramma seguente mostra il processo quando un sistema di layout nativo avvia la misurazione del layout:
Tutti i layout MAUI .NET hanno una singola visualizzazione di backup in ogni piattaforma:
- In Android questa visualizzazione di backup è
LayoutViewGroup
. - In iOS e Mac Catalyst questa visualizzazione di backup è
LayoutView
. - In Windows questa visualizzazione di backup è
LayoutPanel
.
Quando il sistema di layout nativo per una piattaforma richiede la misurazione di una di queste visualizzazioni di supporto, la visualizzazione di supporto chiama il Layout.CrossPlatformMeasure metodo . Questo è il punto in cui il controllo viene passato dal sistema di layout nativo al sistema di layout MAUI di .NET. Layout.CrossPlatformMeasure chiama il metodo dei Measure gestori di layout. Questo metodo è responsabile della misurazione delle visualizzazioni figlio chiamando IView.Measure su ogni visualizzazione nel layout. La vista misura il controllo nativo e ne aggiorna la DesiredSize proprietà in base a tale misura. Questo valore viene restituito alla visualizzazione di backup come risultato del CrossPlatformMeasure
metodo . La visualizzazione di backup esegue qualsiasi elaborazione interna che deve eseguire e restituisce le dimensioni misurate alla piattaforma.
Il diagramma seguente mostra il processo quando un sistema di layout nativo avvia la disposizione del layout:
Quando il sistema di layout nativo per una piattaforma richiede la disposizione o il layout di una di queste visualizzazioni di supporto, la visualizzazione di supporto chiama il Layout.CrossPlatformArrange metodo . Questo è il punto in cui il controllo viene passato dal sistema di layout nativo al sistema di layout MAUI di .NET. Layout.CrossPlatformArrange chiama il metodo dei ArrangeChildren gestori di layout. Questo metodo è responsabile della determinazione della posizione in cui ogni visualizzazione deve essere posizionata entro i limiti del layout e chiama Arrange su ogni visualizzazione per impostarne la posizione. Le dimensioni del layout vengono restituite alla visualizzazione di backup come risultato del CrossPlatformArrange
metodo . La visualizzazione di backup esegue qualsiasi elaborazione interna che deve eseguire e restituisce le dimensioni effettive alla piattaforma.
Nota
ILayoutManager.Measure può essere chiamato più volte prima ArrangeChildren di essere chiamato, perché una piattaforma potrebbe dover eseguire alcune misurazioni speculative prima di organizzare le visualizzazioni.
Approcci di layout personalizzati
Esistono due approcci principali per la creazione di un layout personalizzato:
- Creare un tipo di layout personalizzato, che in genere è una sottoclasse di un tipo di layout esistente o di Layoute eseguire l'override CreateLayoutManager() nel tipo di layout personalizzato. Fornire quindi un'implementazione ILayoutManager che contiene la logica di layout personalizzata. Per altre informazioni, vedere Creare un tipo di layout personalizzato.
- Modificare il comportamento di un tipo di layout esistente creando un tipo che implementa ILayoutManagerFactory. Usare quindi questa factory di Gestione layout per sostituire la gestione layout predefinita di .NET MAUI per il layout esistente con la propria ILayoutManager implementazione che contiene la logica di layout personalizzata. Per altre informazioni, vedere Modificare il comportamento di un layout esistente.
Creare un tipo di layout personalizzato
Il processo di creazione di un tipo di layout personalizzato consiste nel:
Creare una classe che sottoclassi un tipo di layout esistente o la classe ed eseguire l'override Layout CreateLayoutManager() nel tipo di layout personalizzato. Per altre informazioni, vedere Sottoclasse di un layout.
Creare una classe di gestione layout che deriva da un gestore layout esistente o che implementa direttamente l'interfaccia ILayoutManager . Nella classe di gestione layout è necessario:
- Eseguire l'override o implementare il Measure metodo per calcolare le dimensioni totali del layout in base ai relativi vincoli.
- Eseguire l'override o implementare il ArrangeChildren metodo per ridimensionare e posizionare tutti gli elementi figlio all'interno del layout.
Per altre informazioni, vedere Creare un gestore di layout.
Utilizzare il tipo di layout personalizzato aggiungendolo a un Pagee aggiungendo elementi figlio al layout. Per altre informazioni, vedere Utilizzare il tipo di layout.
Per illustrare questo processo viene usato un elemento sensibile HorizontalWrapLayout
all'orientamento. HorizontalWrapLayout
è simile a in HorizontalStackLayout quanto dispone i relativi elementi figlio orizzontalmente nella pagina. Tuttavia, esegue il wrapping della visualizzazione degli elementi figlio in una nuova riga quando incontra il bordo destro del contenitore
Nota
L'esempio definisce layout personalizzati aggiuntivi che possono essere usati per comprendere come produrre un layout personalizzato.
Sottoclasse di un layout
Per creare un tipo di layout personalizzato, è necessario prima sottoclasse un tipo di layout esistente o la Layout classe . Eseguire quindi l'override CreateLayoutManager() nel tipo di layout e restituire una nuova istanza del gestore layout per il tipo di layout:
using Microsoft.Maui.Layouts;
public class HorizontalWrapLayout : HorizontalStackLayout
{
protected override ILayoutManager CreateLayoutManager()
{
return new HorizontalWrapLayoutManager(this);
}
}
HorizontalWrapLayout
deriva da HorizontalStackLayout per usare la relativa funzionalità di layout. I layout MAUI .NET delegano il layout e la misurazione multipiattaforma a una classe di gestione layout. Di conseguenza, l'override CreateLayoutManager() restituisce una nuova istanza della HorizontalWrapLayoutManager
classe , ovvero il gestore layout descritto nella sezione successiva.
Creare un gestore layout
Una classe di gestione layout viene usata per eseguire il layout e la misurazione multipiattaforma per il tipo di layout personalizzato. Deve derivare da un gestore di layout esistente oppure deve implementare direttamente l'interfaccia ILayoutManager . HorizontalWrapLayoutManager
deriva da HorizontalStackLayoutManager in modo che possa usare le funzionalità sottostanti e accedere ai membri nella gerarchia di ereditarietà:
using Microsoft.Maui.Layouts;
using HorizontalStackLayoutManager = Microsoft.Maui.Layouts.HorizontalStackLayoutManager;
public class HorizontalWrapLayoutManager : HorizontalStackLayoutManager
{
HorizontalWrapLayout _layout;
public HorizontalWrapLayoutManager(HorizontalWrapLayout horizontalWrapLayout) : base(horizontalWrapLayout)
{
_layout = horizontalWrapLayout;
}
public override Size Measure(double widthConstraint, double heightConstraint)
{
}
public override Size ArrangeChildren(Rect bounds)
{
}
}
Il HorizontalWrapLayoutManager
costruttore archivia un'istanza del HorizontalWrapLayout
tipo in un campo, in modo che sia accessibile in tutta la gestione layout. Gestione layout esegue anche l'override dei Measure metodi e ArrangeChildren dalla HorizontalStackLayoutManager classe . Questi metodi definiscono la logica per implementare il layout personalizzato.
Misurare le dimensioni del layout
Lo scopo dell'implementazione ILayoutManager.Measure è calcolare le dimensioni totali del layout. Questa operazione deve essere eseguita chiamando IView.Measure su ogni elemento figlio nel layout. Questi dati devono quindi essere usati per calcolare e restituire le dimensioni totali del layout in base ai relativi vincoli.
L'esempio seguente illustra l'implementazione Measure per la HorizontalWrapLayoutManager
classe :
public override Size Measure(double widthConstraint, double heightConstraint)
{
var padding = _layout.Padding;
widthConstraint -= padding.HorizontalThickness;
double currentRowWidth = 0;
double currentRowHeight = 0;
double totalWidth = 0;
double totalHeight = 0;
for (int n = 0; n < _layout.Count; n++)
{
var child = _layout[n];
if (child.Visibility == Visibility.Collapsed)
{
continue;
}
var measure = child.Measure(double.PositiveInfinity, heightConstraint);
// Will adding this IView put us past the edge?
if (currentRowWidth + measure.Width > widthConstraint)
{
// Keep track of the width so far
totalWidth = Math.Max(totalWidth, currentRowWidth);
totalHeight += currentRowHeight;
// Account for spacing
totalHeight += _layout.Spacing;
// Start over at 0
currentRowWidth = 0;
currentRowHeight = measure.Height;
}
currentRowWidth += measure.Width;
currentRowHeight = Math.Max(currentRowHeight, measure.Height);
if (n < _layout.Count - 1)
{
currentRowWidth += _layout.Spacing;
}
}
// Account for the last row
totalWidth = Math.Max(totalWidth, currentRowWidth);
totalHeight += currentRowHeight;
// Account for padding
totalWidth += padding.HorizontalThickness;
totalHeight += padding.VerticalThickness;
// Ensure that the total size of the layout fits within its constraints
var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, totalWidth, Stack.MinimumWidth, Stack.MaximumWidth);
var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, totalHeight, Stack.MinimumHeight, Stack.MaximumHeight);
return new Size(finalWidth, finalHeight);
}
Il Measure(Double, Double) metodo enumera tutti gli elementi figlio visibili nel layout, richiamando il IView.Measure metodo su ogni elemento figlio. Restituisce quindi le dimensioni totali del layout, tenendo conto dei vincoli e dei valori delle Padding proprietà e Spacing . Il ResolveConstraints metodo viene chiamato per garantire che le dimensioni totali del layout si adattino ai relativi vincoli.
Importante
Durante l'enumerazione degli elementi figlio nell'implementazione ILayoutManager.Measure , ignorare qualsiasi elemento figlio la cui Visibility proprietà è impostata su Collapsed. In questo modo, il layout personalizzato non lascerà spazio per gli elementi figlio invisibili.
Disporre gli elementi figlio nel layout
Lo scopo dell'implementazione ArrangeChildren è ridimensionare e posizionare tutti gli elementi figlio all'interno del layout. Per determinare dove ogni figlio deve essere posizionato entro i limiti del layout, deve chiamare Arrange su ogni elemento figlio con i limiti appropriati. Deve quindi restituire un valore che rappresenta le dimensioni effettive del layout.
Avviso
Se non si richiama il ArrangeChildren metodo in ogni elemento figlio nel layout, l'elemento figlio non riceverà mai una dimensione o una posizione corretta e pertanto il figlio non diventerà visibile nella pagina.
L'esempio seguente illustra l'implementazione ArrangeChildren per la HorizontalWrapLayoutManager
classe :
public override Size ArrangeChildren(Rect bounds)
{
var padding = Stack.Padding;
double top = padding.Top + bounds.Top;
double left = padding.Left + bounds.Left;
double currentRowTop = top;
double currentX = left;
double currentRowHeight = 0;
double maxStackWidth = currentX;
for (int n = 0; n < _layout.Count; n++)
{
var child = _layout[n];
if (child.Visibility == Visibility.Collapsed)
{
continue;
}
if (currentX + child.DesiredSize.Width > bounds.Right)
{
// Keep track of our maximum width so far
maxStackWidth = Math.Max(maxStackWidth, currentX);
// Move down to the next row
currentX = left;
currentRowTop += currentRowHeight + _layout.Spacing;
currentRowHeight = 0;
}
var destination = new Rect(currentX, currentRowTop, child.DesiredSize.Width, child.DesiredSize.Height);
child.Arrange(destination);
currentX += destination.Width + _layout.Spacing;
currentRowHeight = Math.Max(currentRowHeight, destination.Height);
}
var actual = new Size(maxStackWidth, currentRowTop + currentRowHeight);
// Adjust the size if the layout is set to fill its container
return actual.AdjustForFill(bounds, Stack);
}
Il ArrangeChildren
metodo enumera tutti gli elementi figlio visibili nel layout per ridimensionarli e posizionarli all'interno del layout. Questa operazione viene eseguita richiamando Arrange su ogni elemento figlio con limiti appropriati, che prendono in considerazione e Padding Spacing del layout sottostante. Restituisce quindi le dimensioni effettive del layout. Il AdjustForFill metodo viene chiamato per assicurarsi che le dimensioni tengano conto se il layout ha HorizontalLayoutAlignment le relative proprietà e VerticalLayoutAlignment impostate su LayoutOptions.Fill.
Importante
Durante l'enumerazione degli elementi figlio nell'implementazione ArrangeChildren , ignorare qualsiasi elemento figlio la cui Visibility proprietà è impostata su Collapsed. In questo modo, il layout personalizzato non lascerà spazio per gli elementi figlio invisibili.
Utilizzare il tipo di layout
La HorizontalWrapLayout
classe può essere utilizzata inserendola in un Page tipo derivato:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:layouts="clr-namespace:CustomLayoutDemos.Layouts"
x:Class="CustomLayoutDemos.Views.HorizontalWrapLayoutPage"
Title="Horizontal wrap layout">
<ScrollView Margin="20">
<layouts:HorizontalWrapLayout Spacing="20">
<Image Source="img_0074.jpg"
WidthRequest="150" />
<Image Source="img_0078.jpg"
WidthRequest="150" />
<Image Source="img_0308.jpg"
WidthRequest="150" />
<Image Source="img_0437.jpg"
WidthRequest="150" />
<Image Source="img_0475.jpg"
WidthRequest="150" />
<Image Source="img_0613.jpg"
WidthRequest="150" />
<!-- More images go here -->
</layouts:HorizontalWrapLayout>
</ScrollView>
</ContentPage>
I controlli possono essere aggiunti a in HorizontalWrapLayout
base alle esigenze. In questo esempio, quando viene visualizzata la pagina contenente , HorizontalWrapLayout
vengono visualizzati i Image controlli:
Il numero di colonne in ogni riga dipende dalle dimensioni dell'immagine, dalla larghezza della pagina e dal numero di pixel per unità indipendente dal dispositivo:
Nota
Lo scorrimento è supportato eseguendo il wrapping di HorizontalWrapLayout
in un oggetto ScrollView.
Modificare il comportamento di un layout esistente
In alcuni scenari può essere necessario modificare il comportamento di un tipo di layout esistente senza dover creare un tipo di layout personalizzato. Per questi scenari è possibile creare un tipo che implementa ILayoutManagerFactory e usarlo per sostituire il gestore layout predefinito di .NET MAUI per il layout esistente con la propria ILayoutManager implementazione. In questo modo è possibile definire un nuovo gestore layout per un layout esistente, ad esempio fornire un gestore layout personalizzato per Grid. Questo può essere utile per gli scenari in cui vuoi aggiungere un nuovo comportamento a un layout, ma non vuoi aggiornare il tipo di un layout ampiamente usato nella tua app.
Il processo di modifica del comportamento di un layout esistente, con una factory di gestione layout, consiste nel:
- Creare un gestore di layout che deriva da uno dei tipi di gestione layout di .NET MAUI. Per altre informazioni, vedere Creare un gestore layout personalizzato.
- Creare un tipo che implementa ILayoutManagerFactory. Per altre informazioni, vedere Creare una factory di gestione layout.
- Registrare la factory di gestione layout con il provider di servizi dell'app. Per altre informazioni, vedere Registrare la factory di gestione layout.
Creare un gestore layout personalizzato
Un gestore layout viene usato per eseguire il layout e la misurazione multipiattaforma per un layout. Per modificare il comportamento di un layout esistente, è necessario creare un gestore layout personalizzato che deriva dal gestore layout per il layout:
using Microsoft.Maui.Layouts;
public class CustomGridLayoutManager : GridLayoutManager
{
public CustomGridLayoutManager(IGridLayout layout) : base(layout)
{
}
public override Size Measure(double widthConstraint, double heightConstraint)
{
EnsureRows();
return base.Measure(widthConstraint, heightConstraint);
}
void EnsureRows()
{
if (Grid is not Grid grid)
{
return;
}
// Find the maximum row value from the child views
int maxRow = 0;
foreach (var child in grid)
{
maxRow = Math.Max(grid.GetRow(child), maxRow);
}
// Add more rows if we need them
for (int n = grid.RowDefinitions.Count; n <= maxRow; n++)
{
grid.RowDefinitions.Add(new RowDefinition(GridLength.Star));
}
}
}
In questo esempio deriva CustomGridLayoutManager
dalla classe MAUI di .NET ed esegue l'override del GridLayoutManager relativo Measure metodo. Questo gestore layout personalizzato garantisce che in fase di esecuzione RowDefinitions per include Grid righe sufficienti per tenere conto di ogni Grid.Row
proprietà associata impostata in una visualizzazione figlio. Senza questa modifica, l'oggetto RowDefinitions per Grid deve essere specificato in fase di progettazione.
Importante
Quando si modifica il comportamento di un gestore di layout esistente, non dimenticare di chiamare il base.Measure
metodo dall'implementazione Measure .
Creare una factory di gestione layout
Il gestore layout personalizzato deve essere creato in una factory di gestione layout. Questa operazione viene ottenuta creando un tipo che implementa l'interfaccia ILayoutManagerFactory :
using Microsoft.Maui.Layouts;
public class CustomLayoutManagerFactory : ILayoutManagerFactory
{
public ILayoutManager CreateLayoutManager(Layout layout)
{
if (layout is Grid)
{
return new CustomGridLayoutManager(layout as IGridLayout);
}
return null;
}
}
In questo esempio viene restituita un'istanza CustomGridLayoutManager
se il layout è .Grid
Registrare la factory di gestione layout
La factory di gestione layout deve essere registrata con il provider di servizi dell'app nella classe MauiProgram
:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// Setup a custom layout manager so the default manager for the Grid can be replaced.
builder.Services.Add(new ServiceDescriptor(typeof(ILayoutManagerFactory), new CustomLayoutManagerFactory()));
return builder.Build();
}
}
Quindi, quando l'app esegue il rendering di un Grid oggetto, userà il gestore layout personalizzato per assicurarsi che in fase di esecuzione RowDefinitions per include Grid righe sufficienti per tenere conto di ogni Grid.Row
proprietà associata impostata nelle visualizzazioni figlio.
L'esempio seguente mostra un oggetto Grid che imposta la Grid.Row
proprietà associata nelle visualizzazioni figlio, ma non imposta la RowDefinitions proprietà :
<Grid>
<Label Text="This Grid demonstrates replacing the LayoutManager for an existing layout type." />
<Label Grid.Row="1"
Text="In this case, it's a LayoutManager for Grid which automatically adds enough rows to accommodate the rows specified in the child views' attached properties." />
<Label Grid.Row="2"
Text="Notice that the Grid doesn't explicitly specify a RowDefinitions collection." />
<Label Grid.Row="3"
Text="In MauiProgram.cs, an instance of an ILayoutManagerFactory has been added that replaces the default GridLayoutManager. The custom manager will automatically add the necessary RowDefinitions at runtime." />
<Label Grid.Row="5"
Text="We can even skip some rows, and it will add the intervening ones for us (notice the gap between the previous label and this one)." />
</Grid>
La factory di gestione layout usa il gestore layout personalizzato per assicurarsi che Grid in questo esempio venga visualizzato correttamente, nonostante la RowDefinitions proprietà non sia impostata: