Layout collegati
Un contenitore, ad esempio Panel, che delega la propria logica di layout a un altro oggetto si basa sull'oggetto layout collegato per fornire il comportamento di layout per i relativi elementi figlio. Un modello di layout collegato offre a un'applicazione la flessibilità necessaria per modificare il layout degli elementi in fase di esecuzione o condividere più facilmente aspetti del layout tra parti diverse dell'interfaccia utente, ad esempio gli elementi delle righe di una tabella che risultano allineati all'interno di una colonna.
Questo argomento illustra gli aspetti correlati alla creazione di un layout collegato (con e senza virtualizzazione), i concetti e le classi che è necessario comprendere e i compromessi da considerare per effettuare la scelta appropriata.
Ottenere WinUI |
---|
Questo controllo è incluso come parte di WinUI, un pacchetto NuGet che contiene i nuovi controlli e le funzionalità dell'interfaccia utente per le app di Windows. Per maggiori informazioni, incluse le istruzioni per l'installazione, vedere la panoramica di WinUI. |
API importanti:
Concetti chiave
Per eseguire il processo di layout è necessario rispondere a due domande per ogni elemento:
Che dimensioni avrà questo elemento?
Quale sarà la posizione di questo elemento?
Il sistema di layout di XAML, che risponde a queste domande, viene brevemente trattato nell'ambito della discussione relativa ai pannelli personalizzati.
Contenitori e contesto
A livello concettuale, il contenitore Panel di XAML svolge due ruoli importanti nel framework:
- Può contenere elementi figlio e introduce la diramazione nell'albero degli elementi.
- Applica una specifica strategia di layout agli elementi figlio.
Per questo motivo, un contenitore Panel in XAML è spesso sinonimo di layout, ma dal punto di vista tecnico non si limita a definire il layout.
Anche ItemsRepeater si comporta come Panel, ma, a differenza di Panel, non espone una proprietà Children che consente di aggiungere o rimuovere elementi figlio UIElement a livello di codice. Al contrario, la durata dei relativi elementi figlio viene gestita automaticamente dal framework in modo da corrispondere a una raccolta di elementi di dati. Anche se non è derivato da Panel, questo contenitore si comporta e viene trattato dal framework come un contenitore Panel.
Nota
LayoutPanel è un contenitore, derivato da Panel, che delega la propria logica all'oggetto Layout collegato. LayoutPanel è in Anteprima ed è attualmente disponibile solo nelle versioni preliminari del pacchetto WinUI.
Contenitori
A livello concettuale, Panel è un contenitore di elementi che può anche eseguire il rendering dei pixel per un oggetto Background. I contenitori Panel consentono di incapsulare la comune logica di layout in un pacchetto di facile utilizzo.
Il concetto di layout collegato rende più chiara la distinzione tra i due ruoli di contenitore e layout. Se il contenitore delega la propria logica di layout a un altro oggetto, quest'ultimo viene definito layout collegato, come illustrato nel frammento di codice seguente. I contenitori che ereditano da FrameworkElement, ad esempio LayoutPanel, espongono automaticamente le proprietà comuni che forniscono input al processo di layout di XAML (ad esempio, Height e Width).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Durante il processo di layout il contenitore si basa sull'oggetto UniformGridLayout collegato per misurare e disporre gli elementi figlio.
Stato per contenitore
Con un layout collegato, una singola istanza dell'oggetto layout può essere associata a molti contenitori, come nel frammento di codice seguente. Pertanto, non deve dipendere dal contenitore host né farvi direttamente riferimento. Ad esempio:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
In questo caso, ExampleLayout deve considerare con attenzione lo stato usato nel calcolo del proprio layout e la posizione in cui tale stato viene archiviato per evitare l'impatto reciproco del layout degli elementi in un pannello con quello dell'altro pannello. Si tratta di un elemento analogo a un contenitore Panel personalizzato la cui logica di MeasureOverride e ArrangeOverride dipende dai valori delle relative proprietà statiche.
LayoutContext
Lo scopo di LayoutContext è quello di gestire questi problemi. Offre al layout collegato la possibilità di interagire con il contenitore host, ad esempio recuperando gli elementi figlio, senza introdurre una dipendenza diretta tra i due. Il contesto consente inoltre al layout di archiviare qualsiasi stato richiesto che potrebbe essere correlato agli elementi figlio del contenitore.
I layout semplici e senza virtualizzazione spesso non necessitano la gestione di uno stato e pertanto questo non costituisce un problema. Un layout più complesso, ad esempio Grid, può tuttavia scegliere di gestire lo stato tra le chiamate a Measure e Arrange per evitare che un valore venga ricalcolato.
I layout con virtualizzazione hanno spesso la necessità di gestire uno stato tra le chiamate a Measure e Arrange e tra i passaggi di layout iterativi.
Inizializzazione e annullamento dell'inizializzazione dello stato per contenitore
Quando un layout è collegato a un contenitore, viene chiamato il relativo metodo InitializeForContextCore, che offre la possibilità di inizializzare un oggetto per archiviare lo stato.
Analogamente, quando il layout viene rimosso da un contenitore, viene chiamato il metodo UninitializeForContextCore. In questo modo, il layout ha la possibilità di pulire qualsiasi stato associato a tale contenitore.
L'oggetto dello stato del layout può essere archiviato e recuperato dal contenitore con la proprietà LayoutState nel contesto.
Virtualizzazione dell'interfaccia utente
Virtualizzare l'interfaccia utente significa ritardare la creazione di un oggetto dell'interfaccia utente fino al momento in cui questo risulta necessario. Si tratta di un'ottimizzazione delle prestazioni. Per gli scenari senza scorrimento, la determinazione del momento in cui l'oggetto risulta necessario può essere basata su un numero qualsiasi di elementi specifici dell'app. In questi casi, è opportuno prendere in considerazione l'uso di x:Load per le app. Non è necessaria alcuna particolare attività di gestione nel layout.
Negli scenari basati sullo scorrimento, ad esempio un elenco, la determinazione del momento in cui l'oggetto risulta necessario è spesso basata sul "momento in cui sarà visibile a un utente". Questo dipende principalmente dalla posizione assegnata durante il processo di layout e richiede speciali considerazioni. Questo scenario ha un'importanza fondamentale per questo documento.
Nota
Anche se non sono trattate in questo documento, le stesse funzionalità che consentono la virtualizzazione dell'interfaccia utente negli scenari con scorrimento possono essere applicate negli scenari senza scorrimento. È il caso di un controllo ToolBar basato sui dati che gestisce la durata dei comandi presentati e risponde alle modifiche nello spazio disponibile mediante il riciclo o lo spostamento degli elementi tra un'area visibile e un menu di overflow.
Introduzione
Prima di tutto, decidi se il layout da creare deve supportare la virtualizzazione dell'interfaccia utente.
Alcuni aspetti da tenere presenti…
- I layout senza virtualizzazione sono più facili da creare. Se prevedi che il numero di elementi sia sempre ridotto, è consigliabile creare un layout senza virtualizzazione.
- La piattaforma offre un set di layout collegati che funzionano con ItemsRepeater e LayoutPanel per soddisfare le esigenze più comuni. Acquisisci familiarità con questi layout prima di decidere se devi definire un layout personalizzato.
- I layout con virtualizzazione presentano sempre un costo, una complessità e un sovraccarico aggiuntivi in termini di CPU e memoria rispetto a quelli senza virtualizzazione. Come regola empirica generale, se gli elementi figlio che dovranno essere gestiti dal layout presumibilmente si adatteranno a un'area di dimensioni di tre volte superiore a quelle del viewport, l'uso di un layout con virtualizzazione potrebbe non costituire un grande vantaggio. Il criterio delle dimensioni di tre volte superiori è illustrato in dettaglio più avanti in questo documento, ma è dovuto alla natura asincrona dello scorrimento in Windows e al suo impatto sulla virtualizzazione.
Suggerimento
Come punto di riferimento, le impostazioni predefinite per ListView (e ItemsRepeater) prevedono che il riciclo non inizi fino a quando il numero di elementi è sufficiente a riempire un'area di dimensioni di tre volte superiori a quelle del viewport corrente.
Scegli il tipo di base
Il tipo di Layout di base presenta due tipi derivati che vengono usati come punto di partenza per la creazione di un layout collegato:
Layout senza virtualizzazione
L'approccio per la creazione di un layout senza virtualizzazione dovrebbe risultare familiare a chiunque abbia creato un pannello personalizzato. Sono validi gli stessi concetti. La differenza principale consiste nel fatto che viene usato un oggetto NonVirtualizingLayoutContext per accedere alla raccolta Children e il layout può scegliere di archiviare lo stato.
- Deriva il layout dal tipo di base NonVirtualizingLayout, anziché da Panel.
- (Facoltativo) Definisci le proprietà di dipendenza che, se modificate, renderanno il layout non valido.
- (Nuovo/Facoltativo) Inizializza qualsiasi oggetto di stato richiesto dal layout come parte del metodo InitializeForContextCore. Accantonalo con il contenitore host usando la proprietà LayoutState fornita con il contesto.
- Esegui l'override di MeasureOverride e chiama il metodo Measure su tutti gli elementi figlio.
- Esegui l'override di ArrangeOverride e chiama il metodo Arrange su tutti gli elementi figlio.
- (Nuovo/Facoltativo) Pulisci lo stato salvato come parte del metodo UninitializeForContextCore.
Esempio: layout pila semplice (elementi di dimensioni variabili)
Di seguito è riportato un layout pila semplice senza virtualizzazione di elementi di dimensioni variabili. Non include proprietà per la modifica del comportamento del layout. L'implementazione seguente illustra il modo in cui il layout si basa sull'oggetto context fornito dal contenitore per:
- Ottenere il conteggio degli elementi figlio.
- Accedere a ogni elemento figlio in base all'indice.
public class MyStackLayout : NonVirtualizingLayout
{
protected override Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize)
{
double extentHeight = 0.0;
foreach (var element in context.Children)
{
element.Measure(availableSize);
extentHeight += element.DesiredSize.Height;
}
return new Size(availableSize.Width, extentHeight);
}
protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize)
{
double offset = 0.0;
foreach (var element in context.Children)
{
element.Arrange(
new Rect(0, offset, finalSize.Width, element.DesiredSize.Height));
offset += element.DesiredSize.Height;
}
return finalSize;
}
}
<LayoutPanel MaxWidth="196">
<LayoutPanel.Layout>
<local:MyStackLayout/>
</LayoutPanel.Layout>
<Button HorizontalAlignment="Stretch">1</Button>
<Button HorizontalAlignment="Right">2</Button>
<Button HorizontalAlignment="Center">3</Button>
<Button>4</Button>
</LayoutPanel>
Layout con virtualizzazione
I passaggi di alto livello per un layout con virtualizzazione sono identici a quelli di un layout senza virtualizzazione. La complessità consiste soprattutto nel determinare quali elementi rientrano nel viewport e devono essere realizzati.
- Deriva il layout dal tipo di base VirtualizingLayout.
- (Facoltativo) Definisci le proprietà di dipendenza che, se modificate, renderanno il layout non valido.
- Inizializza qualsiasi oggetto di stato che verrà richiesto dal layout come parte del metodo InitializeForContextCore. Accantonalo con il contenitore host usando la proprietà LayoutState fornita con il contesto.
- Esegui l'override di MeasureOverride e chiama il metodo Measure per ogni elemento figlio da realizzare.
- Il metodo GetOrCreateElementAt viene usato per recuperare un oggetto UIElement preparato dal framework, ad esempio, i data binding applicati.
- Esegui l'override di ArrangeOverride e chiama il metodo Arrange per ogni elemento figlio realizzato.
- (Facoltativo) Pulisci qualsiasi stato salvato come parte del metodo UninitializeForContextCore.
Suggerimento
Il valore restituito da MeasureOverride viene usato come dimensioni del contenuto virtualizzato.
Esistono due approcci generali da considerare durante la creazione di un layout con virtualizzazione. La scelta di uno dei due dipende in larga misura da "come vengono determinate le dimensioni di un elemento". Se è sufficiente individuare l'indice di un elemento nel set di dati o i dati stessi ne determinano le dimensioni finali, l'elemento verrà considerato dipendente dai dati. I layout di questo tipo sono più semplici da creare. Se, tuttavia, l'unico modo per determinare le dimensioni di un elemento consiste nel creare e misurare l'interfaccia utente, l'elemento verrà considerato dipendente dal contenuto. In questo caso, i layout sono più complessi.
Processo di layout
Qualunque sia il tipo di layout che intendi creare, dipendente dai dati o dal contenuto, è importante comprendere il processo di layout e l'impatto dello scorrimento asincrono di Windows.
Di seguito sono elencati in sintesi i passaggi che esegue il framework dall'avvio fino alla visualizzazione dell'interfaccia utente sullo schermo:
Analizza il markup.
Genera un albero di elementi.
Esegue un passaggio di layout.
Esegue un passaggio di rendering.
Con la virtualizzazione dell'interfaccia utente, la creazione di elementi che normalmente verrebbero eseguiti nel passaggio 2 viene ritardata o terminata in anticipo una volta stabilito che è stato creato contenuto sufficiente per riempire il viewport. Un contenitore con virtualizzazione, ad esempio ItemsRepeater, affida al relativo layout collegato il compito di guidare questo processo. Fornisce al layout collegato un oggetto VirtualizingLayoutContext che espone le informazioni aggiuntive necessarie per il layout di virtualizzazione.
RealizationRect (ad esempio il viewport)
Lo scorrimento su Windows avviene in modo asincrono rispetto al thread dell'interfaccia utente. Non è controllato dal layout del framework. Al contrario, l'interazione e lo spostamento avvengono nel programma di composizione del sistema. Il vantaggio di questo approccio consiste nel fatto che la panoramica può essere sempre eseguita a 60 fps. Il problema, tuttavia, è che il "viewport", come osservato dal layout, potrebbe essere leggermente meno aggiornato rispetto a quello effettivamente visibile sullo schermo. Se un utente scorre rapidamente, può superare la velocità del thread dell'interfaccia utente nel generare nuovo contenuto con conseguenti problemi di visualizzazione. Per questo motivo, è spesso necessario che un layout con virtualizzazione generi un buffer aggiuntivo di elementi preparati, sufficiente a riempire un'area di maggiori dimensioni rispetto al viewport. In questo modo, anche in caso di un carico maggiore durante lo scorrimento, l'utente potrà visualizzare il contenuto.
Poiché la creazione di elementi comporta un certo costo, i contenitori con virtualizzazione (come ItemsRepeater) forniscono inizialmente al layout collegato un oggetto RealizationRect che corrisponde al viewport. Durante i tempi di inattività, il contenitore può aumentare il buffer del contenuto preparato effettuando chiamate ripetute al layout con un rettangolo di realizzazione sempre più grande. Questo comportamento rappresenta un'ottimizzazione delle prestazioni con l'obiettivo di raggiungere il giusto equilibrio tra un tempo di avvio rapido e una buona esperienza di panoramica. Le dimensioni massime del buffer che verranno generate da ItemsRepeater sono controllate dalle proprietà VerticalCacheLength e HorizontalCacheLength.
Riutilizzo degli elementi (riciclo)
Il layout deve ridimensionare e posizionare gli elementi in modo da riempire l'oggetto RealizationRect ogni volta che viene eseguito. Per impostazione predefinita, l'oggetto VirtualizingLayout ricicla tutti gli elementi inutilizzati alla fine di ogni passaggio di layout.
L'oggetto VirtualizingLayoutContext che viene passato al layout come parte dei metodi MeasureOverride e ArrangeOverride fornisce le informazioni aggiuntive necessarie per il layout con virtualizzazione. Di seguito sono riportate alcune delle operazioni più comuni che consente di effettuare:
- Eseguire una query relativa al numero di elementi nei dati (ItemCount).
- Recuperare un elemento specifico tramite il metodo GetItemAt.
- Recuperare un oggetto RealizationRect che rappresenta il viewport e il buffer che deve essere riempito dal layout con gli elementi realizzati.
- Richiedere l'oggetto UIElement per un elemento specifico con il metodo GetOrCreateElement.
Se un elemento viene richiesto per un determinato indice, verrà contrassegnato come "in uso" per il passaggio del layout. Se l'elemento non esiste già, verrà realizzato e preparato automaticamente per l'uso, ad esempio ampliando l'albero dell'interfaccia utente definita in un DataTemplate, elaborando un data binding e così via. In caso contrario, verrà recuperato da un pool di istanze esistenti.
Alla fine di ogni passaggio di misurazione, qualsiasi elemento realizzato e non contrassegnato come "in uso" viene considerato automaticamente disponibile per il riutilizzo a meno che non sia stata usata l'opzione SuppressAutoRecycle quando l'elemento è stato recuperato tramite il metodo GetOrCreateElementAt. Il framework sposta automaticamente l'elemento in un pool di riciclo e lo rende disponibile. È possibile che in seguito ne venga eseguito il pull per l'uso da parte di un contenitore diverso. Il framework tenta di evitare questa eventualità, quando possibile, poiché la riorganizzazione di un elemento padre comporta dei costi.
Se un layout con virtualizzazione conosce all'inizio di ogni misurazione gli elementi che non rientrano più nel rettangolo di realizzazione, può ottimizzarne il riutilizzo. Non deve necessariamente basarsi sul comportamento predefinito del framework. Il layout può spostare preventivamente gli elementi nel pool di riciclo usando il metodo RecycleElement. Se questo metodo viene chiamato prima di richiedere nuovi elementi, gli elementi esistenti saranno disponibili quando il layout invierà un richiesta GetOrCreateElementAt per un indice non ancora associato a un elemento.
L'oggetto VirtualizingLayoutContext offre due proprietà aggiuntive progettate per gli autori che creano layout dipendenti dal contenuto. Queste proprietà verranno illustrate in dettaglio più avanti in questo documento.
- La proprietà RecommendedAnchorIndex che fornisce un input facoltativo al layout.
- La proprietà LayoutOrigin che costituisce un output facoltativo del layout.
Layout con virtualizzazione dipendenti dai dati
Un layout con virtualizzazione è più semplice se conosci le dimensioni di ogni elemento senza dover misurare il contenuto da mostrare. In questo documento faremo semplicemente riferimento a questa categoria di layout con virtualizzazione come layout di dati poiché in genere riguardano l'ispezione dei dati. In base ai dati, un'app può scegliere una rappresentazione visiva di dimensioni note, presumibilmente perché fa parte dei dati o è stata determinata in precedenza dalla progettazione.
L'approccio generale per il layout consiste nelle operazioni seguenti:
- Calcolare le dimensioni e la posizione di ogni elemento.
- Come parte del metodo MeasureOverride:
- Usare l'oggetto RealizationRect per determinare quali elementi devono essere visualizzati nel viewport.
- Recuperare l'oggetto UIElement che deve rappresentare l'elemento con il metodo GetOrCreateElementAt.
- Misurare l'oggetto UIElement con le dimensioni precalcolate.
- Come parte del metodo ArrangeOverride, disporre ogni UIElement realizzato con la posizione precalcolata.
Nota
L'approccio basato su layout di dati è spesso incompatibile con la virtualizzazione dei dati. In particolare, nel caso in cui gli unici dati caricati in memoria siano i dati necessari per riempire l'area visibile all'utente. La virtualizzazione dei dati non si riferisce al caricamento lento o incrementale dei dati quando un utente scorre verso il basso nell'area in cui i dati rimangono residenti. Si riferisce piuttosto al momento in cui gli elementi vengono rilasciati dalla memoria man mano che scorrono fuori dalla visualizzazione. Un layout di dati che controlla ogni elemento di dati impedirebbe alla virtualizzazione dei dati di funzionare come previsto. Un'eccezione è rappresentata da un layout come UniformGridLayout che presuppone che tutti gli elementi abbiano le stesse dimensioni.
Suggerimento
Se stai creando un controllo personalizzato per una libreria di controlli che verrà usata da altri utenti in un'ampia gamma di situazioni, un layout di dati potrebbe non costituire una possibile soluzione.
Esempio: layout del feed attività Xbox
L'interfaccia utente per il feed attività Xbox usa un modello ripetuto in cui ogni riga ha un riquadro ampio, seguito da due riquadri stretti, che viene invertito nella riga successiva. In questo layout, le dimensioni di ogni elemento sono una funzione della posizione dell'elemento nel set di dati e delle dimensioni note per i riquadri (ampi e stretti).
Il codice seguente illustra il modo in cui un'interfaccia utente di virtualizzazione personalizzata per il feed attività potrebbe essere quella di illustrare l'approccio generale che è possibile adottare per un layout di dati.
Suggerimento
Se è installata l'app WinUI 3 Gallery, fare clic qui per aprire l'app e visualizzare il controllo ItemsRepeater in azione. Ottenere l'app da Microsoft Store o visualizzare il codice sorgente in GitHub.
Implementazione
/// <summary>
/// This is a custom layout that displays elements in two different sizes
/// wide (w) and narrow (n). There are two types of rows
/// odd rows - narrow narrow wide
/// even rows - wide narrow narrow
/// This pattern repeats.
/// </summary>
public class ActivityFeedLayout : VirtualizingLayout // STEP #1 Inherit from base attached layout
{
// STEP #2 - Parameterize the layout
#region Layout parameters
// We'll cache copies of the dependency properties to avoid calling GetValue during layout since that
// can be quite expensive due to the number of times we'd end up calling these.
private double _rowSpacing;
private double _colSpacing;
private Size _minItemSize = Size.Empty;
/// <summary>
/// Gets or sets the size of the whitespace gutter to include between rows
/// </summary>
public double RowSpacing
{
get { return _rowSpacing; }
set { SetValue(RowSpacingProperty, value); }
}
/// <summary>
/// Gets or sets the size of the whitespace gutter to include between items on the same row
/// </summary>
public double ColumnSpacing
{
get { return _colSpacing; }
set { SetValue(ColumnSpacingProperty, value); }
}
public Size MinItemSize
{
get { return _minItemSize; }
set { SetValue(MinItemSizeProperty, value); }
}
public static readonly DependencyProperty RowSpacingProperty =
DependencyProperty.Register(
nameof(RowSpacing),
typeof(double),
typeof(ActivityFeedLayout),
new PropertyMetadata(0, OnPropertyChanged));
public static readonly DependencyProperty ColumnSpacingProperty =
DependencyProperty.Register(
nameof(ColumnSpacing),
typeof(double),
typeof(ActivityFeedLayout),
new PropertyMetadata(0, OnPropertyChanged));
public static readonly DependencyProperty MinItemSizeProperty =
DependencyProperty.Register(
nameof(MinItemSize),
typeof(Size),
typeof(ActivityFeedLayout),
new PropertyMetadata(Size.Empty, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var layout = obj as ActivityFeedLayout;
if (args.Property == RowSpacingProperty)
{
layout._rowSpacing = (double)args.NewValue;
}
else if (args.Property == ColumnSpacingProperty)
{
layout._colSpacing = (double)args.NewValue;
}
else if (args.Property == MinItemSizeProperty)
{
layout._minItemSize = (Size)args.NewValue;
}
else
{
throw new InvalidOperationException("Don't know what you are talking about!");
}
layout.InvalidateMeasure();
}
#endregion
#region Setup / teardown // STEP #3: Initialize state
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
base.InitializeForContextCore(context);
var state = context.LayoutState as ActivityFeedLayoutState;
if (state == null)
{
// Store any state we might need since (in theory) the layout could be in use by multiple
// elements simultaneously
// In reality for the Xbox Activity Feed there's probably only a single instance.
context.LayoutState = new ActivityFeedLayoutState();
}
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
base.UninitializeForContextCore(context);
// clear any state
context.LayoutState = null;
}
#endregion
#region Layout // STEP #4,5 - Measure and Arrange
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (this.MinItemSize == Size.Empty)
{
var firstElement = context.GetOrCreateElementAt(0);
firstElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// setting the member value directly to skip invalidating layout
this._minItemSize = firstElement.DesiredSize;
}
// Determine which rows need to be realized. We know every row will have the same height and
// only contain 3 items. Use that to determine the index for the first and last item that
// will be within that realization rect.
var firstRowIndex = Math.Max(
(int)(context.RealizationRect.Y / (this.MinItemSize.Height + this.RowSpacing)) - 1,
0);
var lastRowIndex = Math.Min(
(int)(context.RealizationRect.Bottom / (this.MinItemSize.Height + this.RowSpacing)) + 1,
(int)(context.ItemCount / 3));
// Determine which items will appear on those rows and what the rect will be for each item
var state = context.LayoutState as ActivityFeedLayoutState;
state.LayoutRects.Clear();
// Save the index of the first realized item. We'll use it as a starting point during arrange.
state.FirstRealizedIndex = firstRowIndex * 3;
// ideal item width that will expand/shrink to fill available space
double desiredItemWidth = Math.Max(this.MinItemSize.Width, (availableSize.Width - this.ColumnSpacing * 3) / 4);
// Foreach item between the first and last index,
// Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
// from a recycle pool
// Measure the element using an appropriate size
//
// Any element that was previously realized which we don't retrieve in this pass (via a call to
// GetElementOrCreateAt) will be automatically cleared and set aside for later re-use.
// Note: While this work fine, it does mean that more elements than are required may be
// created because it isn't until after our MeasureOverride completes that the unused elements
// will be recycled and available to use. We could avoid this by choosing to track the first/last
// index from the previous layout pass. The diff between the previous range and current range
// would represent the elements that we can pre-emptively make available for re-use by calling
// context.RecycleElement(element).
for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
{
int firstItemIndex = rowIndex * 3;
var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var index = firstItemIndex + columnIndex;
var rect = boundsForCurrentRow[index % 3];
var container = context.GetOrCreateElementAt(index);
container.Measure(
new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));
state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
}
}
// Calculate and return the size of all the content (realized or not) by figuring out
// what the bottom/right position of the last item would be.
var extentHeight = ((int)(context.ItemCount / 3) - 1) * (this.MinItemSize.Height + this.RowSpacing) + this.MinItemSize.Height;
// Report this as the desired size for the layout
return new Size(desiredItemWidth * 4 + this.ColumnSpacing * 2, extentHeight);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
// walk through the cache of containers and arrange
var state = context.LayoutState as ActivityFeedLayoutState;
var virtualContext = context as VirtualizingLayoutContext;
int currentIndex = state.FirstRealizedIndex;
foreach (var arrangeRect in state.LayoutRects)
{
var container = virtualContext.GetOrCreateElementAt(currentIndex);
container.Arrange(arrangeRect);
currentIndex++;
}
return finalSize;
}
#endregion
#region Helper methods
private Rect[] CalculateLayoutBoundsForRow(int rowIndex, double desiredItemWidth)
{
var boundsForRow = new Rect[3];
var yoffset = rowIndex * (this.MinItemSize.Height + this.RowSpacing);
boundsForRow[0].Y = boundsForRow[1].Y = boundsForRow[2].Y = yoffset;
boundsForRow[0].Height = boundsForRow[1].Height = boundsForRow[2].Height = this.MinItemSize.Height;
if (rowIndex % 2 == 0)
{
// Left tile (narrow)
boundsForRow[0].X = 0;
boundsForRow[0].Width = desiredItemWidth;
// Middle tile (narrow)
boundsForRow[1].X = boundsForRow[0].Right + this.ColumnSpacing;
boundsForRow[1].Width = desiredItemWidth;
// Right tile (wide)
boundsForRow[2].X = boundsForRow[1].Right + this.ColumnSpacing;
boundsForRow[2].Width = desiredItemWidth * 2 + this.ColumnSpacing;
}
else
{
// Left tile (wide)
boundsForRow[0].X = 0;
boundsForRow[0].Width = (desiredItemWidth * 2 + this.ColumnSpacing);
// Middle tile (narrow)
boundsForRow[1].X = boundsForRow[0].Right + this.ColumnSpacing;
boundsForRow[1].Width = desiredItemWidth;
// Right tile (narrow)
boundsForRow[2].X = boundsForRow[1].Right + this.ColumnSpacing;
boundsForRow[2].Width = desiredItemWidth;
}
return boundsForRow;
}
#endregion
}
internal class ActivityFeedLayoutState
{
public int FirstRealizedIndex { get; set; }
/// <summary>
/// List of layout bounds for items starting with the
/// FirstRealizedIndex.
/// </summary>
public List<Rect> LayoutRects
{
get
{
if (_layoutRects == null)
{
_layoutRects = new List<Rect>();
}
return _layoutRects;
}
}
private List<Rect> _layoutRects;
}
(Facoltativo) Gestione del mapping tra elementi e UIElement
Per impostazione predefinita, l'oggetto VirtualizingLayoutContext gestisce un mapping tra gli elementi realizzati e l'indice nell'origine dati che rappresentano. Un layout può scegliere di gestire questo mapping richiedendo sempre l'opzione SuppressAutoRecycle quando recupera un elemento tramite il metodo GetOrCreateElementAt, impedendo così il comportamento predefinito di riciclo automatico. Un layout può ad esempio scegliere di eseguire questa operazione se verrà usato solo quando lo scorrimento è limitato a una direzione e gli elementi considerati sono sempre contigui, ovvero quando la conoscenza dell'indice del primo elemento e dell'ultimo è sufficiente per conoscere tutti gli elementi che devono essere realizzati.
Esempio: misurazione del feed attività Xbox
Il frammento di codice seguente mostra la logica che è possibile aggiungere al metodo MeasureOverride nell'esempio precedente per gestire il mapping.
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
//...
// Determine which items will appear on those rows and what the rect will be for each item
var state = context.LayoutState as ActivityFeedLayoutState;
state.LayoutRects.Clear();
// Recycle previously realized elements that we know we won't need so that they can be used to
// fill in gaps without requiring us to realize additional elements.
var newFirstRealizedIndex = firstRowIndex * 3;
var newLastRealizedIndex = lastRowIndex * 3 + 3;
for (int i = state.FirstRealizedIndex; i < newFirstRealizedIndex; i++)
{
context.RecycleElement(state.IndexToElementMap.Get(i));
state.IndexToElementMap.Clear(i);
}
for (int i = state.LastRealizedIndex; i < newLastRealizedIndex; i++)
{
context.RecycleElement(context.IndexElementMap.Get(i));
state.IndexToElementMap.Clear(i);
}
// ...
// Foreach item between the first and last index,
// Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
// from a recycle pool
// Measure the element using an appropriate size
//
for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
{
int firstItemIndex = rowIndex * 3;
var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var index = firstItemIndex + columnIndex;
var rect = boundsForCurrentRow[index % 3];
UIElement container = null;
if (state.IndexToElementMap.Contains(index))
{
container = state.IndexToElementMap.Get(index);
}
else
{
container = context = context.GetOrCreateElementAt(index, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
state.IndexToElementMap.Add(index, container);
}
container.Measure(
new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));
state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
}
}
// ...
}
internal class ActivityFeedLayoutState
{
// ...
Dictionary<int, UIElement> IndexToElementMap { get; set; }
// ...
}
Layout con virtualizzazione dipendenti dal contenuto
Se devi prima misurare il contenuto dell'interfaccia utente per un elemento per determinarne le dimensioni esatte, il layout è dipendente dal contenuto. Puoi anche considerarlo come un layout in cui deve essere ogni elemento a determinare le proprie dimensioni e non il layout. I layout con virtualizzazione che rientrano in questa categoria sono più complessi.
Nota
I layout dipendenti dal contenuto non interrompono (e non devono interrompere) la virtualizzazione dei dati.
Stime
I layout dipendenti dal contenuto si basano sulle stime per indovinare sia le dimensioni del contenuto non realizzato sia la posizione del contenuto realizzato. Con il variare delle stime, il contenuto realizzato cambierà regolarmente posizione all'interno dell'area scorrevole. Questo può causare un'esperienza utente molto frustrante e fastidiosa, se non viene mitigata. Di seguito sono descritti i potenziali problemi e le possibili azioni di mitigazione.
Nota
I layout di dati che prendono in considerazione ogni elemento e conoscono le dimensioni esatte di tutti gli elementi, realizzati o meno, e le rispettive posizioni possono evitare completamente questi problemi.
Ancoraggio dello scorrimento
XAML offre un meccanismo per attenuare i cambi improvvisi di viewport con controlli di scorrimento che supportano l'ancoraggio dello scorrimento tramite l'implementazione dell'interfaccia IScrollAnchorPovider. Mentre l'utente modifica il contenuto, il controllo di scorrimento seleziona continuamente un elemento dal set di candidati che sono stati prescelti per essere rilevati. Se la posizione dell'elemento di ancoraggio viene spostata durante il layout, il controllo di scorrimento sposta automaticamente il viewport per mantenerlo visualizzato.
Il valore di RecommendedAnchorIndex fornito al layout può riflettere l'elemento di ancoraggio attualmente selezionato che è stato scelto dal controllo di scorrimento. In alternativa, se uno sviluppatore richiede in modo esplicito che un elemento venga realizzato per un indice con il metodo GetOrCreateElement sul contenitore ItemsRepeater, tale indice viene assegnato come RecommendedAnchorIndex al successivo passaggio di layout. In questo modo il layout è preparato allo scenario probabile che uno sviluppatore realizzi un elemento e successivamente ne richieda la visualizzazione tramite il metodo StartBringIntoView.
L'oggetto RecommendedAnchorIndex è l'indice dell'elemento nell'origine dati che un layout dipendente dal contenuto deve posizionare per primo quando stima la posizione dei relativi elementi. Deve essere usato come punto di partenza per il posizionamento di altri elementi realizzati.
Impatto sulle barre di scorrimento
Anche con l'ancoraggio dello scorrimento, se le stime del layout variano notevolmente, presumibilmente a causa di variazioni significative delle dimensioni del contenuto, potrebbe sembrare che il cursore della barra di scorrimento si muova a salti. Questo può risultare fastidioso a un utente se il cursore non sembra rilevare la posizione del puntatore del mouse durante il trascinamento.
Quanto maggiore è l'accuratezza delle stime del layout, tanto meno probabile sarà la visualizzazione di un movimento a salti del cursore della barra di scorrimento.
Correzioni del layout
Un layout dipendente dal contenuto deve essere preparato per razionalizzare la stima in base alla realtà. Quando, ad esempio, l'utente scorre verso la parte superiore del contenuto e il layout realizza il primo elemento, potrebbe rilevare che la posizione prevista dell'elemento rispetto a quello da cui è partito ne determini la visualizzazione in un punto diverso dall'origine (x:0, y:0). Quando ciò si verifica, il layout può usare la proprietà LayoutOrigin per impostare la posizione calcolata come nuova origine del layout. Il risultato finale è simile all'ancoraggio dello scorrimento in cui il viewport del controllo di scorrimento viene regolato automaticamente in base alla posizione del contenuto come riportato dal layout.
Viewport scollegati
Le dimensioni restituite dal metodo MeasureOverride del layout rappresentano l'ipotesi più plausibile delle dimensioni del contenuto, che può variare con ogni layout successivo. Quando un utente esegue lo scorrimento, il layout viene costantemente rivalutato con un oggetto RealizationRect aggiornato.
Se un utente trascina il cursore molto rapidamente, è possibile che il viewport, dal punto di vista del layout, si muova a grandi salti nel caso in cui la posizione precedente non si sovrapponga a quella corrente. Ciò è dovuto alla natura asincrona dello scorrimento. È anche possibile che un'app che utilizza il layout richieda che venga visualizzato un elemento che non è attualmente realizzato e che probabilmente verrà posizionato all'esterno dell'intervallo corrente rilevato dal layout.
Quando il layout scopre che la sua ipotesi non è corretta e/o rileva uno spostamento imprevisto del viewport, deve modificare l'orientamento della sua posizione iniziale. I layout con virtualizzazione inclusi nei controlli XAML sono sviluppati come layout dipendenti dal contenuto poiché inseriscono un minor numero di restrizioni sulla natura del contenuto che verrà visualizzato.
Esempio: layout pila semplice con virtualizzazione per elementi di dimensioni variabili
L'esempio seguente illustra un semplice layout pila per elementi di dimensioni variabili con le caratteristiche seguenti:
- Supporta la virtualizzazione dell'interfaccia utente.
- Usa le stime per indovinare le dimensioni degli elementi non realizzati.
- È consapevole dei possibili spostamenti discontinui del viewport.
- Applica correzioni di layout per tenere conto di tali spostamenti.
Utilizzo: Markup
<ScrollViewer>
<ItemsRepeater x:Name="repeater" >
<ItemsRepeater.Layout>
<local:VirtualizingStackLayout />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:Key="item">
<UserControl IsTabStop="True" UseSystemFocusVisuals="True" Margin="5">
<StackPanel BorderThickness="1" Background="LightGray" Margin="5">
<Image x:Name="recipeImage" Source="{Binding ImageUri}" Width="100" Height="100"/>
<TextBlock x:Name="recipeDescription"
Text="{Binding Description}"
TextWrapping="Wrap"
Margin="10" />
</StackPanel>
</UserControl>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
Codebehind: Main.cs
string _lorem = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam laoreet erat vel massa rutrum, eget mollis massa vulputate. Vivamus semper augue leo, eget faucibus nulla mattis nec. Donec scelerisque lacus at dui ultricies, eget auctor ipsum placerat. Integer aliquet libero sed nisi eleifend, nec rutrum arcu lacinia. Sed a sem et ante gravida congue sit amet ut augue. Donec quis pellentesque urna, non finibus metus. Proin sed ornare tellus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam laoreet erat vel massa rutrum, eget mollis massa vulputate. Vivamus semper augue leo, eget faucibus nulla mattis nec. Donec scelerisque lacus at dui ultricies, eget auctor ipsum placerat. Integer aliquet libero sed nisi eleifend, nec rutrum arcu lacinia. Sed a sem et ante gravida congue sit amet ut augue. Donec quis pellentesque urna, non finibus metus. Proin sed ornare tellus.";
var rnd = new Random();
var data = new ObservableCollection<Recipe>(Enumerable.Range(0, 300).Select(k =>
new Recipe
{
ImageUri = new Uri(string.Format("ms-appx:///Images/recipe{0}.png", k % 8 + 1)),
Description = k + " - " + _lorem.Substring(0, rnd.Next(50, 350))
}));
repeater.ItemsSource = data;
Codice: VirtualizingStackLayout.cs
// This is a sample layout that stacks elements one after
// the other where each item can be of variable height. This is
// also a virtualizing layout - we measure and arrange only elements
// that are in the viewport. Not measuring/arranging all elements means
// that we do not have the complete picture and need to estimate sometimes.
// For example the size of the layout (extent) is an estimation based on the
// average heights we have seen so far. Also, if you drag the mouse thumb
// and yank it quickly, then we estimate what goes in the new viewport.
// The layout caches the bounds of everything that are in the current viewport.
// During measure, we might get a suggested anchor (or start index), we use that
// index to start and layout the rest of the items in the viewport relative to that
// index. Note that since we are estimating, we can end up with negative origin when
// the viewport is somewhere in the middle of the extent. This is achieved by setting the
// LayoutOrigin property on the context. Once this is set, future viewport will account
// for the origin.
public class VirtualizingStackLayout : VirtualizingLayout
{
// Estimation state
List<double> m_estimationBuffer = Enumerable.Repeat(0d, 100).ToList();
int m_numItemsUsedForEstimation = 0;
double m_totalHeightForEstimation = 0;
// State to keep track of realized bounds
int m_firstRealizedDataIndex = 0;
List<Rect> m_realizedElementBounds = new List<Rect>();
Rect m_lastExtent = new Rect();
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var viewport = context.RealizationRect;
DebugTrace("MeasureOverride: Viewport " + viewport);
// Remove bounds for elements that are now outside the viewport.
// Proactive recycling elements means we can reuse it during this measure pass again.
RemoveCachedBoundsOutsideViewport(viewport);
// Find the index of the element to start laying out from - the anchor
int startIndex = GetStartIndex(context, availableSize);
// Measure and layout elements starting from the start index, forward and backward.
Generate(context, availableSize, startIndex, forward:true);
Generate(context, availableSize, startIndex, forward:false);
// Estimate the extent size. Note that this can have a non 0 origin.
m_lastExtent = EstimateExtent(context, availableSize);
context.LayoutOrigin = new Point(m_lastExtent.X, m_lastExtent.Y);
return new Size(m_lastExtent.Width, m_lastExtent.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
DebugTrace("ArrangeOverride: Viewport" + context.RealizationRect);
for (int realizationIndex = 0; realizationIndex < m_realizedElementBounds.Count; realizationIndex++)
{
int currentDataIndex = m_firstRealizedDataIndex + realizationIndex;
DebugTrace("Arranging " + currentDataIndex);
// Arrange the child. If any alignment needs to be done, it
// can be done here.
var child = context.GetOrCreateElementAt(currentDataIndex);
var arrangeBounds = m_realizedElementBounds[realizationIndex];
arrangeBounds.X -= m_lastExtent.X;
arrangeBounds.Y -= m_lastExtent.Y;
child.Arrange(arrangeBounds);
}
return finalSize;
}
// The data collection has changed, since we are maintaining the bounds of elements
// in the viewport, we will update the list to account for the collection change.
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
{
InvalidateMeasure();
if (m_realizedElementBounds.Count > 0)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
case NotifyCollectionChangedAction.Replace:
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
case NotifyCollectionChangedAction.Remove:
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
break;
case NotifyCollectionChangedAction.Reset:
m_realizedElementBounds.Clear();
m_firstRealizedDataIndex = 0;
break;
default:
throw new NotImplementedException();
}
}
}
// Figure out which index to use as the anchor and start laying out around it.
private int GetStartIndex(VirtualizingLayoutContext context, Size availableSize)
{
int startDataIndex = -1;
var recommendedAnchorIndex = context.RecommendedAnchorIndex;
bool isSuggestedAnchorValid = recommendedAnchorIndex != -1;
if (isSuggestedAnchorValid)
{
if (IsRealized(recommendedAnchorIndex))
{
startDataIndex = recommendedAnchorIndex;
}
else
{
ClearRealizedRange();
startDataIndex = recommendedAnchorIndex;
}
}
else
{
// Find the first realized element that is visible in the viewport.
startDataIndex = GetFirstRealizedDataIndexInViewport(context.RealizationRect);
if (startDataIndex < 0)
{
startDataIndex = EstimateIndexForViewport(context.RealizationRect, context.ItemCount);
ClearRealizedRange();
}
}
// We have an anchorIndex, realize and measure it and
// figure out its bounds.
if (startDataIndex != -1 & context.ItemCount > 0)
{
if (m_realizedElementBounds.Count == 0)
{
m_firstRealizedDataIndex = startDataIndex;
}
var newAnchor = EnsureRealized(startDataIndex);
DebugTrace("Measuring start index " + startDataIndex);
var desiredSize = MeasureElement(context, startDataIndex, availableSize);
var bounds = new Rect(
0,
newAnchor ?
(m_totalHeightForEstimation / m_numItemsUsedForEstimation) * startDataIndex : GetCachedBoundsForDataIndex(startDataIndex).Y,
availableSize.Width,
desiredSize.Height);
SetCachedBoundsForDataIndex(startDataIndex, bounds);
}
return startDataIndex;
}
private void Generate(VirtualizingLayoutContext context, Size availableSize, int anchorDataIndex, bool forward)
{
// Generate forward or backward from anchorIndex until we hit the end of the viewport
int step = forward ? 1 : -1;
int previousDataIndex = anchorDataIndex;
int currentDataIndex = previousDataIndex + step;
var viewport = context.RealizationRect;
while (IsDataIndexValid(currentDataIndex, context.ItemCount) &&
ShouldContinueFillingUpSpace(previousDataIndex, forward, viewport))
{
EnsureRealized(currentDataIndex);
DebugTrace("Measuring " + currentDataIndex);
var desiredSize = MeasureElement(context, currentDataIndex, availableSize);
var previousBounds = GetCachedBoundsForDataIndex(previousDataIndex);
Rect currentBounds = new Rect(0,
forward ? previousBounds.Y + previousBounds.Height : previousBounds.Y - desiredSize.Height,
availableSize.Width,
desiredSize.Height);
SetCachedBoundsForDataIndex(currentDataIndex, currentBounds);
previousDataIndex = currentDataIndex;
currentDataIndex += step;
}
}
// Remove bounds that are outside the viewport, leaving one extra since our
// generate stops after generating one extra to know that we are outside the
// viewport.
private void RemoveCachedBoundsOutsideViewport(Rect viewport)
{
int firstRealizedIndexInViewport = 0;
while (firstRealizedIndexInViewport < m_realizedElementBounds.Count &&
!Intersects(m_realizedElementBounds[firstRealizedIndexInViewport], viewport))
{
firstRealizedIndexInViewport++;
}
int lastRealizedIndexInViewport = m_realizedElementBounds.Count - 1;
while (lastRealizedIndexInViewport >= 0 &&
!Intersects(m_realizedElementBounds[lastRealizedIndexInViewport], viewport))
{
lastRealizedIndexInViewport--;
}
if (firstRealizedIndexInViewport > 0)
{
m_firstRealizedDataIndex += firstRealizedIndexInViewport;
m_realizedElementBounds.RemoveRange(0, firstRealizedIndexInViewport);
}
if (lastRealizedIndexInViewport >= 0 && lastRealizedIndexInViewport < m_realizedElementBounds.Count - 2)
{
m_realizedElementBounds.RemoveRange(lastRealizedIndexInViewport + 2, m_realizedElementBounds.Count - lastRealizedIndexInViewport - 3);
}
}
private bool Intersects(Rect bounds, Rect viewport)
{
return !(bounds.Bottom < viewport.Top ||
bounds.Top > viewport.Bottom);
}
private bool ShouldContinueFillingUpSpace(int dataIndex, bool forward, Rect viewport)
{
var bounds = GetCachedBoundsForDataIndex(dataIndex);
return forward ?
bounds.Y < viewport.Bottom :
bounds.Y > viewport.Top;
}
private bool IsDataIndexValid(int currentDataIndex, int itemCount)
{
return currentDataIndex >= 0 && currentDataIndex < itemCount;
}
private int EstimateIndexForViewport(Rect viewport, int dataCount)
{
double averageHeight = m_totalHeightForEstimation / m_numItemsUsedForEstimation;
int estimatedIndex = (int)(viewport.Top / averageHeight);
// clamp to an index within the collection
estimatedIndex = Math.Max(0, Math.Min(estimatedIndex, dataCount));
return estimatedIndex;
}
private int GetFirstRealizedDataIndexInViewport(Rect viewport)
{
int index = -1;
if (m_realizedElementBounds.Count > 0)
{
for (int i = 0; i < m_realizedElementBounds.Count; i++)
{
if (m_realizedElementBounds[i].Y < viewport.Bottom &&
m_realizedElementBounds[i].Bottom > viewport.Top)
{
index = m_firstRealizedDataIndex + i;
break;
}
}
}
return index;
}
private Size MeasureElement(VirtualizingLayoutContext context, int index, Size availableSize)
{
var child = context.GetOrCreateElementAt(index);
child.Measure(availableSize);
int estimationBufferIndex = index % m_estimationBuffer.Count;
bool alreadyMeasured = m_estimationBuffer[estimationBufferIndex] != 0;
if (!alreadyMeasured)
{
m_numItemsUsedForEstimation++;
}
m_totalHeightForEstimation -= m_estimationBuffer[estimationBufferIndex];
m_totalHeightForEstimation += child.DesiredSize.Height;
m_estimationBuffer[estimationBufferIndex] = child.DesiredSize.Height;
return child.DesiredSize;
}
private bool EnsureRealized(int dataIndex)
{
if (!IsRealized(dataIndex))
{
int realizationIndex = RealizationIndex(dataIndex);
Debug.Assert(dataIndex == m_firstRealizedDataIndex - 1 ||
dataIndex == m_firstRealizedDataIndex + m_realizedElementBounds.Count ||
m_realizedElementBounds.Count == 0);
if (realizationIndex == -1)
{
m_realizedElementBounds.Insert(0, new Rect());
}
else
{
m_realizedElementBounds.Add(new Rect());
}
if (m_firstRealizedDataIndex > dataIndex)
{
m_firstRealizedDataIndex = dataIndex;
}
return true;
}
return false;
}
// Figure out the extent of the layout by getting the number of items remaining
// above and below the realized elements and getting an estimation based on
// average item heights seen so far.
private Rect EstimateExtent(VirtualizingLayoutContext context, Size availableSize)
{
double averageHeight = m_totalHeightForEstimation / m_numItemsUsedForEstimation;
Rect extent = new Rect(0, 0, availableSize.Width, context.ItemCount * averageHeight);
if (context.ItemCount > 0 && m_realizedElementBounds.Count > 0)
{
extent.Y = m_firstRealizedDataIndex == 0 ?
m_realizedElementBounds[0].Y :
m_realizedElementBounds[0].Y - (m_firstRealizedDataIndex - 1) * averageHeight;
int lastRealizedIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count;
if (lastRealizedIndex == context.ItemCount - 1)
{
var lastBounds = m_realizedElementBounds[m_realizedElementBounds.Count - 1];
extent.Y = lastBounds.Bottom;
}
else
{
var lastBounds = m_realizedElementBounds[m_realizedElementBounds.Count - 1];
int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count;
int numItemsAfterLastRealizedIndex = context.ItemCount - lastRealizedDataIndex;
extent.Height = lastBounds.Bottom + numItemsAfterLastRealizedIndex * averageHeight - extent.Y;
}
}
DebugTrace("Extent " + extent + " with average height " + averageHeight);
return extent;
}
private bool IsRealized(int dataIndex)
{
int realizationIndex = dataIndex - m_firstRealizedDataIndex;
return realizationIndex >= 0 && realizationIndex < m_realizedElementBounds.Count;
}
// Index in the m_realizedElementBounds collection
private int RealizationIndex(int dataIndex)
{
return dataIndex - m_firstRealizedDataIndex;
}
private void OnItemsAdded(int index, int count)
{
// Using the old indexes here (before it was updated by the collection change)
// if the insert data index is between the first and last realized data index, we need
// to insert items.
int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count - 1;
int newStartingIndex = index;
if (newStartingIndex > m_firstRealizedDataIndex &&
newStartingIndex <= lastRealizedDataIndex)
{
// Inserted within the realized range
int insertRangeStartIndex = newStartingIndex - m_firstRealizedDataIndex;
for (int i = 0; i < count; i++)
{
// Insert null (sentinel) here instead of an element, that way we do not
// end up creating a lot of elements only to be thrown out in the next layout.
int insertRangeIndex = insertRangeStartIndex + i;
int dataIndex = newStartingIndex + i;
// This is to keep the contiguousness of the mapping
m_realizedElementBounds.Insert(insertRangeIndex, new Rect());
}
}
else if (index <= m_firstRealizedDataIndex)
{
// Items were inserted before the realized range.
// We need to update m_firstRealizedDataIndex;
m_firstRealizedDataIndex += count;
}
}
private void OnItemsRemoved(int index, int count)
{
int lastRealizedDataIndex = m_firstRealizedDataIndex + m_realizedElementBounds.Count - 1;
int startIndex = Math.Max(m_firstRealizedDataIndex, index);
int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
bool removeAffectsFirstRealizedDataIndex = (index <= m_firstRealizedDataIndex);
if (endIndex >= startIndex)
{
ClearRealizedRange(RealizationIndex(startIndex), endIndex - startIndex + 1);
}
if (removeAffectsFirstRealizedDataIndex &&
m_firstRealizedDataIndex != -1)
{
m_firstRealizedDataIndex -= count;
}
}
private void ClearRealizedRange(int startRealizedIndex, int count)
{
m_realizedElementBounds.RemoveRange(startRealizedIndex, count);
if (startRealizedIndex == 0)
{
m_firstRealizedDataIndex = m_realizedElementBounds.Count == 0 ? 0 : m_firstRealizedDataIndex + count;
}
}
private void ClearRealizedRange()
{
m_realizedElementBounds.Clear();
m_firstRealizedDataIndex = 0;
}
private Rect GetCachedBoundsForDataIndex(int dataIndex)
{
return m_realizedElementBounds[RealizationIndex(dataIndex)];
}
private void SetCachedBoundsForDataIndex(int dataIndex, Rect bounds)
{
m_realizedElementBounds[RealizationIndex(dataIndex)] = bounds;
}
private Rect GetCachedBoundsForRealizationIndex(int relativeIndex)
{
return m_realizedElementBounds[relativeIndex];
}
void DebugTrace(string message, params object[] args)
{
Debug.WriteLine(message, args);
}
}