Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Ein Container (z. B. Panel), der seine Layoutlogik an ein anderes Objekt delegiert, basiert auf dem angefügten Layoutobjekt, um das Layoutverhalten für seine untergeordneten Elemente bereitzustellen. Ein angefügtes Layoutmodell bietet Flexibilität für eine Anwendung zum Ändern des Layouts von Elementen zur Laufzeit oder eine einfachere Freigabe von Aspekten des Layouts zwischen verschiedenen Teilen der Benutzeroberfläche (z. B. Elemente in den Zeilen einer Tabelle, die scheinbar innerhalb einer Spalte ausgerichtet werden).
In diesem Thema befassen wir uns mit der Erstellung eines angefügten Layouts (Virtualisierung und Nicht-Virtualisierung), den Konzepten und Klassen, die Sie verstehen müssen, und die Kompromisse, die Sie bei der Entscheidung zwischen diesen berücksichtigen müssen.
| WinUI abrufen |
|---|
| Dieses Steuerelement ist bestandteil von WinUI, einem NuGet-Paket, das neue Steuerelemente und UI-Features für Windows-Apps enthält. Weitere Informationen, einschließlich Installationsanweisungen, finden Sie in der WinUI-Übersicht. |
Wichtige APIs:
Schlüsselkonzepte
Das Ausführen des Layouts erfordert, dass zwei Fragen für jedes Element beantwortet werden:
Welche Größe wird dieses Element aufweisen?
Was wird die Position dieses Elements sein?
Das Layoutsystem von XAML, das diese Fragen beantwortet, wird kurz im Rahmen der Diskussion über benutzerdefinierte Panels behandelt.
Container und Kontext
Konzeptionell füllt der XAML-Bereich zwei wichtige Rollen im Framework aus:
- Sie kann untergeordnete Elemente enthalten und führt eine Verzweigung in der Struktur der Elemente ein.
- Sie wendet eine bestimmte Layoutstrategie auf diese untergeordneten Elemente an.
Aus diesem Grund ist ein Panel in XAML häufig synonym mit Layout, aber technisch gesehen, macht mehr als nur Layout.
Der ItemsRepeater verhält sich auch wie Panel, aber im Gegensatz zu Panel wird keine untergeordnete Eigenschaft verfügbar gemacht, die das programmgesteuerte Hinzufügen oder Entfernen von UIElement-untergeordneten Elementen ermöglicht. Stattdessen wird die Lebensdauer der untergeordneten Elemente automatisch vom Framework verwaltet, um einer Sammlung von Datenelementen zu entsprechen. Obwohl es nicht von Panel abgeleitet wird, verhält es sich und wird vom Framework wie ein Panel behandelt.
Hinweis
Das LayoutPanel ist ein Container, der von Panel abgeleitet wird und seine Logik an das angefügte Layout-Objekt delegiert. LayoutPanel befindet sich in der Vorschau und ist derzeit nur in den Prerelease-Tropfen des WinUI-Pakets verfügbar.
Behälter
Panel ist ein Container von Elementen, der auch die Möglichkeit hat, Pixel für einen Hintergrund zu rendern. Panels bieten eine Möglichkeit, allgemeine Layoutlogik in einem einfach zu verwendenden Paket zu kapseln.
Das Konzept des angefügten Layouts macht den Unterschied zwischen den beiden Rollen des Containers und layouts klarer. Wenn der Container seine Layoutlogik an ein anderes Objekt delegiert, würden wir das angefügte Layout aufrufen, wie im folgenden Codeausschnitt zu sehen. Container, die von FrameworkElement erben, z. B. layoutPanel, machen automatisch die allgemeinen Eigenschaften verfügbar, die Eingaben für den LAYOUTprozess von XAML bereitstellen (z. B. Höhe und Breite).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Während des Layoutprozesses basiert der Container auf dem angefügten UniformGridLayout , um seine untergeordneten Elemente zu messen und anzuordnen.
Per-Container Staat
Mit einem angefügten Layout kann eine einzelne Instanz des Layoutobjekts vielen Containern wie im folgenden Codeausschnitt zugeordnet werden. Daher darf sie nicht von dem Hostcontainer abhängig sein oder direkt darauf verweisen. Beispiel:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
Für diese Situation muss ExampleLayout sorgfältig den Zustand berücksichtigen, den es in seiner Layoutberechnung verwendet und wo dieser Zustand gespeichert ist, um zu vermeiden, dass sich das Layout für Elemente in einem Bereich mit dem anderen auswirkt. Es wäre analog zu einem benutzerdefinierten Panel, dessen MeasureOverride- und ArrangeOverride-Logik von den Werten seiner statischen Eigenschaften abhängt.
LayoutContext
Der Zweck des LayoutContext ist es, diese Herausforderungen zu bewältigen. Es bietet das angefügte Layout die Möglichkeit, mit dem Hostcontainer zu interagieren, z. B. das Abrufen untergeordneter Elemente, ohne eine direkte Abhängigkeit zwischen den beiden einzuführen. Der Kontext ermöglicht es dem Layout auch, jeden Zustand zu speichern, für den er erforderlich ist, dass er mit den untergeordneten Elementen des Containers verknüpft sein kann.
Einfache, nicht virtualisierende Layouts müssen häufig keinen Zustand beibehalten, wodurch es zu einem Nicht-Problem wird. Ein komplexeres Layout, z. B. "Grid", kann sich jedoch entscheiden, den Zustand zwischen dem Measure beizubehalten und den Aufruf anzuordnen, um die Neuberechnung eines Werts zu vermeiden.
Beim Virtualisieren von Layouts muss häufig ein Zustand zwischen dem Measure und der Anordnung sowie zwischen iterativen Layoutdurchläufen beibehalten werden.
Initialisieren und Aufheben der Initialisierung Per-Container Zustands
Wenn ein Layout an einen Container angefügt wird, wird seine InitializeForContextCore-Methode aufgerufen und bietet die Möglichkeit, ein Objekt zum Speichern des Zustands zu initialisieren.
Ebenso wird beim Entfernen des Layouts aus einem Container die UninitializeForContextCore-Methode aufgerufen. Dadurch erhält das Layout die Möglichkeit, jeden Zustand zu bereinigen, den er diesem Container zugeordnet hatte.
Das Zustandsobjekt des Layouts kann mit dem Container mit der LayoutState-Eigenschaft im Kontext gespeichert und abgerufen werden.
UI-Virtualisierung
Die UI-Virtualisierung bedeutet, dass die Erstellung eines UI-Objekts verzögert wird , bis es erforderlich ist. Es ist eine Leistungsoptimierung. Für Szenarien ohne Bildlauf, die bei Bedarf ermitteln, kann auf einer beliebigen Anzahl von App-spezifischen Elementen basieren. In diesen Fällen sollten Apps die Verwendung von "x:Load" in Betracht ziehen. Es ist keine spezielle Behandlung in Ihrem Layout erforderlich.
Bei Bildlauf-basierten Szenarien wie z. B. einer Liste basiert die Bestimmung bei Bedarf häufig auf "wird es für einen Benutzer sichtbar sein", was stark davon abhängt, wo sie während des Layoutprozesses platziert wurde und besondere Überlegungen erfordert. Dieses Szenario ist ein Fokus für dieses Dokument.
Hinweis
Obwohl in diesem Dokument nicht behandelt wird, können dieselben Funktionen, die ui-Virtualisierung in Bildlaufszenarien aktivieren, in Szenarien ohne Bildlauf angewendet werden. Beispielsweise ein datengesteuertes ToolBar-Steuerelement, das die Lebensdauer der angezeigten Befehle verwaltet und auf Änderungen im verfügbaren Raum reagiert, indem Elemente zwischen einem sichtbaren Bereich und einem Überlaufmenü wiederverwertt/verschoben werden.
Erste Schritte
Entscheiden Sie zunächst, ob das zu erstellende Layout die UI-Virtualisierung unterstützen soll.
Ein paar Dinge, die Sie berücksichtigen sollten...
- Nicht virtualisierende Layouts sind einfacher zu erstellen. Wenn die Anzahl der Elemente immer klein ist, wird das Erstellen eines nicht virtualisierenden Layouts empfohlen.
- Die Plattform bietet eine Reihe angefügter Layouts, die mit ItemsRepeater und LayoutPanel funktionieren, um allgemeine Anforderungen zu decken. Machen Sie sich mit denen vertraut, bevor Sie sich entscheiden, ein benutzerdefiniertes Layout zu definieren.
- Beim Virtualisieren von Layouts gibt es im Vergleich zu einem nicht virtualisierenden Layout immer zusätzliche CPU- und Arbeitsspeicherkosten/-komplexität/Mehraufwand. Als allgemeine Faustregel, wenn die untergeordneten Elemente das Layout verwalten müssen, wahrscheinlich in einen Bereich passen, der 3x die Größe des Viewports ist, dann gibt es möglicherweise nicht viel Gewinn aus einem Virtualisierungslayout. Die 3x-Größe wird weiter unten in diesem Dokument ausführlicher behandelt, liegt jedoch an der asynchronen Art des Bildlaufs unter Windows und deren Auswirkungen auf die Virtualisierung.
Tipp
Als Referenzpunkt beginnen die Standardeinstellungen für listView (und ItemsRepeater), dass das Recycling erst beginnt, wenn die Anzahl der Elemente ausreicht, um 3x die Größe des aktuellen Viewports auszufüllen.
Auswählen des Basistyps
Der Basislayouttyp weist zwei abgeleitete Typen auf, die als Ausgangspunkt für die Erstellung eines angefügten Layouts dienen:
Layout ohne Virtualisierung
Der Ansatz zum Erstellen eines nicht virtualisierenden Layouts sollte allen Benutzern vertraut sein, die einen benutzerdefinierten Bereich erstellt haben. Dieselben Konzepte gelten. Der Hauptunterschied besteht darin, dass ein NonVirtualizingLayoutContextfür den Zugriff auf die Children-Auflistung verwendet wird, und das Layout kann den Zustand speichern.
- Abgeleitet vom Basistyp NonVirtualizingLayout (anstelle von Panel).
- (Optional) Definieren Sie Abhängigkeitseigenschaften, die beim Ändern das Layout ungültig werden.
- (Neu/Optional) Initialisieren Sie jedes Zustandsobjekt, das vom Layout als Teil der InitializeForContextCore erforderlich ist. Verstecke es im Host-Container, indem du den mit dem Kontext bereitgestellten LayoutState verwendest.
- Überschreiben Sie die MeasureOverride , und rufen Sie die Measure-Methode für alle untergeordneten Elemente auf.
- Überschreiben Sie die ArrangeOverride , und rufen Sie die Arrange-Methode für alle untergeordneten Elemente auf.
- (Neu/Optional) Bereinigen Sie alle gespeicherten Zustände als Teil der UninitializeForContextCore.
Beispiel: Ein einfaches Stapellayout (Varying-Sized Elemente)
Hier ist ein sehr einfaches, nicht virtualisierendes Stapellayout unterschiedlicher Größe. Es fehlt an Eigenschaften, um das Layoutverhalten anzupassen. Die folgende Implementierung veranschaulicht, wie das Layout auf das kontextobjekt basiert, das vom Container bereitgestellt wird:
- Abrufen der Anzahl von untergeordneten Elementen und
- Greifen Sie auf jedes untergeordnete Element nach Index zu.
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>
Virtualisieren von Layouts
Ähnlich wie bei einem nicht virtualisierenden Layout sind die allgemeinen Schritte für ein Virtualisierungslayout identisch. Die Komplexität liegt weitgehend darin, zu bestimmen, welche Elemente innerhalb des Viewports fallen und realisiert werden sollten.
- Abgeleitet vom Basistyp VirtualizingLayout.
- (Optional) Definieren Sie Die Abhängigkeitseigenschaften, die beim Ändern das Layout ungültig werden.
- Initialisieren Sie alle Zustandsobjekte, die vom Layout als Teil der InitializeForContextCore benötigt werden. Verwahren Sie es im Host-Container mithilfe des mit dem Kontext bereitgestellten LayoutState.
- Überschreiben Sie die MeasureOverride , und rufen Sie die Measure-Methode für jedes untergeordnete Element auf, das realisiert werden soll.
- Die GetOrCreateElementAt-Methode wird verwendet, um ein UIElement abzurufen, das vom Framework vorbereitet wurde (z. B. angewendete Datenbindungen).
- Überschreiben Sie die ArrangeOverride , und rufen Sie die Arrange-Methode für jedes realisierte untergeordnete Element auf.
- (Optional) Bereinigen Sie alle gespeicherten Zustände als Teil der UninitializeForContextCore.
Tipp
Der von der MeasureOverride zurückgegebene Wert wird als Größe des virtualisierten Inhalts verwendet.
Beim Erstellen eines Virtualisierungslayouts müssen zwei allgemeine Ansätze berücksichtigt werden. Ob man eine oder andere auswählen soll, hängt weitgehend davon ab, wie Sie die Größe eines Elements bestimmen. Wenn der Index eines Elements im Dataset oder die Daten selbst die letztendliche Größe bestimmen, würden wir es als datenabhängig betrachten. Dies ist einfacher zu erstellen. Wenn jedoch die einzige Möglichkeit zum Ermitteln der Größe eines Elements darin besteht, die Benutzeroberfläche zu erstellen und zu messen, würden wir sagen, dass es inhaltsabhängig ist. Diese sind komplexer.
Der Layoutprozess
Ganz gleich, ob Sie ein Daten- oder inhaltsabhängiges Layout erstellen, es ist wichtig, den Layoutprozess und die Auswirkungen des asynchronen Bildlaufs von Windows zu verstehen.
Eine (über)vereinfachte Ansicht der vom Framework durchgeführten Schritte vom Start bis zum Anzeigen der Benutzeroberfläche auf dem Bildschirm ist folgendes:
Es analysiert das Markup.
Generiert eine Struktur von Elementen.
Führt einen Layoutdurchlauf aus.
Führt einen Renderdurchlauf aus.
Bei der UI-Virtualisierung wird das Erstellen der Elemente, die normalerweise in Schritt 2 durchgeführt werden würden, verzögert oder frühzeitig beendet, nachdem festgestellt wurde, dass ausreichend Inhalt erstellt wurde, um den Viewport auszufüllen. Ein Virtualisierungscontainer (z. B. ItemsRepeater) wird auf das angefügte Layout zurückgesetzt, um diesen Prozess zu steuern. Es stellt das angefügte Layout mit einem VirtualizingLayoutContext bereit, der die zusätzlichen Informationen anzeigt, die ein virtualisierendes Layout benötigt.
Die RealisierungRect (d. h. Viewport)
Scrollen unter Windows erfolgt asynchron mit dem UI-Thread. Sie wird nicht vom Layout des Frameworks gesteuert. Vielmehr tritt die Interaktion und Bewegung im Kompositor des Systems auf. Der Vorteil dieses Ansatzes besteht darin, dass das Verschieben von Inhalten immer bei 60 fps erfolgen kann. Die Herausforderung besteht jedoch darin, dass der "Viewport", wie vom Layout gesehen, etwas veraltet sein könnte, relativ zu dem, was tatsächlich auf dem Bildschirm sichtbar ist. Wenn ein Benutzer schnell scrollt, kann er die Geschwindigkeit des UI-Threads übertreffen, um neue Inhalte zu generieren und "In Schwarz verschieben". Aus diesem Grund ist es häufig erforderlich, dass ein Virtualisierungslayout einen zusätzlichen Puffer vorbereiteter Elemente generiert, der ausreicht, um einen Bereich zu füllen, der größer als der Viewport ist. Wenn während des Bildlaufs beim Scrollen unter einer höheren Last der Benutzer noch Inhalte angezeigt werden.
Da die Elementerstellung kostspielig ist, stellt die Virtualisierung von Containern (z. B. ItemsRepeater) zunächst das angefügte Layout mit einem RealisationRect bereit, der dem Viewport entspricht. Im Leerlauf kann der Container den Puffer der vorbereiteten Inhalte vergrößern, indem wiederholte Aufrufe an das Layout mit einem zunehmend größeren Realisierungsrecht vorgenommen werden. Dieses Verhalten ist eine Leistungsoptimierung, die versucht, eine Balance zwischen schneller Startzeit und einer guten Verschiebungserfahrung zu erzielen. Die maximale Puffergröße, die der ItemsRepeater generiert, wird durch die Eigenschaften VerticalCacheLength und HorizontalCacheLength gesteuert.
Wiederverwenden von Elementen (Recycling)
Das Layout wird erwartet, dass die Elemente bei jeder Ausführung des RealizationRect-Elements vergrößert und positioniert werden. Standardmäßig werden alle nicht verwendeten Elemente am Ende jedes Layoutdurchlaufs wiederverwendet.
Der VirtualizingLayoutContext , der als Teil der MeasureOverride und ArrangeOverride an das Layout übergeben wird, stellt die zusätzlichen Informationen bereit, die ein Virtualisierungslayout benötigt. Einige der am häufigsten verwendeten Dinge, die es bietet, sind die Möglichkeit:
- Abfragen der Anzahl der Elemente in den Daten (ItemCount).
- Rufen Sie ein bestimmtes Element mithilfe der GetItemAt-Methode ab.
- Rufen Sie eine RealisationRect ab, die den Viewport und den Puffer darstellt, den das Layout mit realisierten Elementen füllen soll.
- Fordern Sie das UIElement für ein bestimmtes Element mit der GetOrCreateElement-Methode an.
Wenn Sie ein Element für einen bestimmten Index anfordern, wird dieses Element für diesen Durchlauf des Layouts als "verwendet" gekennzeichnet. Wenn das Element noch nicht vorhanden ist, wird es realisiert und automatisch für die Verwendung vorbereitet (z. B. durch Aufblasen der in einer DataTemplate definierten UI-Struktur, Verarbeiten einer Datenbindung usw.). Andernfalls wird sie aus einem Pool vorhandener Instanzen abgerufen.
Am Ende jedes Messdurchlaufs wird jedes vorhandene, realisierte Element, das nicht als "verwendet" gekennzeichnet wurde, automatisch für die erneute Verwendung verfügbar betrachtet, es sei denn, die Option " SuppressAutoRecycle " wurde verwendet, als das Element über die GetOrCreateElementAt-Methode abgerufen wurde. Das Framework verschiebt es automatisch in einen Papierkorb und macht es verfügbar. Es kann anschließend für die Verwendung durch einen anderen Container abgerufen werden. Das Framework versucht, dies nach Möglichkeit zu vermeiden, da es einige Kosten gibt, die dem erneuten Übergeordneten eines Elements zugeordnet sind.
Wenn ein Virtualisierungslayout am Anfang jeder Messung weiß, welche Elemente nicht mehr in das Realisierungsrecht fallen, kann es seine Wiederverwendung optimieren. Anstatt sich auf das Standardverhalten des Frameworks zu verlassen. Das Layout kann Elemente vorab mithilfe der RecycleElement-Methode in den Papierkorb verschieben. Das Aufrufen dieser Methode vor dem Anfordern neuer Elemente bewirkt, dass diese vorhandenen Elemente verfügbar sind, wenn das Layout später eine GetOrCreateElementAt-Anforderung für einen Index ausgibt, der noch nicht einem Element zugeordnet ist.
Der VirtualizingLayoutContext stellt zwei zusätzliche Eigenschaften bereit, die für Layoutautoren entwickelt wurden, die ein inhaltsabhängiges Layout erstellen. Sie werden später ausführlicher erörtert.
- Ein RecommendedAnchorIndex , der eine optionale Eingabe für das Layout bereitstellt.
- Ein LayoutOrigin , das eine optionale Ausgabe des Layouts ist.
Datenabhängige Virtualisierungslayouts
Ein Virtualisierungslayout ist einfacher, wenn Sie wissen, wie groß jedes Elements sein soll, ohne den anzuzeigenden Inhalt messen zu müssen. In diesem Dokument beziehen wir uns einfach auf diese Kategorie der Virtualisierung von Layouts als Datenlayouts , da sie in der Regel das Überprüfen der Daten umfassen. Basierend auf den Daten kann eine App eine visuelle Darstellung mit einer bekannten Größe auswählen – vielleicht weil ihr Teil der Daten oder zuvor vom Entwurf bestimmt wurde.
Der allgemeine Ansatz richtet sich an folgendes Layout:
- Berechnen sie eine Größe und Position jedes Elements.
- Im Rahmen der MeasureOverride:
- Verwenden Sie die RealisationRect , um zu bestimmen, welche Elemente innerhalb des Viewports angezeigt werden sollen.
- Rufen Sie das UIElement ab, das das Element mit der GetOrCreateElementAt-Methode darstellen soll.
- Messen Sie das UIElement mit der vordefinierten Größe.
- Ordnen Sie im Rahmen der ArrangeOverride jedes realisierte UIElement mit der vorberechnenen Position an.
Hinweis
Ein Datenlayoutansatz ist häufig mit der Datenvirtualisierung nicht kompatibel. Insbesondere, wenn die einzigen Daten, die in den Arbeitsspeicher geladen wurden, sind die Daten, die zum Ausfüllen der für den Benutzer sichtbaren Daten erforderlich sind. Die Datenvirtualisierung bezieht sich nicht auf faule oder inkrementelles Laden von Daten, da ein Benutzer nach unten scrollt, wo diese Daten verbleiben. Stattdessen wird darauf verwiesen, wann Elemente aus dem Speicher freigegeben werden, während sie aus der Ansicht gescrollt werden. Wenn Sie ein Datenlayout haben, das jedes Datenelement als Teil eines Datenlayouts prüft, würde die Datenvirtualisierung nicht wie erwartet funktionieren. Eine Ausnahme ist ein Layout wie das UniformGridLayout, das davon ausgeht, dass alles dieselbe Größe aufweist.
Tipp
Wenn Sie ein benutzerdefiniertes Steuerelement für eine Steuerelementbibliothek erstellen, das von anderen Benutzern in einer Vielzahl von Situationen verwendet wird, ist ein Datenlayout möglicherweise keine Option für Sie.
Beispiel: Layout des Xbox-Aktivitätsfeeds
Die Benutzeroberfläche für den Xbox-Aktivitätsfeed verwendet ein wiederholtes Muster, bei dem jede Zeile eine breite Kachel aufweist, gefolgt von zwei schmalen Kacheln, die in der nachfolgenden Zeile umgekehrt sind. In diesem Layout ist die Größe für jedes Element eine Funktion der Position des Elements im Dataset und die bekannte Größe für die Kacheln (breit oder schmal).
Der folgende Code führt durch, was eine benutzerdefinierte Virtualisierungs-UI für den Aktivitätsfeed sein könnte, um den allgemeinen Ansatz zu veranschaulichen, den Sie für ein Datenlayout verwenden können.
Tipp
Wenn Sie die WinUI 3 Gallery-App installiert haben, klicken Sie hier, um die App zu öffnen und den ItemsRepeater in Aktion zu sehen. Rufen Sie die App aus dem Microsoft Store ab, oder rufen Sie den Quellcode auf GitHub ab.
Implementation
/// <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;
}
(Optional) Verwalten der Element-zu-UIElement-Zuordnung
Standardmäßig verwaltet der VirtualizingLayoutContext eine Zuordnung zwischen den realisierten Elementen und dem Index in der Datenquelle, die sie darstellen. Ein Layout kann sich dafür entscheiden, diese Zuordnung selbst zu verwalten, indem immer die Option " SuppressAutoRecycle" angefordert wird, wenn ein Element über die GetOrCreateElementAt-Methode abgerufen wird, wodurch das standardmäßige Verhalten der automatischen Wiederverwendung verhindert wird. Ein Layout kann dies z. B. tun, wenn es nur verwendet wird, wenn der Bildlauf auf eine Richtung beschränkt ist, und die elemente, die er betrachtet, immer zusammenhängend sind (d. h. das Wissen des Index des ersten und letzten Elements reicht aus, um alle Elemente zu kennen, die realisiert werden sollten).
Beispiel: Xbox-Aktivitätsfeed-Measure
Der folgende Codeausschnitt zeigt die zusätzliche Logik, die der MeasureOverride im vorherigen Beispiel hinzugefügt werden kann, um die Zuordnung zu verwalten.
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; }
// ...
}
Inhaltsabhängige Virtualisierungslayouts
Wenn Sie zuerst den UI-Inhalt für ein Element messen müssen, um dessen genaue Größe zu ermitteln, handelt es sich um ein inhaltsabhängiges Layout. Sie können es auch als Layout betrachten, in dem sich jedes Element selbst anpassen muss, anstatt das Layout, das dem Element seine Größe mitteilt, zu ändern. Das Virtualisieren von Layouts, die in diese Kategorie fallen, sind stärker beteiligt.
Hinweis
Inhaltsabhängige Layouts unterbrechen die Datenvirtualisierung nicht (sollte nicht).
Schätzungen
Inhaltsabhängige Layouts basieren auf der Schätzung, um sowohl die Größe von nicht realisierten Inhalten als auch die Position des realisierten Inhalts zu erraten. Wenn sich diese Schätzungen ändern, bewirkt dies, dass der realisierte Inhalt regelmäßig Positionen innerhalb des bildlauffähigen Bereichs verschiebt. Dies kann zu einer sehr frustrierenden und besorgniserregenden Benutzererfahrung führen, wenn sie nicht entschärft wird. Die potenziellen Probleme und Abhilfemaßnahmen werden hier erörtert.
Hinweis
Datenlayouts, die jedes Element berücksichtigen und die genaue Größe aller Elemente kennen, realisiert oder nicht, und ihre Positionen können diese Probleme vollständig vermeiden.
Bildlaufankerung
XAML bietet einen Mechanismus, um plötzliche Viewportverschiebungen zu minimieren, indem Bildlaufsteuerelemente die Bildlaufankerung unterstützen, indem die IScrollAnchorPovider-Schnittstelle implementiert wird. Während der Benutzer den Inhalt bearbeitet, wählt das Bildlaufsteuerelement kontinuierlich ein Element aus der Gruppe der Kandidaten aus, die sich für die Nachverfolgung entschieden haben. Wenn sich die Position des Ankerelements während des Layouts verschiebt, verschiebt das Bildlaufsteuerelement den Viewport automatisch, um den Viewport beizubehalten.
Der Wert des recommendedAnchorIndex-Elements , das für das Layout bereitgestellt wird, kann das aktuell ausgewählte Ankerelement widerspiegeln, das vom Bildlaufsteuerelement ausgewählt wurde. Wenn ein Entwickler explizit anfordert, dass ein Element für einen Index mit der GetOrCreateElement-Methode für itemsRepeater realisiert wird, wird dieser Index als RecommendedAnchorIndex für den nächsten Layoutdurchlauf angegeben. Auf diese Weise kann das Layout für das wahrscheinliche Szenario vorbereitet werden, das ein Entwickler ein Element erkennt und anschließend fordert, dass es über die StartBringIntoView-Methode in den Blick genommen wird.
Der RecommendedAnchorIndex ist der Index für das Element in der Datenquelle, den ein inhaltsabhängiges Layout zuerst positionieren sollte, wenn die Position der Elemente geschätzt wird. Es sollte als Ausgangspunkt für die Positionierung anderer realisierter Elemente dienen.
Auswirkungen auf ScrollBars
Auch bei der Bildlaufankerung, wenn die Schätzungen des Layouts sehr unterschiedlich sind, möglicherweise aufgrund erheblicher Variationen der Größe des Inhalts, kann die Position des Daumens für die Bildlaufleiste möglicherweise herumspringen. Dies kann für einen Benutzer störend sein, wenn der Daumen nicht angezeigt wird, um die Position des Mauszeigers beim Ziehen nachzuverfolgen.
Je genauer das Layout in seinen Schätzungen sein kann, desto weniger wahrscheinlich wird ein Benutzer den Daumensprung der ScrollBar sehen.
Layoutkorrekturen
Ein inhaltsabhängiges Layout sollte bereit sein, seine Schätzung mit Realität zu rationalisieren. Wenn der Benutzer beispielsweise zum Anfang des Inhalts scrollt und das Layout das erste Element erkennt, kann es feststellen, dass die erwartete Position des Elements relativ zum Element, von dem es gestartet wurde, an einer anderen Stelle als dem Ursprung von (x:0, y:0) angezeigt wird. In diesem Fall kann das Layout die LayoutOrigin-Eigenschaft verwenden, um die Position festzulegen, die als neuer Layoutursprung berechnet wird. Das Nettoergebnis ähnelt der Bildlaufankerung, in der der Viewport des Bildlaufsteuerelements automatisch an die Position des Inhalts angepasst wird, wie vom Layout angegeben.
Getrennte Viewports
Die größe, die von der MeasureOverride-Methode des Layouts zurückgegeben wird, stellt die beste Vermutung bei der Größe des Inhalts dar, die sich bei jedem aufeinander folgenden Layout ändern kann. Wenn ein Benutzer scrollt, wird das Layout kontinuierlich mit einer aktualisierten RealisationRect neu ausgewertet.
Wenn ein Benutzer den Daumen sehr schnell zieht, ist es für den Viewport möglich, aus der Perspektive des Layouts, große Sprünge zu machen, bei denen die vorherige Position die aktuelle Position nicht überlappt. Dies liegt an der asynchronen Art des Bildlaufs. Es ist auch möglich, dass eine App, die das Layout verwendet, um anzufordern, dass ein Element für ein Element angezeigt wird, das derzeit nicht erkannt wird und geschätzt wird, dass er außerhalb des aktuellen Bereichs liegt, der vom Layout nachverfolgt wird.
Wenn das Layout erkennt, dass es falsch ist und/oder eine unerwartete Viewportverschiebung sieht, muss es seine Ausgangsposition neu ausrichten. Die virtualisierenden Layouts, die als Teil der XAML-Steuerelemente bereitgestellt werden, werden als inhaltsabhängige Layouts entwickelt, da sie weniger Einschränkungen hinsichtlich der Art des anzuzeigenden Inhalts enthalten.
Beispiel: Einfaches Virtualisieren des Stapellayouts für Variable-Sized Elemente
Im folgenden Beispiel wird ein einfaches Stapellayout für Elemente mit variabler Größe veranschaulicht, die:
- unterstützt UI-Virtualisierung,
- verwendet Schätzungen, um die Größe von nicht erfolgreichen Elementen zu erraten,
- ist sich der potenziellen diskontinuierlichen Viewportverschiebungen bewusst und
- wendet Layoutkorrekturen an, um diese Schichten zu berücksichtigen.
Verwendung: 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;
Code: 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);
}
}
Verwandte Artikel
Windows developer