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 . Page
I tipi derivati , Layout
e 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
:
- Durante il ciclo di layout, ogni elemento padre è responsabile della chiamata del
Measure
metodo sui relativi elementi figlio. - 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:
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 StackLayout
oggetto , 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:
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:
Creare una classe che derivi dalla classe
Layout<View>
. Per altre informazioni, vedere Creare un wrapLayout.[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.
Eseguire l'override del
OnMeasure
metodo per richiamare ilMeasure
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.Eseguire l'override del
LayoutChildren
metodo per richiamare ilLayout
metodo su tutti gli elementi figlio del layout. Se non si richiama ilLayout
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 cuiIsVisible
proprietà sia impostata sufalse
. In questo modo, il layout personalizzato non lascerà spazio per gli elementi figlio invisibili.[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.[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 Page
oggetto 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 nonWrapLayout
è 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:
Gli screenshot seguenti mostrano l'oggetto WrapLayout
dopo che è stato ruotato sull'orientamento orizzontale:
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.