Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Een container (bijvoorbeeld Panel) die de indelingslogica aan een ander object delegeert, is afhankelijk van het gekoppelde indelingsobject om het gedrag van de indeling voor de onderliggende elementen te bieden. Een gekoppeld indelingsmodel biedt flexibiliteit voor een toepassing om de indeling van items tijdens runtime te wijzigen of delen eenvoudiger aspecten van de indeling tussen verschillende onderdelen van de gebruikersinterface (bijvoorbeeld items in de rijen van een tabel die in een kolom lijken te worden uitgelijnd).
In dit onderwerp bespreken we wat er betrokken is bij het maken van een gekoppelde indeling (virtualiseren en niet-virtualiseren), de concepten en klassen die u moet begrijpen, en de afwegingen die u moet overwegen bij het kiezen tussen deze concepten.
| Windows App SDK |
|---|
| De indelingsklassen die hier worden beschreven, maken deel uit van WinUI 3, opgenomen in de Windows App SDK. Zie Windows App SDK overzicht voor meer informatie. |
Belangrijke API's:
Sleutelbegrippen
Voor het uitvoeren van de indeling moeten twee vragen worden beantwoord voor elk element:
Hoe groot zal dit element zijn?
Wat is de positie van dit element?
Het lay-outsysteem van XAML, dat deze vragen beantwoordt, wordt kort behandeld als onderdeel van de bespreking van aangepaste panelen.
Containers en context
Conceptueel gezien vult het deelvenster van XAML twee belangrijke rollen in het framework:
- Het kan onderliggende elementen bevatten, en introduceert vertakkingen in de elementenboom.
- Er wordt een specifieke indelingsstrategie toegepast op deze kinderen.
Daarom is een paneel in XAML vaak synoniem voor lay-out, maar technisch gesproken, doet meer dan alleen indeling.
De ItemsRepeater gedraagt zich ook als Panel, maar, in tegenstelling tot Panel, heeft het geen Children-eigenschap waarmee je programmatisch UIElement-kinderen kunt toevoegen of verwijderen. In plaats daarvan wordt de levensduur van zijn kinderen automatisch beheerd door het framework om aan te sluiten bij een verzameling gegevensitems. Hoewel het niet is afgeleid van Panel, gedraagt het zich en wordt het behandeld door het framework als een Panel.
Opmerking
LayoutPanel is een container, afgeleid van Panel, die zijn logica delegeert aan het gekoppelde Layout-object. LayoutPanel is in Preview en is momenteel alleen beschikbaar in de Prerelease versies van het WinUI 3-pakket.
Containers
Conceptueel is panel een container met elementen die ook de mogelijkheid heeft om pixels voor een achtergrond weer te geven. Panelen bieden een manier om algemene lay-outlogica in te kapselen in een eenvoudig te gebruiken pakket.
Het concept van gekoppelde indeling maakt het onderscheid tussen de twee rollen van container en indeling duidelijker. Als de container de indelingslogica aan een ander object delegeert, wordt dat object de bijgevoegde indeling aangeroepen, zoals te zien is in het onderstaande fragment. Containers die overnemen van FrameworkElement, zoals LayoutPanel, maken automatisch de algemene eigenschappen beschikbaar die invoer bieden voor het indelingsproces van XAML (bijvoorbeeld Hoogte en Breedte).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Tijdens het indelingsproces is de container afhankelijk van de gekoppelde UniformGridLayout om de onderliggende items te meten en te rangschikken.
Per-Container staat
Met een gekoppelde indeling kan één exemplaar van het indelingsobject worden gekoppeld aan veel containers, zoals in het onderstaande fragment; Daarom mag deze niet afhankelijk zijn van of rechtstreeks verwijzen naar de hostcontainer. Voorbeeld:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
</Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
Voor deze situatie moet ExampleLayout zorgvuldig rekening houden met de status die wordt gebruikt in de indelingsberekening en waar die status wordt opgeslagen om te voorkomen dat de indeling voor elementen in het ene deelvenster met het andere wordt beïnvloed. Het zou vergelijkbaar zijn met een aangepast paneel waarvan de logica voor MeasureOverride en ArrangeOverride afhankelijk is van de waarden van zijn statische eigenschappen.
LayoutContext
Het doel van layoutcontext is om deze uitdagingen aan te pakken. Het biedt de bijgevoegde indeling de mogelijkheid om te communiceren met de hostcontainer, zoals het ophalen van subelementen, zonder dat er een directe afhankelijkheid tussen de twee wordt ingevoerd. Met de context kan de indeling ook elke status opslaan die wellicht verband houdt met de kindelementen van de container.
Eenvoudige, niet-virtualiserende indelingen hoeven vaak geen status te onderhouden, waardoor het een niet-probleem is. Een complexere indeling, zoals Grid, kan er echter voor kiezen om de toestand te behouden tussen het meten en het rangschikken om te voorkomen dat een waarde opnieuw wordt berekend.
Bij het virtualiseren van lay-outs moet er vaak een bepaalde status behouden worden, zowel tijdens het meten en rangschikken alsook tussen de iteratieve lay-outpasses.
Per-containerstatus initialiseren en de-initialiseren
Wanneer een indeling is gekoppeld aan een container, wordt de initializeForContextCore-methode aangeroepen en biedt deze de mogelijkheid om een object te initialiseren om de status op te slaan.
Op dezelfde manier wordt de methode UninitializeForContextCore aangeroepen wanneer de indeling wordt verwijderd uit een container. Dit geeft de indeling de mogelijkheid om alle statussen op te schonen die aan die container zijn gekoppeld.
Het statusobject van de lay-out kan met de eigenschap LayoutState in de context worden opgeslagen in en opgehaald uit de container.
UI-virtualisatie
Ui-virtualisatie betekent dat het maken van een UI-object wordt vertraagd totdat het nodig is. Het is een optimalisatie van prestaties. Voor niet-scrollende scenario's die bepalen wanneer dat nodig is, kunnen worden gebaseerd op een willekeurig aantal dingen die specifiek zijn voor apps. In die gevallen moeten apps overwegen om x :Load te gebruiken. Er zijn geen bijzondere handelingen in uw lay-out vereist.
In scenario's waarin wordt gescrolld, zoals een lijst, is het bepalen wanneer dat nodig is vaak gebaseerd op de vraag of het zichtbaar is voor een gebruiker, wat sterk afhankelijk is van waar het is geplaatst tijdens het lay-outproces en speciale overwegingen vereist. Dit scenario is een focus voor dit document.
Opmerking
Hoewel dit document niet wordt behandeld, kunnen dezelfde mogelijkheden waarmee ui-virtualisatie in schuifscenario's mogelijk zijn, worden toegepast in scenario's zonder scrolling. Een besturingselement op basis van gegevensgestuurde werkbalk dat de levensduur van de opdrachten beheert die het presenteert en reageert op wijzigingen in de beschikbare ruimte door elementen te recyclen/verplaatsen tussen een zichtbaar gebied en een overloopmenu.
Aan de slag komen
Bepaal eerst of de indeling die u moet maken ondersteuning biedt voor ui-virtualisatie.
Een paar dingen om rekening mee te houden...
- Indelingen die niet worden gevirtualiseerd, zijn gemakkelijker te ontwerpen. Als het aantal items altijd klein is, wordt het ontwerpen van een niet-gevirtualiseerde indeling aanbevolen.
- Het platform biedt een set gekoppelde indelingen die werken met de ItemsRepeater en LayoutPanel om te voldoen aan algemene behoeften. Maak uzelf vertrouwd met deze voordat u besluit een aangepaste indeling te definiëren.
- Het virtualiseren van indelingen heeft altijd wat extra CPU- en geheugenkosten/complexiteit/overhead vergeleken met een indeling die niet wordt gevirtualiseerd. Als vuistregel geldt dat als de elementen die door de indeling beheerd moeten worden waarschijnlijk in een gebied passen dat drie keer zo groot is als de viewport, er mogelijk niet veel voordeel is te behalen uit een virtueel gevirtualiseerde indeling. De 3x-grootte wordt verderop in dit document uitvoeriger besproken, maar is te wijten aan de asynchrone aard van het schuiven op Windows en de impact ervan op virtualisatie.
Aanbeveling
Als referentiepunt zijn de standaardinstellingen voor ListView (en ItemsRepeater) dat recycling pas begint als het aantal items voldoende is om 3x de grootte van de huidige viewport te vullen.
Kies uw basistype
Het basisindelingstype heeft twee afgeleide typen die fungeren als beginpunt voor het ontwerpen van een bijgevoegde indeling:
Niet-virtualiserende indeling
De aanpak voor het maken van een niet-virtualiserende indeling moet vertrouwd zijn voor iedereen die een Custom Panel heeft gemaakt. Dezelfde concepten zijn van toepassing. Het belangrijkste verschil is dat een NonVirtualizingLayoutContext wordt gebruikt voor toegang tot de Children-collectie en de indeling kan ervoor kiezen om de status op te slaan.
- Afgeleid van het basistype NonVirtualizingLayout (in plaats van panel).
- (Optioneel) Definieer afhankelijkheidseigenschappen die de indeling ongeldig maken wanneer deze wordt gewijzigd.
- (Nieuw/optioneel) Initialiseer een statusobject dat is vereist voor de indeling als onderdeel van initializeForContextCore. Berg het op bij de hostcontainer met behulp van de LayoutState die is geleverd met de context.
- Overschrijf de MeasureOverride en roep de Measure methode aan voor alle kind-elementen.
- Overschrijf ArrangeOverride en roep de Arrange methode aan voor alle kinderen.
- (Nieuw/optioneel) Schoon alle opgeslagen status op als onderdeel van de UninitializeForContextCore.
Voorbeeld: Een Eenvoudige Stack-Indeling (items van verschillende grootte)
Hier is een zeer eenvoudige niet-virtualiserende stapelindeling van items van verschillende groottes. Er ontbreken eigenschappen om het gedrag van de indeling aan te passen. In de onderstaande implementatie ziet u hoe de indeling afhankelijk is van het contextobject dat door de container wordt geleverd voor:
- Verkrijg het aantal kinderen en
- Verzeker toegang tot elk kind-element via index.
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>
Indelingen virtualiseren
Net als bij een indeling die niet wordt gevirtualiseerd, zijn de stappen op hoog niveau voor een virtualisatieindeling hetzelfde. De complexiteit is grotendeels in het bepalen welke elementen binnen de viewport vallen en moeten worden gerealiseerd.
- Afgeleid van het basistype VirtualizingLayout.
- (Optioneel) Definieer uw afhankelijkheidseigenschappen die wanneer deze worden gewijzigd, de indeling ongeldig maken.
- Initialiseer een statusobject dat vereist is voor de indeling als onderdeel van initializeForContextCore. Berg het op bij de hostcontainer met behulp van de LayoutState die is geleverd met de context.
- Overschrijf de MeasureOverride en roep de Measure-methode aan voor elk kind dat moet worden gerealiseerd.
- De methode GetOrCreateElementAt wordt gebruikt om een UIElement op te halen dat is voorbereid door het framework (bijvoorbeeld toegepaste gegevensbindingen).
- Overschrijf de ArrangeOverride en roep de methode Rangschikken aan voor elk gerealiseerd kind.
- (Optioneel) Verwijder de opgeslagen staat als onderdeel van UninitializeForContextCore.
Aanbeveling
De waarde die door measureOverride wordt geretourneerd, wordt gebruikt als de grootte van de gevirtualiseerde inhoud.
Er zijn twee algemene benaderingen waarmee u rekening moet houden bij het ontwerpen van een indeling voor virtualisatie. Of u een of de andere kiest, hangt grotendeels af van 'hoe bepaalt u de grootte van een element'. Als het voldoende is om de index van een item in de gegevensset te kennen of als de gegevens zelf de uiteindelijke grootte bepalen, wordt deze gegevensafhankelijk gezien. Deze zijn eenvoudiger te maken. Als de enige manier om de grootte van een item te bepalen is door de UI te maken en meten, dan zeggen we dat het inhoudsafhankelijk is. Deze zijn complexer.
Het indelingsproces
Of u nu een gegevens- of inhoudsafhankelijke indeling maakt, het is belangrijk om inzicht te hebben in het indelingsproces en de impact van asynchrone scrolling van Windows.
Een (over)vereenvoudigde weergave van de stappen die door het framework worden uitgevoerd vanaf het opstarten tot het weergeven van de gebruikersinterface op het scherm is dat:
De markering wordt geparseerd.
Er wordt een boom van elementen gegenereerd.
Voert een layout-bewerking uit.
Voert een render-pass uit.
Met UI-virtualisatie wordt het maken van de elementen die normaal gesproken in stap 2 worden uitgevoerd, vertraagd of beëindigd zodra is vastgesteld dat er voldoende inhoud is gemaakt om de viewport te vullen. Een virtualisatiecontainer (bijvoorbeeld ItemsRepeater) maakt gebruik van de gekoppelde indeling om dit proces te sturen. Het biedt de gekoppelde indeling met een VirtualizingLayoutContext die de aanvullende informatie weergeeft die een indeling voor virtualiseren nodig heeft.
De RealizationRect (dat wil zeggen, Viewport)
Scrollen op Windows gebeurt asynchroon van de UI-thread. Het wordt niet beheerd door de indeling van het framework. In plaats daarvan vindt de interactie en beweging plaats in de compositor van het systeem. Het voordeel van deze aanpak is dat panning-inhoud altijd bij 60fps kan worden uitgevoerd. De uitdaging is echter dat de 'viewport', zoals gezien door de indeling, enigszins verouderd kan zijn ten opzichte van wat er daadwerkelijk op het scherm zichtbaar is. Als een gebruiker snel scrolt, kan hij de snelheid van de UI-thread overtreffen om nieuwe inhoud te genereren en naar zwart te verschuiven. Daarom is het vaak nodig om een indeling te virtualiseren om een extra buffer van voorbereide elementen te genereren die voldoende zijn om een gebied groter dan de viewport te vullen. Wanneer de gebruiker onder zwaardere belasting bij het scrollen nog steeds inhoud te zien krijgt.
Aangezien het maken van elementen kostbaar is, biedt het virtualiseren van containers (bijvoorbeeld ItemsRepeater) in eerste instantie de bijgevoegde indeling met een RealizationRect die overeenkomt met de viewport. Na inactieve tijd kan de container de buffer van voorbereide inhoud vergroten door herhaalde aanroepen naar de layout te maken met behulp van een steeds groter wordende realisatie-rect. Dit gedrag is een optimalisatie van prestaties die een balans probeert te vinden tussen een snelle opstarttijd en een goede panning-ervaring. De maximale buffergrootte die door de ItemsRepeater wordt gegenereerd, wordt bepaald door de eigenschappen VerticalCacheLength en HorizontalCacheLength .
Elementen opnieuw gebruiken (recycling)
De indeling moet de grootte en positie van de elementen aanpassen om de RealizationRect te vullen, elke keer dat het wordt uitgevoerd. Standaard recyclet virtualizingLayout alle ongebruikte elementen aan het einde van elke indelingspas.
VirtualizingLayoutContext die als onderdeel van de MeasureOverride en ArrangeOverride wordt doorgegeven aan de indeling, biedt de aanvullende informatie die nodig is voor het virtualiseren van de indeling. Enkele van de meest gebruikte dingen die het biedt, zijn de mogelijkheid om:
- Voer een query uit op het aantal items in de gegevens (ItemCount).
- Een specifiek item ophalen met behulp van de methode GetItemAt .
- Haal een RealizationRect op die de viewport en buffer vertegenwoordigt die de lay-out moet vullen met weergaveklare elementen.
- Vraag het UIElement aan voor een specifiek item met de methode GetOrCreateElementAt .
Als u een element voor een bepaalde index aanvraagt, wordt dat element gemarkeerd als 'in gebruik' voor die pas van de indeling. Als het element nog niet bestaat, wordt het gerealiseerd en automatisch voorbereid voor gebruik (bijvoorbeeld het opblazen van de UI-structuur die is gedefinieerd in een DataTemplate, het verwerken van gegevensbindingen, enzovoort). Anders wordt deze uit een pool van bestaande exemplaren opgehaald.
Aan het einde van elke metingspas wordt elk bestaand, gerealiseerd element dat niet als 'in gebruik' is gemarkeerd, automatisch als beschikbaar beschouwd voor hergebruik, tenzij de optie voor SuppressAutoRecycle werd gebruikt toen het element werd opgehaald via de methode GetOrCreateElementAt . Het framework verplaatst het automatisch naar een prullenbak en maakt het beschikbaar. Het kan vervolgens worden opgehaald voor gebruik door een andere container. Het framework probeert dit zo mogelijk te voorkomen, omdat er enkele kosten zijn verbonden aan het opnieuw koppelen van een element.
Als een virtualisatie-indeling aan het begin van elke meting weet welke elementen niet meer binnen het realisatie-rect vallen, kan het hergebruik ervan worden geoptimaliseerd. In plaats van te vertrouwen op het standaardgedrag van het framework. De indeling kan elementen met de methode RecycleElement preventief naar de prullenbak verplaatsen. Als u deze methode aanroept voordat nieuwe elementen worden aangevraagd, worden deze bestaande elementen beschikbaar wanneer de indeling later een GetOrCreateElementAt-aanvraag voor een index uitgeeft die nog niet is gekoppeld aan een element.
VirtualizingLayoutContext biedt twee extra eigenschappen die zijn ontworpen voor auteurs van indelingen die een inhoudsafhankelijke indeling maken. Ze worden later uitvoeriger besproken.
- Een RecommendedAnchorIndex die een optionele invoer voor de indeling biedt.
- Een LayoutOrigin die een optionele uitvoer van de indeling is.
Indelingen voor gegevensafhankelijke virtualisatie
Een virtualisatie-indeling is eenvoudiger als u weet wat de grootte van elk item moet zijn zonder dat u de inhoud hoeft te meten om weer te geven. In dit document verwijzen we gewoon naar deze categorie van het virtualiseren van indelingen als gegevensindelingen , omdat ze meestal betrekking hebben op het inspecteren van de gegevens. Op basis van de gegevens kan een app een visuele weergave met een bekende grootte kiezen, mogelijk omdat het deel van de gegevens of eerder door het ontwerp is bepaald.
De algemene benadering is bedoeld om het volgende te doen:
- Bereken een grootte en positie van elk item.
- Als onderdeel van measureOverride:
- Gebruik de RealizationRect om te bepalen welke items moeten worden weergegeven in de viewport.
- Haal het UIElement op dat het item moet vertegenwoordigen met de methode GetOrCreateElementAt .
- Meet het UIElement met de vooraf berekende grootte.
- Als onderdeel van ArrangeOverriderangschikt u elk gerealiseerd UIElement met de vooraf berekende positie.
Opmerking
Een benadering van gegevensindeling is vaak niet compatibel met gegevensvirtualisatie. Specifiek wanneer alleen die gegevens in het geheugen worden geladen die nodig zijn om in te vullen wat zichtbaar is voor de gebruiker. Gegevensvirtualisatie verwijst niet naar vertraagd of incrementeel laden van gegevens wanneer een gebruiker naar beneden scrollt terwijl die gegevens op hun plaats blijven. In plaats daarvan verwijst het naar wanneer items uit het geheugen worden vrijgegeven wanneer ze uit de weergave worden geschoven. Als u een gegevensindeling hebt die elk gegevensitem inspecteert als onderdeel van een gegevensindeling, kan gegevensvirtualisatie niet werken zoals verwacht. Een uitzondering is een indeling zoals de UniformGridLayout die ervan uitgaat dat alles dezelfde grootte heeft.
Aanbeveling
Als u een aangepast besturingselement maakt voor een besturingselementbibliotheek die door anderen in een groot aantal situaties wordt gebruikt, is een gegevensindeling mogelijk geen optie voor u.
Voorbeeld: indeling van Xbox activiteitsfeed
De gebruikersinterface voor de Xbox-activiteitsfeed maakt gebruik van een herhalend patroon waarbij elke regel een brede tegel heeft, gevolgd door twee smalle tegels die op de volgende regel worden omgekeerd. In deze indeling is de grootte voor elk item een functie van de positie van het item in de gegevensset en de bekende grootte voor de tegels (breed versus smal).
In de onderstaande code wordt uitgelegd wat een aangepaste virtualisatiegebruikersinterface voor de activiteitsfeed kan zijn om de algemene benadering te illustreren die u kunt gebruiken voor een gegevensindeling.
Aanbeveling
Als u de WinUI 3 Gallery-app hebt geïnstalleerd, klikt u hier om de app te openen en de ItemsRepeater in actie te zien. Haal de app op uit de Microsoft Store of haal de broncode op GitHub op.
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;
}
(Optioneel) Toewijzing van het item aan UIElement beheren
VirtualizingLayoutContext onderhoudt standaard een toewijzing tussen de gerealiseerde elementen en de index in de gegevensbron die ze vertegenwoordigen. Een indeling kan ervoor kiezen om deze toewijzing zelf te beheren door bij het ophalen van een element via de methode GetOrCreateElementAt altijd de optie SuppressAutoRecycle aan te vragen, waardoor het standaardgedrag voor automatisch recyclen wordt voorkomen. Een indeling kan ervoor kiezen om dit te doen, bijvoorbeeld als deze alleen wordt gebruikt wanneer schuiven wordt beperkt tot één richting en de items die worden beschouwd, altijd aaneengesloten zijn (dat wil zeggen dat de index van het eerste en laatste element voldoende is om alle elementen te weten die moeten worden gerealiseerd).
Voorbeeld: meting Xbox-activiteitsfeed
In het onderstaande fragment ziet u de aanvullende logica die zou kunnen worden toegevoegd aan MeasureOverride in het vorige voorbeeld om de mapping te beheren.
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.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; }
// ...
}
Indelingen voor inhoudsafhankelijke virtualisatie
Als u eerst de ui-inhoud voor een item moet meten om de exacte grootte ervan te bepalen, is het een inhoudsafhankelijke indeling. U kunt het ook beschouwen als een indeling waarin elk item de grootte moet aanpassen in plaats van de indeling die het item de grootte aangeeft. Het virtualiseren van indelingen die in deze categorie vallen, is meer betrokken.
Opmerking
Inhoudsafhankelijke indelingen zouden gegevensvirtualisatie niet moeten verstoren.
Schattingen
Inhoudsafhankelijke indelingen zijn afhankelijk van schattingen om zowel de grootte van niet-geordend inhoud als de positie van de gerealiseerde inhoud te raden. Naarmate deze schattingen veranderen, zorgt dit ervoor dat de gerealiseerde inhoud regelmatig posities verplaatst binnen het schuifbare gebied. Dit kan leiden tot een zeer frustrerende en vervelende gebruikerservaring als deze niet wordt verzacht. De mogelijke problemen en oplossingen worden hier besproken.
Opmerking
Gegevensindelingen die elk item beschouwen en de exacte grootte van alle items kennen, gerealiseerd of niet, en hun posities kunnen deze problemen volledig voorkomen.
Scroll Anchoring
XAML biedt een mechanisme om plotselinge verschuivingen van de viewport te beperken door schuifbesturingselementen scrollverankering te laten ondersteunen via implementatie van de interface IScrollAnchorProvider. Terwijl de gebruiker de inhoud bewerkt, selecteert het schuifbesturingselement voortdurend een element uit de set kandidaten die zijn aangemeld om te worden bijgehouden. Als de positie van het ankerelement verschuift tijdens de lay-out, verschuift de scrollbesturing automatisch de viewport om deze te behouden.
De waarde van de RecommendedAnchorIndex die aan de indeling is verstrekt, kan overeenkomen met het geselecteerde ankerelement dat is gekozen door het schuifbesturingselement. Als een ontwikkelaar expliciet verzoekt dat een element voor een index wordt aangemaakt met de methode GetOrCreateElement op de ItemsRepeater, dan wordt die index in de volgende lay-outpas gegeven als de RecommendedAnchorIndex. Hierdoor kan de lay-out worden voorbereid op het waarschijnlijke scenario dat een ontwikkelaar een element realiseert en vervolgens vraagt dat dit in beeld wordt gebracht via de methode StartBringIntoView .
De RecommendedAnchorIndex is de index voor het item in de gegevensbron dat een inhoudsafhankelijke indeling eerst moet positioneren bij het schatten van de positie van de items. Het moet fungeren als uitgangspunt voor het positioneren van andere gerealiseerde items.
Invloed op schuifbalken
Zelfs als de schattingen van de indeling veel verschillen, misschien vanwege aanzienlijke variaties in de grootte van de inhoud, kan de positie van de duim voor de schuifbalk lijken te springen. Dit kan verwarrend zijn voor een gebruiker als de duim de positie van de muisaanwijzer niet volgt wanneer deze wordt versleept.
Hoe nauwkeuriger de indeling kan zijn in de schattingen, hoe minder waarschijnlijk een gebruiker de duim van de schuifbalk ziet springen.
Indelingscorrecties
Een lay-out die afhankelijk is van de inhoud moet worden voorbereid om de schatting in overeenstemming te brengen met de realiteit. Als de gebruiker bijvoorbeeld naar de bovenkant van de inhoud schuift en de indeling het eerste element realiseert, kan het zijn dat de verwachte positie van het element ten opzichte van het element waaruit het is gestart, ertoe zou leiden dat het ergens anders wordt weergegeven dan de oorsprong van (x:0, y:0). Wanneer dit gebeurt, kan de indeling de eigenschap LayoutOrigin gebruiken om de positie in te stellen die wordt berekend als de nieuwe oorspronkelijke indeling. Het eindresultaat is vergelijkbaar met scrollverankering, waarbij de viewport van de scrolbesturing automatisch wordt aangepast om rekening te houden met de positie van de inhoud zoals door de lay-out wordt gerapporteerd.
Losgekoppelde viewports
De grootte die wordt geretourneerd door de MeasureOverride-methode van de indeling vertegenwoordigt de beste inschatting van de inhoud die bij elke opeenvolgende indeling kan veranderen. Wanneer een gebruiker door de indeling schuift, wordt de lay-out voortdurend opnieuw geëvalueerd met een bijgewerkte RealizationRect.
Als een gebruiker de duim zeer snel sleept, is het mogelijk voor de viewport, vanuit het perspectief van de indeling, om grote sprongen te maken waarbij de vorige positie de huidige positie niet overlapt. Dit komt door de asynchrone aard van schuiven. Het is ook mogelijk voor een app die de indeling gebruikt om aan te vragen dat een element in beeld wordt gebracht voor een item dat momenteel niet wordt gerealiseerd en naar schatting buiten het huidige bereik ligt dat wordt bijgehouden door de indeling.
Wanneer de indeling detecteert dat de schatting onjuist is en/of een onverwachte viewport-verschuiving ziet, moet de beginpositie opnieuw worden aangepast. De virtualisatie-indelingen die als onderdeel van de XAML-besturingselementen worden verzonden, worden ontwikkeld als inhoudsafhankelijke indelingen, omdat ze minder beperkingen opleggen aan de aard van de inhoud die wordt weergegeven.
Voorbeeld: Eenvoudige virtualisatiestackindeling voor variabel grote onderdelen
In het onderstaande voorbeeld ziet u een eenvoudige stack-indeling voor items met een variabele grootte die:
- ondersteunt UI-virtualisatie,
- maakt gebruik van schattingen om de grootte van niet-gerealiseerde items te raden,
- Is op de hoogte van mogelijke niet-aaneenlopende verschuivingen van de viewport, en
- past indelingscorrecties toe om rekening te houden met die verschuivingen.
Gebruik: Markeringen
<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);
}
}
Verwante artikelen
Windows developer