Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Conteneur (par exemple, Panel) qui délègue sa logique de disposition à un autre objet s’appuie sur l’objet de disposition attaché pour fournir le comportement de disposition de ses éléments enfants. Un modèle de disposition attaché permet à une application de modifier la disposition des éléments au moment de l’exécution, ou de partager plus facilement des aspects de disposition entre différentes parties de l’interface utilisateur (par exemple, des éléments dans les lignes d’une table qui semblent être alignés dans une colonne).
Dans cette rubrique, nous abordons ce qui est impliqué dans la création d’une disposition jointe (virtualisation et non virtualisation), les concepts et les classes que vous devez comprendre et les compromis que vous devez prendre en compte lors du choix entre eux.
| Obtenir WinUI |
|---|
| Ce contrôle est inclus dans le cadre de WinUI, un package NuGet qui contient de nouveaux contrôles et fonctionnalités d’interface utilisateur pour les applications Windows. Pour plus d’informations, y compris les instructions d’installation, consultez la vue d’ensemble de WinUI. |
API importantes :
Concepts clés
L’exécution de la disposition nécessite que deux questions soient répondues pour chaque élément :
Quelle est la taille de cet élément ?
Quelle sera la position de cet élément ?
Le système de disposition xaml, qui répond à ces questions, est brièvement abordé dans le cadre de la discussion des panneaux personnalisés.
Conteneurs et contexte
Conceptuellement, le panneau XAML remplit deux rôles importants dans l’infrastructure :
- Il peut contenir des éléments enfants et introduire la branche dans l’arborescence des éléments.
- Il applique une stratégie de disposition spécifique à ces enfants.
Pour cette raison, un panneau en XAML a souvent été synonyme de disposition, mais techniquement, fait plus que simplement la disposition.
ItemsRepeater se comporte également comme Panel, mais, contrairement à Panel, il n’expose pas de propriété Children qui permettrait d’ajouter ou de supprimer par programmation des enfants UIElement. Au lieu de cela, la durée de vie de ses enfants est automatiquement gérée par l’infrastructure pour correspondre à une collection d’éléments de données. Bien qu’il ne soit pas dérivé du Groupe spécial, il se comporte et est traité par le cadre comme un groupe spécial.
Note
LayoutPanel est un conteneur dérivé de Panel qui délègue sa logique à l’objet Layout attaché. LayoutPanel est disponible en préversion et est actuellement disponible uniquement dans les suppressions préliminaires du package WinUI.
Containers
Conceptuellement, Panel est un conteneur d’éléments qui a également la possibilité de restituer des pixels pour un arrière-plan. Les panneaux fournissent un moyen d’encapsuler une logique de disposition commune dans un package facile à utiliser.
Le concept de disposition attachée fait la distinction entre les deux rôles de conteneur et de disposition plus clair. Si le conteneur délègue sa logique de disposition à un autre objet, nous appelons cet objet la disposition jointe comme indiqué dans l’extrait de code ci-dessous. Les conteneurs qui héritent de FrameworkElement, tels que LayoutPanel, exposent automatiquement les propriétés communes qui fournissent une entrée au processus de disposition xaml (par exemple, Height et Width).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Pendant le processus de disposition, le conteneur s’appuie sur l’UniformGridLayout attaché pour mesurer et organiser ses enfants.
État Per-Container
Avec une disposition jointe, une seule instance de l’objet de disposition peut être associée à de nombreux conteneurs comme dans l’extrait de code ci-dessous ; par conséquent, elle ne doit pas dépendre ou référencer directement le conteneur hôte. Par exemple:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
Pour cette situation , ExampleLayout doit examiner attentivement l’état qu’il utilise dans son calcul de disposition et où cet état est stocké pour éviter d’affecter la disposition pour les éléments d’un panneau avec l’autre. Il serait analogue à un panneau personnalisé dont la logique MeasureOverride et ArrangeOverride dépend des valeurs de ses propriétés statiques .
LayoutContext
L’objectif de LayoutContext est de traiter ces défis. Il fournit à la disposition jointe la possibilité d’interagir avec le conteneur hôte, comme la récupération d’éléments enfants, sans introduire de dépendance directe entre les deux. Le contexte permet également à la disposition de stocker n’importe quel état requis, ce qui peut être lié aux éléments enfants du conteneur.
Les dispositions simples et non virtualisantes n’ont souvent pas besoin de maintenir un état, ce qui en fait un non-problème. Toutefois, une disposition plus complexe, telle que Grid, peut choisir de maintenir l’état entre la mesure et d’organiser l’appel pour éviter de calculer une valeur.
La virtualisation des dispositions doit souvent conserver un état entre la mesure et organiser ainsi qu’entre les passes de disposition itératives.
Initialisation et non initialisation de l’état de Per-Container
Lorsqu’une disposition est attachée à un conteneur, sa méthode InitializeForContextCore est appelée et permet d’initialiser un objet pour stocker l’état.
De même, lorsque la disposition est supprimée d’un conteneur, la méthode UninitializeForContextCore est appelée. Cela donne la possibilité de nettoyer tout état associé à ce conteneur.
L’objet d’état de la disposition peut être stocké et récupéré à partir du conteneur avec la propriété LayoutState sur le contexte.
Virtualisation de l’interface utilisateur
La virtualisation de l’interface utilisateur signifie retarder la création d’un objet d’interface utilisateur jusqu’à ce qu’elle soit nécessaire. Il s’agit d’une optimisation des performances. Pour les scénarios de non-défilement déterminant si nécessaire peut être basé sur un nombre quelconque d’éléments spécifiques à l’application. Dans ce cas, les applications doivent envisager d’utiliser x :Load. Il ne nécessite aucune gestion spéciale dans votre disposition.
Dans les scénarios de défilement tels qu’une liste, déterminer si nécessaire est souvent basé sur « sera-t-il visible pour un utilisateur » qui dépend fortement de l’emplacement où il a été placé pendant le processus de disposition et nécessite des considérations particulières. Ce scénario est un focus pour ce document.
Note
Bien qu’elles ne soient pas abordées dans ce document, les mêmes fonctionnalités qui permettent la virtualisation de l’interface utilisateur dans les scénarios de défilement peuvent être appliquées dans des scénarios de non-défilement. Par exemple, un contrôle ToolBar piloté par les données qui gère la durée de vie des commandes qu’il présente et répond aux modifications apportées à l’espace disponible en recyclant/déplaçant des éléments entre une zone visible et un menu de dépassement de capacité.
Getting Started
Tout d’abord, déterminez si la disposition que vous devez créer doit prendre en charge la virtualisation de l’interface utilisateur.
Quelques points à garder à l’esprit...
- Les dispositions non virtualisantes sont plus faciles à créer. Si le nombre d’éléments est toujours petit, la création d’une disposition non virtualisante est recommandée.
- La plateforme fournit un ensemble de dispositions jointes qui fonctionnent avec ItemsRepeater et LayoutPanel pour couvrir les besoins courants. Familiarisez-vous avec celles-ci avant de décider de définir une disposition personnalisée.
- Les dispositions de virtualisation ont toujours des coûts supplémentaires de processeur et de mémoire/complexité/surcharge par rapport à une disposition qui ne virtualise pas. En règle générale, si les enfants que la disposition devra gérer seront probablement adaptés à une zone qui est 3x la taille de la fenêtre d’affichage, il peut ne pas y avoir beaucoup d’avantages à partir d’une disposition virtualisante. La taille 3x est abordée plus en détail plus loin dans ce document, mais est due à la nature asynchrone du défilement sur Windows et à son impact sur la virtualisation.
Conseil / Astuce
À titre de référence, les paramètres par défaut de ListView (et ItemsRepeater) sont que le recyclage ne commence pas tant que le nombre d’éléments n’est pas suffisant pour remplir jusqu’à 3 fois la taille de la fenêtre d’affichage actuelle.
Choisir votre type de base
Le type de disposition de base a deux types dérivés qui servent de point de départ pour la création d’une disposition jointe :
Disposition sans virtualisation
L’approche de création d’une disposition sans virtualisation doit se sentir familière à toute personne qui a créé un panneau personnalisé. Les mêmes concepts s’appliquent. La principale différence est qu’un NonVirtualizingLayoutContext est utilisé pour accéder à la collection Children et que la disposition peut choisir de stocker l’état.
- Dérivez du type de base NonVirtualizingLayout (au lieu de Panel).
- (Facultatif) Définissez les propriétés de dépendance qui, lorsqu’elles ont été modifiées, invalideront la disposition.
- (Nouveau/facultatif) Initialisez tout objet d’état requis par la disposition dans le cadre de l’InitializeForContextCore. Placez-la avec le conteneur hôte à l’aide de LayoutState fournie avec le contexte.
- Remplacez MeasureOverride et appelez la méthode Measure sur tous les enfants.
- Remplacez ArrangeOverride et appelez la méthode Arrange sur tous les enfants.
- (Nouveau/facultatif) Nettoyez tout état enregistré dans le cadre de l’uninitializeForContextCore.
Exemple : disposition de pile simple (Varying-Sized éléments)
Voici une disposition de pile non virtualisante très simple de différents éléments de taille. Il n’y a pas de propriétés pour ajuster le comportement de la disposition. L’implémentation ci-dessous illustre la façon dont la disposition s’appuie sur l’objet de contexte fourni par le conteneur pour :
- Obtenir le nombre d’enfants et
- Accédez à chaque élément enfant par 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>
Virtualisation des dispositions
Comme pour une disposition sans virtualisation, les étapes générales d’une disposition de virtualisation sont identiques. La complexité est en grande partie de déterminer quels éléments seront inclus dans la fenêtre d’affichage et devraient être réalisés.
- Dérivez du type de base VirtualizingLayout.
- (Facultatif) Définissez vos propriétés de dépendance qui, lorsqu’elles sont modifiées, invalideront la disposition.
- Initialisez tout objet d’état requis par la disposition dans le cadre de l’initializeForContextCore. Placez-la avec le conteneur hôte à l’aide de LayoutState fournie avec le contexte.
- Remplacez MeasureOverride et appelez la méthode Measure pour chaque enfant qui doit être réalisé.
- La méthode GetOrCreateElementAt est utilisée pour récupérer un UIElement préparé par l’infrastructure (par exemple, les liaisons de données appliquées).
- Remplacez ArrangeOverride et appelez la méthode Arrange pour chaque enfant réalisé.
- (Facultatif) Nettoyez tout état enregistré dans le cadre de l’uninitializeForContextCore.
Conseil / Astuce
La valeur retournée par MeasureOverride est utilisée comme taille du contenu virtualisé.
Il existe deux approches générales à prendre en compte lors de la création d’une disposition de virtualisation. Le choix d’un ou de l’autre dépend en grande partie de la façon dont vous déterminerez la taille d’un élément. Si elle suffit pour connaître l’index d’un élément dans le jeu de données ou les données elle-même dictent sa taille éventuelle, nous l’envisagerions dépendante des données. Celles-ci sont plus simples à créer. Toutefois, si la seule façon de déterminer la taille d’un élément consiste à créer et à mesurer l’interface utilisateur, nous dis-le dépend du contenu. Ils sont plus complexes.
Processus de disposition
Que vous créez une disposition dépendante du contenu ou des données, il est important de comprendre le processus de disposition et l’impact du défilement asynchrone de Windows.
Une vue (sur)simplifiée des étapes effectuées par l’infrastructure depuis le démarrage jusqu’à l’affichage de l’interface utilisateur à l’écran est celle-ci :
Il analyse le balisage.
Génère une arborescence d’éléments.
Effectue une passe de disposition.
Effectue une passe de rendu.
Avec la virtualisation de l’interface utilisateur, la création des éléments qui seraient normalement effectués à l’étape 2 est retardée ou terminée tôt une fois qu’elle a été déterminée que le contenu suffisant a été créé pour remplir la fenêtre d’affichage. Un conteneur de virtualisation (par exemple, ItemsRepeater) reporte sa disposition attachée pour piloter ce processus. Il fournit la disposition jointe avec un VirtualizingLayoutContext qui expose les informations supplémentaires dont a besoin une disposition virtualisante.
The RealizationRect (c.-à-d. Viewport)
Le défilement sur Windows se produit de façon asynchrone vers le thread d’interface utilisateur. Elle n’est pas contrôlée par la disposition du framework. Au lieu de cela, l’interaction et le mouvement se produisent dans le compositeur du système. L’avantage de cette approche est que le contenu panoramique peut toujours être effectué à 60fps. Toutefois, le défi est que la « fenêtre d’affichage », comme l’indique la disposition, peut être légèrement obsolète par rapport à ce qui est réellement visible à l’écran. Si un utilisateur fait défiler rapidement, il risque d’espacer la vitesse du thread d’interface utilisateur pour générer du nouveau contenu et « faire défiler vers le noir ». Pour cette raison, il est souvent nécessaire pour une disposition de virtualisation de générer une mémoire tampon supplémentaire d’éléments préparés suffisants pour remplir une zone supérieure à la fenêtre d’affichage. Lorsque la charge est plus lourde pendant le défilement, l’utilisateur est toujours présenté avec du contenu.
Étant donné que la création d’éléments est coûteuse, la virtualisation des conteneurs (par exemple , ItemsRepeater) fournit initialement la disposition jointe avec un RealizationRect qui correspond à la fenêtre d’affichage. En cas d’inactivité, le conteneur peut augmenter la mémoire tampon du contenu préparé en effectuant des appels répétés à la disposition à l’aide d’un rect de plus en plus grande de réalisation. Ce comportement est une optimisation des performances qui tente de trouver un équilibre entre le temps de démarrage rapide et une bonne expérience de panoramique. La taille maximale de la mémoire tampon générée par ItemsRepeater est contrôlée par ses propriétés VerticalCacheLength et HorizontalCacheLength .
Réutilisation d’éléments (recyclage)
La disposition est censée dimensionner et positionner les éléments pour remplir le Runtime de réalisation chaque fois qu’il est exécuté. Par défaut, VirtualizingLayout recycle tous les éléments inutilisés à la fin de chaque passe de disposition.
VirtualizingLayoutContext qui est passé à la disposition dans le cadre de MeasureOverride et ArrangeOverride fournit des informations supplémentaires sur les besoins de disposition de virtualisation. Voici quelques-unes des choses les plus couramment utilisées qu’il fournit :
- Interrogez le nombre d’éléments dans les données (ItemCount).
- Récupérez un élément spécifique à l’aide de la méthode GetItemAt .
- Récupérez un RealizationRect qui représente la fenêtre d’affichage et la mémoire tampon que la disposition doit remplir avec des éléments réalisés.
- Demandez l’élément UIElement pour un élément spécifique avec la méthode GetOrCreateElement .
La demande d’un élément pour un index donné entraîne la marque de cet élément comme étant « en cours d’utilisation » pour cette passe de la disposition. Si l’élément n’existe pas déjà, il est réalisé et préparé automatiquement pour une utilisation (par exemple, enflant l’arborescence de l’interface utilisateur définie dans un DataTemplate, en traitant toute liaison de données, etc.). Sinon, il sera récupéré à partir d’un pool d’instances existantes.
À la fin de chaque passe de mesure, tout élément existant, réalisé qui n’a pas été marqué « en cours d’utilisation » est automatiquement considéré comme disponible pour une réutilisation, sauf si l’option de SuppressAutoRecycle a été utilisée lorsque l’élément a été récupéré via la méthode GetOrCreateElementAt . L’infrastructure la déplace automatiquement vers un pool de recyclage et la rend disponible. Il peut par la suite être extrait pour une utilisation par un autre conteneur. Le framework tente d’éviter cela si possible, car il existe un coût associé au re-parenting d’un élément.
Si une disposition de virtualisation sait au début de chaque mesure que les éléments ne tomberont plus dans le rect de réalisation, il peut optimiser sa réutilisation. Plutôt que de compter sur le comportement par défaut du framework. La disposition peut préemptivement déplacer des éléments vers le pool de recyclage à l’aide de la méthode RecycleElement . L’appel de cette méthode avant de demander de nouveaux éléments entraîne la disponibilité de ces éléments existants lorsque la disposition émet ultérieurement une requête GetOrCreateElementAt pour un index qui n’est pas déjà associé à un élément.
VirtualizingLayoutContext fournit deux propriétés supplémentaires conçues pour les auteurs de disposition créant une disposition dépendante du contenu. Ils sont abordés plus en détail plus tard.
- RecommandéAnchorIndex qui fournit une entrée facultative à la disposition.
- DispositionOrigin qui est une sortie facultative de la disposition.
Dispositions de virtualisation dépendantes des données
Une disposition de virtualisation est plus facile si vous connaissez la taille de chaque élément sans avoir à mesurer le contenu à afficher. Dans ce document, nous allons simplement faire référence à cette catégorie de dispositions de virtualisation en tant que dispositions de données , car elles impliquent généralement d’inspecter les données. En fonction des données qu’une application peut choisir une représentation visuelle avec une taille connue, peut-être parce que sa partie des données ou a été déterminée précédemment par la conception.
L’approche générale consiste à mettre en page les éléments suivants :
- Calculez une taille et une position de chaque élément.
- Dans le cadre de MeasureOverride :
- Utilisez la propriété RealizationRect pour déterminer quels éléments doivent apparaître dans la fenêtre d’affichage.
- Récupérez l’uiElement qui doit représenter l’élément avec la méthode GetOrCreateElementAt .
- Mesurez l’UIElement avec la taille précalculée.
- Dans le cadre de l’ArrangeOverride, arrangez chaque UIElement réalisé avec la position précalculée.
Note
Une approche de disposition des données est souvent incompatible avec la virtualisation des données. Plus précisément, où les seules données chargées en mémoire sont les données requises pour remplir ce qui est visible pour l’utilisateur. La virtualisation des données ne fait pas référence à un chargement différé ou incrémentiel de données en tant qu’utilisateur fait défiler vers le bas l’emplacement où ces données restent résidentes. Au lieu de cela, il fait référence au moment où les éléments sont libérés de la mémoire à mesure qu’ils font défiler l’affichage. Avoir une disposition de données qui inspecte chaque élément de données dans le cadre d’une disposition de données empêcherait la virtualisation des données de fonctionner comme prévu. Une exception est une disposition telle que UniformGridLayout, qui suppose que tout a la même taille.
Conseil / Astuce
Si vous créez un contrôle personnalisé pour une bibliothèque de contrôles qui sera utilisée par d’autres personnes dans une grande variété de situations, une disposition de données peut ne pas être une option pour vous.
Exemple : disposition du flux d’activité Xbox
L’interface utilisateur du flux d’activité Xbox utilise un modèle répétitif où chaque ligne a une vignette large, suivie de deux vignettes étroites inversées sur la ligne suivante. Dans cette disposition, la taille de chaque élément est une fonction de la position de l’élément dans le jeu de données et de la taille connue des vignettes (large ou étroite).
Le code ci-dessous décrit ce qu’une interface utilisateur de virtualisation personnalisée pour le flux d’activité peut être d’illustrer l’approche générale que vous pouvez adopter pour une disposition de données.
Conseil / Astuce
Si l’application Galerie WinUI 3 est installée, cliquez ici pour ouvrir l’application et voir ItemsRepeater en action. Obtenez l’application à partir du Microsoft Store ou obtenez le code source sur 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;
}
(Facultatif) Gestion du mappage d’élément vers UIElement
Par défaut, VirtualizingLayoutContext conserve un mappage entre les éléments réalisés et l’index dans la source de données qu’ils représentent. Une disposition peut choisir de gérer ce mappage lui-même en demandant toujours l’option de SuppressAutoRecycle lors de la récupération d’un élément via la méthode GetOrCreateElementAt , ce qui empêche le comportement de recyclage automatique par défaut. Une disposition peut choisir de le faire, par exemple, si elle ne sera utilisée que lorsque le défilement est limité à une direction et que les éléments qu’il considère seront toujours contigus (c’est-à-dire que le fait de connaître l’index du premier et du dernier élément suffit pour connaître tous les éléments qui doivent être réalisés).
Exemple : mesure du flux d’activité Xbox
L’extrait de code ci-dessous montre la logique supplémentaire qui peut être ajoutée à MeasureOverride dans l’exemple précédent pour gérer le mappage.
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
//...
// Determine which items will appear on those rows and what the rect will be for each item
var state = context.LayoutState as ActivityFeedLayoutState;
state.LayoutRects.Clear();
// Recycle previously realized elements that we know we won't need so that they can be used to
// fill in gaps without requiring us to realize additional elements.
var newFirstRealizedIndex = firstRowIndex * 3;
var newLastRealizedIndex = lastRowIndex * 3 + 3;
for (int i = state.FirstRealizedIndex; i < newFirstRealizedIndex; i++)
{
context.RecycleElement(state.IndexToElementMap.Get(i));
state.IndexToElementMap.Clear(i);
}
for (int i = state.LastRealizedIndex; i < newLastRealizedIndex; i++)
{
context.RecycleElement(context.IndexElementMap.Get(i));
state.IndexToElementMap.Clear(i);
}
// ...
// Foreach item between the first and last index,
// Call GetElementOrCreateElementAt which causes an element to either be realized or retrieved
// from a recycle pool
// Measure the element using an appropriate size
//
for (int rowIndex = firstRowIndex; rowIndex < lastRowIndex; rowIndex++)
{
int firstItemIndex = rowIndex * 3;
var boundsForCurrentRow = CalculateLayoutBoundsForRow(rowIndex, desiredItemWidth);
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var index = firstItemIndex + columnIndex;
var rect = boundsForCurrentRow[index % 3];
UIElement container = null;
if (state.IndexToElementMap.Contains(index))
{
container = state.IndexToElementMap.Get(index);
}
else
{
container = context = context.GetOrCreateElementAt(index, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
state.IndexToElementMap.Add(index, container);
}
container.Measure(
new Size(boundsForCurrentRow[columnIndex].Width, boundsForCurrentRow[columnIndex].Height));
state.LayoutRects.Add(boundsForCurrentRow[columnIndex]);
}
}
// ...
}
internal class ActivityFeedLayoutState
{
// ...
Dictionary<int, UIElement> IndexToElementMap { get; set; }
// ...
}
Dispositions de virtualisation dépendantes du contenu
Si vous devez d’abord mesurer le contenu de l’interface utilisateur d’un élément pour déterminer sa taille exacte, il s’agit d’une disposition dépendante du contenu. Vous pouvez également le considérer comme une disposition où chaque élément doit se dimensionner lui-même plutôt que la disposition indiquant à l’élément sa taille. Les dispositions de virtualisation qui appartiennent à cette catégorie sont plus impliquées.
Note
Les dispositions dépendantes du contenu ne sont pas (ne doivent pas) interrompre la virtualisation des données.
Estimations
Les dispositions dépendantes du contenu s’appuient sur l’estimation pour deviner à la fois la taille du contenu non réalisé et la position du contenu réalisé. À mesure que ces estimations changent, le contenu réalisé change régulièrement les positions dans la zone de défilement. Cela peut entraîner une expérience utilisateur très frustrante et énervante si elle n’est pas atténuée. Les problèmes potentiels et les atténuations sont abordés ici.
Note
Les dispositions de données qui prennent en compte chaque élément et connaissent la taille exacte de tous les éléments, réalisés ou non, et leurs positions peuvent éviter entièrement ces problèmes.
Ancrement de défilement
XAML fournit un mécanisme permettant d’atténuer les décalages soudains de la fenêtre d’affichage en ayant des contrôles de défilement prenant en charge l’ancrage de défilement en implémentant l’interface IScrollAnchorPovider . Lorsque l’utilisateur manipule le contenu, le contrôle de défilement sélectionne continuellement un élément dans l’ensemble de candidats qui ont été sélectionnés pour être suivis. Si la position de l’élément d’ancrage se déplace pendant la disposition, le contrôle de défilement déplace automatiquement sa fenêtre d’affichage pour maintenir la fenêtre d’affichage.
La valeur de RecommendedAnchorIndex fournie à la disposition peut refléter l’élément d’ancrage actuellement sélectionné choisi par le contrôle de défilement. Sinon, si un développeur demande explicitement qu’un élément soit réalisé pour un index avec la méthode GetOrCreateElement sur ItemsRepeater, cet index est donné en tant que RecommendedAnchorIndex lors de la passe de disposition suivante. Cela permet de préparer la disposition pour le scénario probable qu’un développeur réalise un élément et demande par la suite qu’il soit mis en vue via la méthode StartBringIntoView .
RecommendedAnchorIndex est l’index de l’élément dans la source de données qu’une disposition dépendante du contenu doit positionner en premier lors de l’estimation de la position de ses éléments. Il doit servir de point de départ pour positionner d’autres éléments réalisés.
Impact sur les barres de défilement
Même avec l’ancrage du défilement, si les estimations de la disposition varient beaucoup, peut-être en raison de variations significatives de la taille du contenu, la position du pouce pour la barre de défilement peut sembler sauter. Cela peut être jarring pour un utilisateur si le pouce n’apparaît pas pour suivre la position de son pointeur de souris lorsqu’il le fait glisser.
Plus la disposition peut être précise dans ses estimations, alors moins un utilisateur verra le saut du curseur ScrollBar.
Corrections de disposition
Une disposition dépendante du contenu doit être prête à rationaliser son estimation avec la réalité. Par exemple, lorsque l’utilisateur fait défiler vers le haut du contenu et que la disposition réalise le tout premier élément, il peut constater que la position attendue de l’élément par rapport à l’élément à partir duquel il a commencé l’entraînerait à apparaître ailleurs que l’origine de (x :0, y :0). Lorsque cela se produit, la disposition peut utiliser la propriété LayoutOrigin pour définir la position calculée comme nouvelle origine de disposition. Le résultat net est similaire à l’ancrage de défilement dans lequel la fenêtre d’affichage du contrôle de défilement est automatiquement ajustée pour tenir compte de la position du contenu, comme indiqué par la disposition.
Fenêtres d’affichage déconnectées
La taille retournée par la méthode MeasureOverride de la disposition représente la meilleure estimation de la taille du contenu qui peut changer avec chaque disposition successive. À mesure qu’un utilisateur fait défiler la disposition, elle sera réévaluée en permanence avec une mise à jour de RealizationRect.
Si un utilisateur fait glisser le pouce très rapidement, il est possible pour la fenêtre d’affichage, du point de vue de la disposition, de faire des sauts volumineux où la position précédente ne chevauche pas la position actuelle. Cela est dû à la nature asynchrone du défilement. Il est également possible pour une application qui consomme la disposition de demander qu’un élément soit mis en vue pour un élément qui n’est pas actuellement réalisé et qui est estimé à se trouver en dehors de la plage actuelle suivie par la disposition.
Lorsque la disposition découvre qu’elle est incorrecte et/ou voit un décalage inattendu de la fenêtre d’affichage, il doit réorienter sa position de départ. Les dispositions de virtualisation fournies dans le cadre des contrôles XAML sont développées en tant que dispositions dépendantes du contenu, car elles placent moins de restrictions sur la nature du contenu qui s’affiche.
Exemple : Disposition de la pile de virtualisation simple pour les éléments Variable-Sized
L’exemple ci-dessous illustre une disposition de pile simple pour les éléments de taille variable qui :
- prend en charge la virtualisation de l’interface utilisateur,
- utilise des estimations pour deviner la taille des éléments non réalisés,
- est conscient des décalages potentiels des fenêtres d’affichage discontinues, et
- applique des corrections de disposition pour tenir compte de ces décalages.
Utilisation : balisage
<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);
}
}
Articles connexes
Windows developer