Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
En container (till exempel Panel) som delegerar layoutlogik till ett annat objekt förlitar sig på det bifogade layoutobjektet för att tillhandahålla layoutbeteendet för dess underordnade element. En bifogad layoutmodell ger flexibilitet för ett program att ändra layouten för objekt vid körning, eller enklare dela aspekter av layouten mellan olika delar av användargränssnittet (till exempel objekt i raderna i en tabell som verkar vara justerade i en kolumn).
I det här avsnittet går vi igenom vad som ingår i skapandet av en bifogad layout (virtualisering och icke-virtualisering), de begrepp och klasser som du behöver förstå och vilka kompromisser du måste tänka på när du bestämmer dig för dem.
| SDK för Windows-appar |
|---|
| Layoutklasserna som beskrivs här är en del av WinUI 3, som ingår i Windows App SDK. Mer information finns i Windows App SDK översikt. |
Viktiga API:er:
Viktiga begrepp
Att utföra layouten kräver att två frågor besvaras för varje element:
Vilken storlek kommer det här elementet att vara?
Vilken position kommer det här elementet att vara?
XAML:s layoutsystem, som besvarar dessa frågor, beskrivs kortfattat som en del av diskussionen om anpassade paneler.
Containrar och kontext
Konceptuellt fyller XAML:s panel två viktiga roller i ramverket:
- Den kan innehålla underordnade element och introducerar förgrening i elementträdet.
- Den tillämpar en specifik layoutstrategi för dessa element.
Därför har en panel i XAML ofta varit synonym med layout, men tekniskt sett gör mer än bara layout.
ItemsRepeater fungerar också som Panel, men till skillnad från Panel exponerar den inte någon Children-egenskap för att programmässigt kunna lägga till eller ta bort UIElement-element. I stället hanteras livslängden för dess underordnade automatiskt av ramverket för att motsvara en samling dataobjekt. Även om den inte härleds från panelen fungerar den och behandlas av ramverket som en panel.
Anmärkning
LayoutPanel är en container som härleds från panelen och som delegerar dess logik till det anslutna layoutobjektet. LayoutPanel är i förhandsversion och är för närvarande endast tillgängligt i Förhandsversioner av WinUI 3-paketet.
Containers
Konceptuellt är Panel en container med element som också har möjlighet att återge bildpunkter för en bakgrund. Paneler ger ett sätt att kapsla in vanlig layoutlogik i ett lättanvänt paket.
Begreppet bifogad layout gör skillnaden mellan de två rollerna container och layout tydligare. Om containern delegerar sin layoutlogik till ett annat objekt anropar vi objektet den bifogade layouten enligt beskrivningen nedan. Containrar som ärver från FrameworkElement, till exempel LayoutPanel, exponerar automatiskt de vanliga egenskaper som ger indata till XAML:s layoutprocess (till exempel Höjd och Bredd).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Under layoutprocessen förlitar sig containern på den anslutna UniformGridLayout för att mäta och ordna dess underordnade objekt.
Per-Container tillstånd
Med en bifogad layout kan en enda instans av layoutobjektet associeras med många containrar som i kodfragmentet nedan. Därför får den inte vara beroende av eller direkt referera till värdcontainern. Till exempel:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
</Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
För den här situationen måste ExampleLayout noggrant överväga tillståndet som används i layoutberäkningen och var det tillståndet lagras för att undvika att påverka layouten för element i en panel med den andra. Det skulle likna en anpassad panel vars MeasureOverride- och ArrangeOverride-logik är beroende av värdena för dess statiska egenskaper.
LayoutContext
Syftet med LayoutContext är att hantera dessa utmaningar. Det ger den bifogade layouten möjlighet att interagera med värdcontainern, till exempel för att hämta underordnade element, utan att skapa ett direkt beroende mellan dem. Kontexten gör det också möjligt för layouten att lagra alla tillstånd som krävs som kan vara relaterade till containerns underordnade element.
Enkla, icke-virtualiserande layouter behöver ofta inte behålla något tillstånd, vilket inte är ett problem. En mer komplex layout, som till exempel Grid-layout, kan dock välja att bibehålla sitt tillstånd mellan mätnings- och ordningsanropet för att undvika omberäkning av ett värde.
Virtualisering av layout måste ofta upprätthålla ett tillstånd både mellan uppmätning och arrangering samt mellan iterativa layoutpass.
Initiera och avinitiera Per-Container-tillstånd
När en layout är kopplad till en container anropas metoden InitializeForContextCore och ger en möjlighet att initiera ett objekt för lagringstillstånd.
På samma sätt anropas metoden UninitializeForContextCore när layouten tas bort från en container. Detta ger layouten en möjlighet att städa upp alla tillstånd som den har associerat med containern.
Layoutens tillståndsobjekt kan lagras med och hämtas från containern med egenskapen LayoutState i kontexten.
Virtualisering av användargränssnitt
Virtualisering av användargränssnitt innebär att fördröja skapandet av ett gränssnittsobjekt tills det behövs. Det är en prestandaoptimering. För icke-rullningsscenarier kan bestämande av när det behövs baseras på vilket antal som helst av appspecifika faktorer. I sådana fall bör appar överväga att använda x:Load. Det kräver ingen särskild hantering i layouten.
I rullningsbaserade scenarier, till exempel en lista, bestäms "när det behövs" ofta av om det kommer att vara synligt för en användare, vilket beror mycket på var det placerades under layoutprocessen och kräver särskilda överväganden. Det här scenariot är i fokus för det här dokumentet.
Anmärkning
Även om det inte beskrivs i det här dokumentet kan samma funktioner som möjliggör virtualisering av användargränssnittet i rullningsscenarier tillämpas i scenarier som inte rullar. Till exempel en datadriven ToolBar-kontroll som hanterar livslängden för de kommandon som visas och svarar på ändringar i tillgängligt utrymme genom att återanvända/flytta element mellan ett synligt område och en spillmeny.
Komma igång
Bestäm först om layouten som du behöver skapa ska ha stöd för virtualisering av användargränssnittet.
Några saker att tänka på...
- Layouter som inte virtualiserar är enklare att redigera. Om antalet objekt alltid är litet rekommenderar vi att du skapar en layout som inte är virtualiserande.
- Plattformen innehåller en uppsättning bifogade layouter som fungerar med ItemsRepeater och LayoutPanel för att täcka vanliga behov. Bekanta dig med dem innan du bestämmer dig för att definiera en anpassad layout.
- Virtualiseringslayouter har alltid ytterligare processor- och minneskostnader/komplexitet/omkostnader jämfört med en layout som inte är virtualiserande. En allmän riktlinje är att om de underordnade som layouten behöver hantera troligen kommer att passa i ett område som är tre gånger storleken på visningsporten, finns det kanske inte någon större vinst från en virtualiseringslayout. 3x-storleken beskrivs mer detaljerat senare i det här dokumentet, men beror på den asynkrona rullningen på Windows och dess inverkan på virtualisering.
Tips/Råd
Som en referenspunkt är standardinställningarna för ListView (och ItemsRepeater) att återvinningen inte börjar förrän antalet objekt är tillräckligt för att fylla upp 3x storleken på den aktuella vyporten.
Välj bastyp
Baslayouttypen har två härledda typer som fungerar som startpunkt för redigering av en bifogad layout:
Layout som inte virtualiserar
Metoden för att skapa en layout som inte är virtualiserande bör kännas bekant för alla som har skapat en anpassad panel. Samma begrepp gäller. Den primära skillnaden är att en NonVirtualizingLayoutContext används för att komma åt 'Children'-samlingen, och layouten kan välja att lagra tillstånd.
- Härled från bastypen NonVirtualizingLayout istället för Panel.
- (Valfritt) Definiera beroendeegenskaper som när den ändras ogiltigförklarar layouten.
- (Ny/valfritt) Initiera alla tillståndsobjekt som krävs av layouten som en del av InitializeForContextCore. Lagra den i värdcontainern med hjälp av LayoutState som tillhandahålls av kontext.
- Åsidosätt MeasureOverride och anropa metoden Measure på alla barnobjekt.
- Åsidosätt ArrangeOverride och anropa metoden Arrange för alla barn.
- (Ny/valfritt) Rensa alla sparade tillstånd som en del av UninitializeForContextCore.
Exempel: En enkel stacklayout (objekt i olika storlekar)
Här är en mycket grundläggande icke-virtualiserande stack-layout för objekt av varierande storlek. Den saknar egenskaper för att justera layoutens beteende. Implementeringen nedan visar hur layouten förlitar sig på kontextobjektet som tillhandahålls av containern för att:
- Hämta antalet barn och
- Få åtkomst till varje underordnat element genom dess 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>
Virtualisera layouter
Precis som en layout som inte virtualiserar är stegen på hög nivå för en virtualiseringslayout desamma. Komplexiteten är till stor del när det gäller att avgöra vilka element som ska ingå i visningshamnen och bör realiseras.
- Härled från bastypen VirtualizingLayout.
- (Valfritt) Definiera dina beroendeegenskaper som när de ändras ogiltigförklarar layouten.
- Initiera alla tillståndsobjekt som krävs av layouten som en del av InitializeForContextCore. Lagra den i värdcontainern med hjälp av LayoutState som tillhandahålls av kontext.
- Åsidosätt MeasureOverride och anropa metoden Mått för varje underordnad metod som ska realiseras.
- Metoden GetOrCreateElementAt används för att hämta ett UIElement som har förberetts av ramverket (till exempel tillämpade databindningar).
- Åsidosätt ArrangeOverride och anropa metoden Arrange för varje realiserat underordnat objekt.
- (Valfritt) Rensa alla sparade tillstånd som en del av UninitializeForContextCore.
Tips/Råd
Värdet som returneras av MeasureOverride används som storleken på det virtualiserade innehållet.
Det finns två allmänna metoder att tänka på när du skapar en virtualiseringslayout. Om du vill välja det ena eller det andra beror till stor del på "hur avgör du storleken på ett element". Om det är tillräckligt för att känna till indexet för ett objekt i datauppsättningen eller om själva data dikterar dess slutliga storlek, skulle vi betrakta det som databeroende. Dessa är enklare att skapa. Men om det enda sättet att fastställa storleken på ett objekt är att skapa och mäta användargränssnittet skulle vi säga att det är innehållsberoende. Dessa är mer komplexa.
Layoutprocessen
Oavsett om du skapar en data- eller innehållsberoende layout är det viktigt att förstå layoutprocessen och effekten av Windows asynkron rullning.
En (över)förenklad vy över de steg som utförs av ramverket från start till att visa användargränssnittet på skärmen är att:
Det tolkar markup.
Genererar ett träd med element.
Utför ett layoutpass.
Utför ett återgivningspass.
Med virtualisering av användargränssnittet fördröjs eller avslutas skapandet av de element som normalt görs i steg 2 när det har fastställts att tillräckligt med innehåll har skapats för att fylla visningsporten. En virtualiseringscontainer (till exempel ItemsRepeater) överlåter till dess anslutna layout att driva denna process. Den tillhandahåller den bifogade layouten med en VirtualizingLayoutContext som visar den ytterligare information som en virtualiseringslayout behöver.
RealizationRect (dvs. Viewport)
Rullning på Windows sker asynkront i användargränssnittstråden. Det styrs inte av ramverkets layout. I stället sker interaktionen och förflyttningen i systemets compositor. Fördelen med den här metoden är att panorering av innehåll alltid kan göras på 60fps. Utmaningen är dock att "viewport", som visas i layouten, kan vara något inaktuell i förhållande till vad som faktiskt visas på skärmen. Om en användare scrollar snabbt kan de överträffa hastigheten hos användargränssnittstråden när den skapar nytt innehåll, vilket kan resultera i att skärmen blir svart. Därför är det ofta nödvändigt att en virtualiseringslayout genererar ytterligare en buffert med förberedda element som är tillräckligt för att fylla ett område som är större än visningsplatsen. När belastningen är tyngre under rullning visas användaren fortfarande med innehåll.
Eftersom elementskapandet är kostsamt kommer virtualisering av containrar (till exempel ItemsRepeater) inledningsvis att ge den bifogade layouten en RealizationRect som matchar visningsporten. Vid inaktivitet kan containern utöka bufferten av förberett innehåll genom att göra upprepade anrop till layouten med hjälp av en allt större realiseringsrektangel. Det här beteendet är en prestandaoptimering som försöker hitta en balans mellan snabb starttid och en bra panoreringsupplevelse. Den maximala buffertstorleken som ItemsRepeater genererar styrs av egenskaperna VerticalCacheLength och HorizontalCacheLength .
Återanvända element (återvinning)
Layouten förväntas storleksanpassa och placera elementen så att de fyller RealizationRect varje gång det körs. Som standard återanvänder VirtualizingLayout alla oanvända element i slutet av varje layoutpass.
VirtualizingLayoutContext som skickas till layouten som en del av MeasureOverride och ArrangeOverride innehåller ytterligare information som en virtualiseringslayout behöver. Några av de vanligaste sakerna som det ger är möjligheten att:
- Fråga efter antalet objekt i data (ItemCount).
- Hämta ett specifikt objekt med metoden GetItemAt .
- Hämta en RealizationRect som representerar visningsporten och bufferten som layouten ska fylla med realiserade element.
- Begär UIElement för ett specifikt objekt med metoden GetOrCreateElementAt .
Om du begär ett element för ett visst index markeras elementet som "används" för det passet i layouten. Om elementet inte redan finns kommer det att realiseras och förberedas automatiskt för användning (till exempel blåsa upp användargränssnittsträdet som definierats i ett DataTemplate, bearbeta databindningar osv.). Annars hämtas den från en pool med befintliga instanser.
I slutet av varje måttpass anses alla befintliga realiserade element som inte har markerats som "i bruk" automatiskt vara tillgängliga för återanvändning om inte alternativet SuppressAutoRecycle användes när elementet hämtades via metoden GetOrCreateElementAt . Ramverket flyttar det automatiskt till en återanvändningspool och gör det tillgängligt. Den kan senare hämtas för användning av en annan container. Ramverket försöker undvika detta när det är möjligt eftersom det finns en viss kostnad som är associerad med att överordna ett element.
Om en virtualiseringslayout vet i början av varje mätning vilka element som inte längre kommer att ligga inom realiseringsrektangeln kan den optimera sin återanvändning. I stället för att förlita sig på ramverkets standardbeteende. Layouten kan i förebyggande syfte flytta element till återanvändningspoolen med hjälp av metoden RecycleElement . Om du anropar den här metoden innan du begär nya element blir dessa befintliga element tillgängliga när layouten senare utfärdar en GetOrCreateElementAt-begäran för ett index som inte redan är associerat med ett element.
VirtualizingLayoutContext innehåller ytterligare två egenskaper som utformats för layoutförfattare som skapar en innehållsberoende layout. De diskuteras mer detaljerat senare.
- Ett RecommendedAnchorIndex som ger en valfri input till layouten.
- En LayoutOrigin som är ett valfritt resultat från layouten.
Databeroende virtualiseringslayouter
En virtualiseringslayout är enklare om du vet hur stort varje objekt ska vara utan att behöva mäta innehållet som ska visas. I det här dokumentet refererar vi bara till den här kategorin av virtualiseringslayouter som datalayouter eftersom de vanligtvis omfattar granskning av data. Baserat på data kan en app välja en visuell representation med en känd storlek, kanske för att den utgör en del av data eller tidigare har fastställts av designen.
Den allmänna metoden är att layouten ska:
- Beräkna en storlek och position för varje objekt.
- Som en del av MeasureOverride:
- Använd RealizationRect för att avgöra vilka objekt som ska visas i visningsporten.
- Hämta det UIElement som ska representera objektet med metoden GetOrCreateElementAt .
- Mät UIElement med den förberäknade storleken.
- Som en del av ArrangeOverrideordnar du varje realiserat UIElement med den förberäknade positionen.
Anmärkning
En metod för datalayout är ofta inte kompatibel med datavirtualisering. Mer specifikt, där de enda data som läses in i minnet är de data som krävs för att fylla det som är synligt för användaren. Datavirtualisering refererar inte till lat eller inkrementell inläsning av data när en användare rullar ned där dessa data förblir bosatta. I stället refererar det till när objekt frigörs från minnet när de rullas ur vyn. En datalayout som inspekterar varje dataobjekt som en del av en datalayout skulle förhindra att datavirtualisering fungerar som förväntat. Ett undantag är en layout som UniformGridLayout som förutsätter att allt har samma storlek.
Tips/Råd
Om du skapar en anpassad kontroll för ett kontrollbibliotek som kommer att användas av andra i en mängd olika situationer kanske en datalayout inte är ett alternativ för dig.
Exempel: layout för Xbox aktivitetsfeed
Användargränssnittet för Xbox-aktivitetsfeeden använder ett upprepande mönster där varje rad har en bred panel, följt av två smala paneler som inverteras på den efterföljande raden. I den här layouten är storleken för varje objekt en funktion av objektets position i datauppsättningen och den kända storleken för panelerna (bred eller smal).
Koden nedan går igenom vad ett anpassat virtualiseringsgränssnitt för aktivitetsflödet kan vara för att illustrera den allmänna metod som du kan använda för en datalayout.
Tips/Råd
Om du har installerat WinUI 3-galleriappen klickar du här för att öppna appen och ser hur ItemsRepeater fungerar. Hämta appen från Microsoft Store eller hämta källkoden på GitHub.
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;
}
(Valfritt) Hantera objekt till UIElement-mappning
Som standard upprätthåller VirtualizingLayoutContext en mappning mellan de realiserade elementen och indexet i den datakälla som de representerar. En layout kan välja att hantera själva mappningen genom att alltid begära alternativet Att ignoreraAutoRecycle när du hämtar ett element via metoden GetOrCreateElementAt , vilket förhindrar standardbeteendet för automatisk återvinning. En layout kan välja att göra detta, till exempel om den bara används när rullning är begränsad till en riktning och de objekt som den anser alltid kommer att vara sammanhängande (dvs. att känna till indexet för det första och sista elementet räcker för att känna till alla element som bör realiseras).
Exempel: Xbox aktivitetsflödesmått
Kodfragmentet nedan visar den ytterligare logik som kan läggas till i MeasureOverride i föregående exempel för att hantera mappningen.
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; }
// ...
}
Innehållsberoende virtualiseringslayouter
Om du först måste mäta användargränssnittets innehåll för ett objekt för att ta reda på dess exakta storlek är det en innehållsberoende layout. Du kan också se det som en layout där varje objekt måste storleksanpassa sig i stället för layouten som visar objektets storlek. Virtualisering av layouter som ingår i den här kategorin är mer involverad.
Anmärkning
Innehållsberoende layouter bryter inte (bör inte) datavirtualisering.
Beräkningar
Innehållsberoende layouter förlitar sig på uppskattning för att gissa både storleken på orealiserat innehåll och positionen för det realiserade innehållet. När dessa uppskattningar ändras kommer det att leda till att det realiserade innehållet regelbundet flyttar positioner inom det rullningsbara området. Detta kan leda till en mycket frustrerande och skakande användarupplevelse om den inte minimeras. De potentiella problemen och minskningarna beskrivs här.
Anmärkning
Datalayouter som beaktar varje objekt och vet den exakta storleken på alla objekt, realiserade eller inte, och deras positioner kan undvika dessa problem helt och hållet.
Rulla förankring
XAML tillhandahåller en mekanism för att motverka plötsliga förskjutningar i vyn genom att rullningskontroller har stöd för rullningsförankring genom att implementera gränssnittet IScrollAnchorProvider. När användaren manipulerar innehållet väljer rullningskontrollen kontinuerligt ett element från den uppsättning kandidater som har valts att spåras. Om fästpunktselementets position skiftar under layouten flyttar rullningskontrollen automatiskt sin visningsport för att behålla visningsporten.
Värdet för RecommendedAnchorIndex som anges i layouten kan återspegla det ankarelement som valts av rullningskontrollen. Om en utvecklare uttryckligen begär att ett element ska realiseras för ett index med metoden GetOrCreateElement i ItemsRepeater, anges det indexet som RecommendedAnchorIndex vid nästa layoutpass. Detta gör att layouten kan förberedas för det troliga scenariot att en utvecklare inser ett element och därefter begär att det ska visas via metoden StartBringIntoView .
RecommendedAnchorIndex är indexet för objektet i datakällan som en innehållsberoende layout ska placera först när objektens position beräknas. Det bör fungera som utgångspunkt för positionering av andra realiserade objekt.
Påverkan på rullningslister
Även med rullningsankring, om layoutens uppskattningar varierar mycket, kanske på grund av betydande variationer i innehållets storlek, kan tumpositionen för rullningslisten verka hoppa runt. Detta kan vara skakande för en användare om tummen inte verkar spåra muspekarens position när de drar den.
Ju mer exakt layoutens uppskattningar kan vara, desto mindre sannolikt är det att användaren ser rullningslistens indikator hoppa.
Layoutkorrigeringar
En innehållsberoende layout bör vara beredd att rationalisera sin uppskattning med verkligheten. När användaren till exempel rullar överst i innehållet och layouten inser det allra första elementet, kan det hända att elementets förväntade position i förhållande till det element som det startade från skulle leda till att det visas någon annanstans än ursprunget för (x:0, y:0). När detta inträffar kan layouten använda egenskapen LayoutOrigin för att ange den position som beräknas som den nya layoutens ursprung. Nettoresultatet liknar scrollförankring där rullningskontrollens vyfönster automatiskt justeras för att beakta innehållets position så som rapporterat av layouten.
Frånkopplade visningsportar
Storleken som returneras från layoutens MeasureOverride-metod representerar den bästa gissningen på storleken på innehållet som kan ändras med varje efterföljande layout. När en användare rullar kommer layouten kontinuerligt att utvärderas på nytt med en uppdaterad RealizationRect.
Om en användare drar tummen mycket snabbt är det möjligt för viewporten, ur layoutens perspektiv, att verka göra stora hopp där den tidigare positionen inte överlappar den nu aktuella positionen. Detta beror på rullningens asynkrona karaktär. Det är också möjligt för en app som använder layouten för att begära att ett element ska visas för ett objekt som för närvarande inte realiseras och som beräknas ligga utanför det aktuella intervallet som spåras av layouten.
När layouten upptäcker att dess gissning är felaktig och/eller ser en oväntad vyportsförskjutning måste den ändra sin startposition. De virtualiseringslayouter som levereras som en del av XAML-kontrollerna utvecklas som innehållsberoende layouter eftersom de har färre begränsningar för innehållets natur som ska visas.
Exempel: Enkel virtualiseringsstacklayout för objekt med variabel storlek
Exemplet nedan visar en enkel stacklayout för objekt i variabel storlek som:
- stöder virtualisering av användargränssnittet.
- använder uppskattningar för att gissa storleken på orealiserade objekt,
- är medveten om potentiella diskontinuerliga visningsfönsterförskjutningar, och
- Använder layoutkorrigeringar för att hantera dessa förändringar.
Användning: Markering
<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>
Kodbakgrund: 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;
Kod: 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);
}
}
Relaterade artiklar
Windows developer