Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Um contêiner (por exemplo, Painel) que delega sua lógica de layout a outro objeto depende do objeto de layout anexado para fornecer o comportamento de layout para seus elementos filho. Um modelo de layout anexado fornece flexibilidade para um aplicativo alterar o layout de itens em runtime ou compartilhar mais facilmente aspectos de layout entre diferentes partes da interface do usuário (por exemplo, itens nas linhas de uma tabela que parecem estar alinhados dentro de uma coluna).
Neste tópico, abordaremos o que está envolvido na criação de um layout anexado (virtualização e não virtualização), os conceitos e classes que você precisará entender e as compensações que você precisará considerar ao decidir entre eles.
| Obter WinUI |
|---|
| Esse controle é incluído como parte do WinUI, um pacote NuGet que contém novos controles e recursos de interface do usuário para aplicativos do Windows. Para obter mais informações, incluindo instruções de instalação, consulte a visão geral do WinUI. |
APIs importantes:
Principais conceitos
Executar o layout requer que duas perguntas sejam respondidas para cada elemento:
Qual será o tamanho desse elemento?
Qual será a posição desse elemento?
O sistema de layout da XAML, que responde a essas perguntas, é brevemente abordado como parte da discussão de painéis personalizados.
Contêineres e Contexto
Conceitualmente, o Painel XAML preenche duas funções importantes na estrutura:
- Ele pode conter elementos filho e introduz ramificação na árvore de elementos.
- Ele aplica uma estratégia de layout específica a esses filhos.
Por esse motivo, um painel em XAML geralmente é sinônimo de layout, mas tecnicamente falando, faz mais do que apenas layout.
O ItemsRepeater também se comporta como Panel, mas, ao contrário do Painel, ele não expõe uma propriedade Children que permitiria adicionar ou remover crianças UIElement programaticamente. Em vez disso, o tempo de vida de seus filhos é gerenciado automaticamente pela estrutura para corresponder a uma coleção de itens de dados. Embora não seja derivado do Painel, ele se comporta e é tratado pela estrutura como um Painel.
Observação
O LayoutPanel é um contêiner, derivado do Painel, que delega sua lógica ao objeto Layout anexado. O LayoutPanel está em Versão Prévia e está disponível apenas nas quedas de pré-lançamento do pacote WinUI.
Contêineres
Conceitualmente, o Painel é um contêiner de elementos que também tem a capacidade de renderizar pixels para um Plano de Fundo. Os painéis fornecem uma maneira de encapsular a lógica de layout comum em um pacote fácil de usar.
O conceito de layout anexado torna a distinção entre as duas funções de contêiner e layout mais clara. Se o contêiner delegar sua lógica de layout para outro objeto, chamaremos esse objeto de layout anexado, como visto no snippet abaixo. Contêineres que herdam do FrameworkElement, como o LayoutPanel, expõem automaticamente as propriedades comuns que fornecem entrada ao processo de layout do XAML (por exemplo, Altura e Largura).
<LayoutPanel>
<LayoutPanel.Layout>
<UniformGridLayout/>
</LayoutPanel.Layout>
<Button Content="1"/>
<Button Content="2"/>
<Button Content="3"/>
</LayoutPanel>
Durante o processo de layout, o contêiner depende do UniformGridLayout anexado para medir e organizar seus filhos.
Estado Per-Container
Com um layout anexado, uma única instância do objeto de layout pode estar associada a muitos contêineres, como no snippet abaixo; portanto, ele não deve depender ou referenciar diretamente o contêiner de host. Por exemplo:
<!-- ... --->
<Page.Resources>
<ExampleLayout x:Name="exampleLayout"/>
<Page.Resources>
<LayoutPanel x:Name="example1" Layout="{StaticResource exampleLayout}"/>
<LayoutPanel x:Name="example2" Layout="{StaticResource exampleLayout}"/>
<!-- ... --->
Para essa situação , ExampleLayout deve considerar cuidadosamente o estado que usa em seu cálculo de layout e onde esse estado é armazenado para evitar afetar o layout de elementos em um painel com o outro. Seria análogo a um Painel personalizado cuja lógica MeasureOverride e ArrangeOverride depende dos valores de suas propriedades estáticas .
LayoutContext
A finalidade do LayoutContext é lidar com esses desafios. Ele fornece ao layout anexado a capacidade de interagir com o contêiner do host, como recuperar elementos filho, sem introduzir uma dependência direta entre os dois. O contexto também permite que o layout armazene qualquer estado necessário que possa estar relacionado aos elementos filho do contêiner.
Layouts simples e não virtualizados geralmente não precisam manter nenhum estado, tornando-o um problema. Um layout mais complexo, como Grid, no entanto, pode optar por manter o estado entre a medida e organizar a chamada para evitar a re computação de um valor.
A virtualização de layouts geralmente precisa manter algum estado entre a medida e a organização, bem como entre as passagens de layout iterativas.
Inicialização e não inicialização do estado de Per-Container
Quando um layout é anexado a um contêiner, seu método InitializeForContextCore é chamado e oferece uma oportunidade de inicializar um objeto para armazenar o estado.
Da mesma forma, quando o layout estiver sendo removido de um contêiner, o método UninitializeForContextCore será chamado. Isso dá ao layout a oportunidade de limpar qualquer estado associado a esse contêiner.
O objeto de estado do layout pode ser armazenado e recuperado do contêiner com a propriedade LayoutState no contexto.
Virtualização da interface do usuário
Virtualização da interface do usuário significa atrasar a criação de um objeto de interface do usuário até quando for necessário. É uma otimização de desempenho. Para cenários de não rolagem que determinam quando necessário pode ser baseado em qualquer número de coisas específicas do aplicativo. Nesses casos, os aplicativos devem considerar o uso do x:Load. Ele não requer nenhuma manipulação especial em seu layout.
Em cenários baseados em rolagem, como uma lista, determinar quando necessário geralmente é baseado em "será visível para um usuário" que depende muito de onde ele foi colocado durante o processo de layout e requer considerações especiais. Esse cenário é um foco para este documento.
Observação
Embora não seja abordado neste documento, os mesmos recursos que permitem a virtualização da interface do usuário em cenários de rolagem podem ser aplicados em cenários que não são de rolagem. Por exemplo, um controle ToolBar controlado por dados que gerencia o tempo de vida dos comandos que ele apresenta e responde a alterações no espaço disponível reciclando/movendo elementos entre uma área visível e um menu de estouro.
Introdução
Primeiro, decida se o layout que você precisa criar deve dar suporte à virtualização da interface do usuário.
Algumas coisas para ter em mente...
- Layouts não virtualizados são mais fáceis de criar. Se o número de itens sempre for pequeno, é recomendável criar um layout não virtualizador.
- A plataforma fornece um conjunto de layouts anexados que funcionam com o ItemsRepeater e o LayoutPanel para cobrir as necessidades comuns. Familiarize-se com aqueles antes de decidir que você precisa definir um layout personalizado.
- A virtualização de layouts sempre tem algum custo/complexidade/sobrecarga adicional de CPU e memória em comparação com um layout não virtualizador. Como regra geral, se os filhos que o layout precisar gerenciar provavelmente se ajustarão em uma área que tenha 3x do tamanho do visor, talvez não haja muito ganho com um layout de virtualização. O tamanho 3x é discutido com mais detalhes mais adiante neste documento, mas é devido à natureza assíncrona da rolagem no Windows e seu impacto na virtualização.
Dica
Como ponto de referência, as configurações padrão para o ListView (e ItemsRepeater) são que a reciclagem não começa até que o número de itens seja suficiente para preencher 3x o tamanho do visor atual.
Escolha seu tipo base
O tipo layout base tem dois tipos derivados que servem como ponto de partida para criar um layout anexado:
Layout não virtualizado
A abordagem para criar um layout não virtualizado deve ser familiar para qualquer pessoa que tenha criado um Painel Personalizado. Os mesmos conceitos se aplicam. A principal diferença é que um NonVirtualizingLayoutContext é usado para acessar a coleção Children e o layout pode optar por armazenar o estado.
- Deriva do tipo base NonVirtualizingLayout (em vez de Panel).
- (Opcional) Defina as propriedades de dependência que, quando alteradas, invalidarão o layout.
- (Novo/Opcional) Inicialize qualquer objeto de estado exigido pelo layout como parte do InitializeForContextCore. Esconda-o com o contêiner de host usando o LayoutState fornecido com o contexto.
- Substitua o MeasureOverride e chame o método Measure em todos os filhos.
- Substitua o ArrangeOverride e chame o método Arrange em todos os filhos.
- (Novo/Opcional) Limpe qualquer estado salvo como parte do UninitializeForContextCore.
Exemplo: um layout de pilha simples ( itens deVarying-Sized)
Aqui está um layout de pilha não virtualizador muito básico de itens de tamanho variável. Ele não tem nenhuma propriedade para ajustar o comportamento do layout. A implementação a seguir ilustra como o layout depende do objeto de contexto fornecido pelo contêiner para:
- Obter a contagem de filhos, e
- Acesse cada elemento filho por índice.
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>
Virtualizando layouts
Semelhante a um layout não virtualizador, as etapas de alto nível para um layout de virtualização são as mesmas. A complexidade está em grande parte na determinação de quais elementos se enquadrarão no visor e devem ser realizados.
- Deriva do tipo base VirtualizingLayout.
- (Opcional) Defina as propriedades de dependência que, quando alteradas, invalidarão o layout.
- Inicialize qualquer objeto de estado que será exigido pelo layout como parte do InitializeForContextCore. Esconda-o com o contêiner de host usando o LayoutState fornecido com o contexto.
- Substitua o MeasureOverride e chame o método Measure para cada filho que deve ser realizado.
- O método GetOrCreateElementAt é usado para recuperar um UIElement que foi preparado pela estrutura (por exemplo, associações de dados aplicadas).
- Substitua o ArrangeOverride e chame o método Arrange para cada filho realizado.
- (Opcional) Limpe qualquer estado salvo como parte do UninitializeForContextCore.
Dica
O valor retornado pelo MeasureOverride é usado como o tamanho do conteúdo virtualizado.
Há duas abordagens gerais a serem consideradas ao criar um layout de virtualização. Se escolher um ou outro depende em grande parte de "como você determinará o tamanho de um elemento". Se for suficiente para saber o índice de um item no conjunto de dados ou os próprios dados ditarem seu tamanho eventual, o consideraremos dependente de dados. Eles são mais simples de criar. Se, no entanto, a única maneira de determinar o tamanho de um item for criar e medir a interface do usuário, diríamos que ela depende de conteúdo. Eles são mais complexos.
O processo de layout
Se você estiver criando um layout dependente de dados ou conteúdo, é importante entender o processo de layout e o impacto da rolagem assíncrona do Windows.
Uma exibição (over)simplificada das etapas executadas pela estrutura desde a inicialização até a exibição da interface do usuário na tela é a seguinte:
Ele analisa a marcação.
Gera uma árvore de elementos.
Executa uma passagem de layout.
Executa uma passagem de renderização.
Com a virtualização da interface do usuário, a criação dos elementos que normalmente seriam feitos na etapa 2 é atrasada ou encerrada mais cedo, uma vez que foi determinado que conteúdo suficiente foi criado para preencher o visor. Um contêiner de virtualização (por exemplo, ItemsRepeater) adia seu layout anexado para conduzir esse processo. Ele fornece o layout anexado com um VirtualizingLayoutContext que apresenta as informações adicionais necessárias para um layout de virtualização.
A RealizationRect (ou seja, Viewport)
A rolagem no Windows ocorre de forma assíncrona para o thread da interface do usuário. Ele não é controlado pelo layout da estrutura. Em vez disso, a interação e o movimento ocorrem no compositor do sistema. A vantagem dessa abordagem é que o conteúdo panorâmico sempre pode ser feito em 60fps. O desafio, no entanto, é que o "visor", como visto pelo layout, pode estar ligeiramente desatualizado em relação ao que é realmente visível na tela. Se um usuário rolar rapidamente, ele poderá superar a velocidade do thread de interface do usuário para gerar novo conteúdo e "pan to black". Por esse motivo, geralmente é necessário que um layout de virtualização gere um buffer adicional de elementos preparados suficiente para preencher uma área maior que o visor. Quando sob carga mais pesada durante a rolagem, o usuário ainda é apresentado com conteúdo.
Como a criação de elementos é cara, a virtualização de contêineres (por exemplo, ItemsRepeater) fornecerá inicialmente o layout anexado com um RealizationRect que corresponde ao visor. No tempo ocioso, o contêiner pode aumentar o buffer de conteúdo preparado fazendo chamadas repetidas para o layout usando uma retência de realização cada vez maior. Esse comportamento é uma otimização de desempenho que tenta encontrar um equilíbrio entre o tempo de inicialização rápido e uma boa experiência de movimento panorâmico. O tamanho máximo do buffer que o ItemsRepeater gerará é controlado por suas propriedades VerticalCacheLength e HorizontalCacheLength .
Reutilmente elementos (reciclagem)
Espera-se que o layout dimensione e posicione os elementos para preencher o RealizationRect sempre que ele for executado. Por padrão, o VirtualizingLayout reciclará todos os elementos não utilizados no final de cada passe de layout.
O VirtualizingLayoutContext que é passado para o layout como parte do MeasureOverride e ArrangeOverride fornece as informações adicionais que um layout de virtualização precisa. Algumas das coisas mais usadas que ele fornece são a capacidade de:
- Consulte o número de itens nos dados (ItemCount).
- Recupere um item específico usando o método GetItemAt .
- Recupere um RealizationRect que representa o visor e o buffer que o layout deve preencher com elementos realizados.
- Solicite o UIElement para um item específico com o método GetOrCreateElement .
Solicitar um elemento para um determinado índice fará com que esse elemento seja marcado como "em uso" para essa passagem do layout. Se o elemento ainda não existir, ele será realizado e preparado automaticamente para uso (por exemplo, inflando a árvore de interface do usuário definida em um DataTemplate, processando qualquer associação de dados etc.). Caso contrário, ele será recuperado de um pool de instâncias existentes.
No final de cada aprovação de medida, qualquer elemento existente e realizado que não foi marcado como "em uso" é automaticamente considerado disponível para reutilização, a menos que a opção de SuppressAutoRecycle tenha sido usada quando o elemento foi recuperado por meio do método GetOrCreateElementAt . A estrutura a move automaticamente para um pool de reciclagem e a disponibiliza. Posteriormente, ele pode ser puxado para uso por um contêiner diferente. A estrutura tenta evitar isso quando possível, pois há algum custo associado à nova criação de um elemento.
Se um layout de virtualização souber no início de cada medida quais elementos não se enquadrarão mais na reticência de realização, ele poderá otimizar seu reutilização. Em vez de depender do comportamento padrão da estrutura. O layout pode mover elementos preventivamente para o pool de reciclagem usando o método RecycleElement . Chamar esse método antes de solicitar novos elementos faz com que esses elementos existentes fiquem disponíveis quando o layout mais tarde emite uma solicitação GetOrCreateElementAt para um índice que ainda não está associado a um elemento.
O VirtualizingLayoutContext fornece duas propriedades adicionais projetadas para autores de layout criando um layout dependente de conteúdo. Eles são discutidos mais detalhadamente mais tarde.
- Um RecommendedAnchorIndex que fornece uma entrada opcional para o layout.
- Um LayoutOrigin que é uma saída opcional do layout.
Layouts de Virtualização dependentes de dados
Um layout de virtualização será mais fácil se você souber qual deve ser o tamanho de cada item sem precisar medir o conteúdo a ser mostrado. Neste documento, vamos simplesmente nos referir a essa categoria de virtualização de layouts como layouts de dados , pois eles geralmente envolvem inspecionar os dados. Com base nos dados, um aplicativo pode escolher uma representação visual com um tamanho conhecido - talvez porque sua parte dos dados ou foi determinada anteriormente pelo design.
A abordagem geral é para o layout:
- Calcular um tamanho e uma posição de cada item.
- Como parte do MeasureOverride:
- Use o RealizationRect para determinar quais itens devem aparecer no visor.
- Recupere o UIElement que deve representar o item com o método GetOrCreateElementAt .
- Meça o UIElement com o tamanho pré-calculado.
- Como parte do ArrangeOverride, organize cada UIElement realizado com a posição pré-calculada.
Observação
Uma abordagem de layout de dados geralmente é incompatível com a virtualização de dados. Especificamente, em que os únicos dados carregados na memória são os dados necessários para preencher o que está visível para o usuário. A virtualização de dados não está se referindo ao carregamento lento ou incremental de dados à medida que um usuário rola para baixo onde esses dados permanecem residentes. Em vez disso, ele está se referindo a quando os itens são liberados da memória à medida que são rolados para fora da exibição. Ter um layout de dados que inspecione cada item de dados como parte de um layout de dados impediria que a virtualização de dados funcionasse conforme o esperado. Uma exceção é um layout como o UniformGridLayout que pressupõe que tudo tenha o mesmo tamanho.
Dica
Se você estiver criando um controle personalizado para uma biblioteca de controle que será usada por outras pessoas em uma ampla variedade de situações, um layout de dados poderá não ser uma opção para você.
Exemplo: layout do Feed de Atividades do Xbox
A interface do usuário para o Feed de Atividades do Xbox usa um padrão de repetição em que cada linha tem um bloco largo, seguido por dois blocos estreitos invertidos na linha subsequente. Nesse layout, o tamanho de cada item é uma função da posição do item no conjunto de dados e do tamanho conhecido dos blocos (largo versus estreito).
O código a seguir explica o que pode ser uma interface do usuário de virtualização personalizada para o feed de atividades para ilustrar a abordagem geral que você pode adotar para um layout de dados.
Dica
Se você tiver o aplicativo WinUI 3 Gallery instalado, clique aqui para abrir o aplicativo e ver o ItemsRepeater em ação. Obtenha o aplicativo na Microsoft Store ou obtenha o código-fonte no 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;
}
(Opcional) Gerenciando o item para o mapeamento UIElement
Por padrão, o VirtualizingLayoutContext mantém um mapeamento entre os elementos realizados e o índice na fonte de dados que eles representam. Um layout pode optar por gerenciar esse mapeamento em si sempre solicitando a opção de SuppressAutoRecycle ao recuperar um elemento por meio do método GetOrCreateElementAt que impede o comportamento padrão de reciclagem automática. Um layout pode optar por fazer isso, por exemplo, se ele só será usado quando a rolagem for restrita a uma direção e os itens que ele considera sempre serão contíguos (ou seja, saber o índice do primeiro e do último elemento é suficiente para conhecer todos os elementos que devem ser realizados).
Exemplo: medida do Feed de Atividades do Xbox
O snippet abaixo mostra a lógica adicional que pode ser adicionada ao MeasureOverride no exemplo anterior para gerenciar o mapeamento.
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; }
// ...
}
Layouts de Virtualização dependentes de conteúdo
Se você precisar primeiro medir o conteúdo da interface do usuário de um item para descobrir seu tamanho exato, ele será um layout dependente do conteúdo. Você também pode pensar nele como um layout em que cada item deve ser dimensionado em vez do layout informando o tamanho do item. Os layouts de virtualização que se enquadram nessa categoria estão mais envolvidos.
Observação
Layouts dependentes de conteúdo não (não devem) interromper a virtualização de dados.
Estimativas
Os layouts dependentes de conteúdo dependem da estimativa para adivinhar o tamanho do conteúdo não realizado e a posição do conteúdo realizado. À medida que essas estimativas forem alteradas, o conteúdo realizado mudará regularmente as posições dentro da área rolável. Isso pode levar a uma experiência de usuário muito frustrante e chocante se não for atenuada. Os possíveis problemas e mitigações são discutidos aqui.
Observação
Layouts de dados que consideram cada item e sabem o tamanho exato de todos os itens, realizados ou não, e suas posições podem evitar esses problemas completamente.
Ancoragem de rolagem
O XAML fornece um mecanismo para atenuar as mudanças repentinas do visor, tendo controles de rolagem compatíveis com a ancoragem de rolagem implementando a interface IScrollAnchorPovider . À medida que o usuário manipula o conteúdo, o controle de rolagem seleciona continuamente um elemento do conjunto de candidatos que foram aceitos para serem rastreados. Se a posição do elemento de âncora mudar durante o layout, o controle de rolagem mudará automaticamente seu visor para manter o visor.
O valor do RecommendedAnchorIndex fornecido ao layout pode refletir esse elemento de âncora selecionado no momento escolhido pelo controle de rolagem. Como alternativa, se um desenvolvedor solicitar explicitamente que um elemento seja realizado para um índice com o método GetOrCreateElement no ItemsRepeater, esse índice será fornecido como RecommendedAnchorIndex na próxima passagem de layout. Isso permite que o layout seja preparado para o cenário provável em que um desenvolvedor percebe um elemento e, posteriormente, solicita que ele seja colocado em exibição por meio do método StartBringIntoView .
O RecommendedAnchorIndex é o índice do item na fonte de dados que um layout dependente de conteúdo deve posicionar primeiro ao estimar a posição de seus itens. Ele deve servir como ponto de partida para posicionar outros itens realizados.
Impacto nas Barras de Rolagem
Mesmo com a ancoragem de rolagem, se as estimativas do layout variarem muito, talvez devido a variações significativas no tamanho do conteúdo, a posição do polegar para a Barra de Rolagem poderá parecer saltar ao redor. Isso pode ser chocante para um usuário se o polegar não parecer rastrear a posição do ponteiro do mouse quando ele estiver arrastando-o.
Quanto mais preciso o layout puder estar em suas estimativas, menor a probabilidade de um usuário ver o polegar da Barra de Rolagem saltando.
Correções de layout
Um layout dependente de conteúdo deve ser preparado para racionalizar sua estimativa com a realidade. Por exemplo, à medida que o usuário rola até a parte superior do conteúdo e o layout percebe o primeiro elemento, ele pode descobrir que a posição antecipada do elemento em relação ao elemento do qual ele começou faria com que ele aparecesse em algum lugar diferente da origem (x:0, y:0). Quando isso ocorre, o layout pode usar a propriedade LayoutOrigin para definir a posição calculada como a nova origem do layout. O resultado da rede é semelhante à ancoragem de rolagem na qual o visor do controle de rolagem é ajustado automaticamente para considerar a posição do conteúdo, conforme relatado pelo layout.
Visores desconectados
O tamanho retornado do método MeasureOverride do layout representa o melhor palpite no tamanho do conteúdo que pode mudar com cada layout sucessivo. À medida que um usuário rola, o layout será reavaliado continuamente com uma RealizationRect atualizada.
Se um usuário arrastar o polegar muito rapidamente, é possível que o visor, da perspectiva do layout, apareça para fazer grandes saltos em que a posição anterior não se sobreponha à posição atual. Isso ocorre devido à natureza assíncrona da rolagem. Também é possível que um aplicativo que está consumindo o layout solicite que um elemento seja colocado em exibição para um item que não foi realizado no momento e que está estimado para ficar fora do intervalo atual acompanhado pelo layout.
Quando o layout descobre que seu palpite está incorreto e/ou vê uma mudança inesperada no visor, ele precisa reorientar sua posição inicial. Os layouts de virtualização que são fornecidos como parte dos controles XAML são desenvolvidos como layouts dependentes de conteúdo, pois impõem menos restrições à natureza do conteúdo que será mostrado.
Exemplo: layout de pilha de virtualização simples para itens de Variable-Sized
O exemplo a seguir demonstra um layout de pilha simples para itens de tamanho variável que:
- dá suporte à virtualização da interface do usuário,
- usa estimativas para adivinhar o tamanho de itens não realizados,
- está ciente de possíveis mudanças de visor descontinuadas e
- aplica correções de layout para considerar esses turnos.
Uso: Marcação
<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;
Código: 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);
}
}
Artigos relacionados
Windows developer