Browse Source

wip

virtualizing_grid
Emmanuel Hansen 3 years ago
parent
commit
be7394b9e1
  1. 4
      samples/ControlCatalog/Pages/AcrylicPage.xaml
  2. 438
      src/Avalonia.Controls/Utils/RealizedGridElements.cs
  3. 686
      src/Avalonia.Controls/VirtualizingUniformGrid.cs

4
samples/ControlCatalog/Pages/AcrylicPage.xaml

@ -55,9 +55,10 @@
</Grid>
</ExperimentalAcrylicBorder>
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" Height="200">
<UniformGrid x:Name="BordersGrid"
HorizontalAlignment="Stretch"
MaxWidth="660"
Columns="3">
<UniformGrid.Styles>
<Style Selector="ExperimentalAcrylicBorder">
@ -149,6 +150,7 @@
</ExperimentalAcrylicBorder>
</UniformGrid>
</ScrollViewer>
<ExperimentalAcrylicBorder Width="{Binding #BordersGrid.Bounds.Width}"

438
src/Avalonia.Controls/Utils/RealizedGridElements.cs

@ -0,0 +1,438 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities;
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Stores the realized element state for a virtualizing panel that arranges its children
/// in a stack layout, such as <see cref="VirtualizingUniformGrid"/>.
/// </summary>
internal class RealizedGridElements
{
private int _firstIndex;
private List<Control?>? _elements;
private List<Size>? _sizes;
/// <summary>
/// Gets the number of realized elements.
/// </summary>
public int Count => _elements?.Count ?? 0;
/// <summary>
/// Gets the index of the first realized element, or -1 if no elements are realized.
/// </summary>
public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1;
/// <summary>
/// Gets the index of the last realized element, or -1 if no elements are realized.
/// </summary>
public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1;
/// <summary>
/// Gets the elements.
/// </summary>
public IReadOnlyList<Control?> Elements => _elements ??= new List<Control?>();
/// <summary>
/// Gets the sizes of the elements on the primary axis.
/// </summary>
public IReadOnlyList<Size> Sizes => _sizes ??= new List<Size>();
public int RowCount { get; set; }
public int ColumnCount { get; set; }
public int FirstColumn { get; set; }
/// <summary>
/// Adds a newly realized element to the collection.
/// </summary>
/// <param name="index">The index of the element.</param>
/// <param name="element">The element.</param>
/// <param name="size">The coordinates of the element.</param>
public void Add(int index, Control element, Size size)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
_elements ??= new List<Control?>();
_sizes ??= new List<Size>();
if (Count == 0)
{
_elements.Add(element);
_sizes.Add(size);
_firstIndex = index;
}
else if (index == LastIndex + 1)
{
_elements.Add(element);
_sizes.Add(size);
}
else if (index == FirstIndex - 1)
{
--_firstIndex;
_elements.Insert(0, element);
_sizes.Insert(0, size);
}
else
{
throw new NotSupportedException("Can only add items to the beginning or end of realized elements.");
}
}
/// <summary>
/// Gets the element at the specified index, if realized.
/// </summary>
/// <param name="index">The index in the source collection of the element to get.</param>
/// <returns>The element if realized; otherwise null.</returns>
public Control? GetElement(int index)
{
var i = index - FirstIndex;
if (i >= 0 && i < _elements?.Count)
return _elements[i];
return null;
}
/// <summary>
/// Gets or estimates the index and start U position of the anchor element for the
/// specified viewport.
/// </summary>
/// <param name="viewportStart">The position of the start of the viewport.</param>
/// <param name="viewportEnd">The position of the end of the viewport.</param>
/// <param name="itemCount">The number of items in the list.</param>
/// <param name="estimatedElementSize">The current estimated element size.</param>
/// <returns>
/// A tuple containing:
/// - The index of the anchor element, or -1 if an anchor could not be determined
/// - The U position of the start of the anchor element, if determined
/// </returns>
/// <remarks>
/// This method tries to find an existing element in the specified viewport from which
/// element realization can start. Failing that it estimates the first element in the
/// viewport.
/// </remarks>
public (int index, Vector coord, Vector lastCoord) GetOrEstimateAnchorElementForViewport(
Point viewportStart,
Point viewportEnd,
int itemCount,
ref Size estimatedElementSize)
{
// We have no elements, nothing to do here.
if (itemCount <= 0)
return (-1, Vector.Zero, Vector.Zero);
if (_sizes is not null)
{
var maxWidth = _sizes.Max(x => x.Width);
var maxHeight = _sizes.Max(x => x.Height);
estimatedElementSize = new Size(maxWidth, maxHeight);
}
var MaxWidth = ColumnCount * estimatedElementSize.Width;
var MaxHeight = RowCount * estimatedElementSize.Height;
var x = Math.Min((int)(Math.Min(viewportStart.X, MaxWidth) / estimatedElementSize.Width), ColumnCount);
var y = Math.Min((int)(Math.Min(viewportStart.Y, MaxHeight) / estimatedElementSize.Height), RowCount);
var lastY = Math.Min((int)(Math.Min(viewportEnd.Y, MaxHeight) / estimatedElementSize.Height), RowCount);
var lastX = lastY > 0 ? ColumnCount : Math.Min((int)(Math.Min(viewportEnd.X, MaxWidth) / estimatedElementSize.Width), ColumnCount);
return (Math.Max(y * ColumnCount + x - FirstColumn, 0), new Vector(x, y), new Vector(lastX, lastY));
}
/// <summary>
/// Gets the position of the element with the requested index on the primary axis, if realized.
/// </summary>
/// <returns>
/// The position of the element, or NaN if the element is not realized.
/// </returns>
public Vector GetElementCoord(int index)
{
var div = Math.DivRem(index + FirstColumn, ColumnCount, out int rem);
return new Vector(rem, div);
}
public Vector GetOrEstimateElementU(int index)
{
return GetElementCoord(index);
}
/// <summary>
/// Gets the index of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The index or -1 if the element is not present in the collection.</returns>
public int GetIndex(Control element)
{
return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1;
}
/// <summary>
/// Updates the elements in response to items being inserted into the source collection.
/// </summary>
/// <param name="index">The index in the source collection of the insert.</param>
/// <param name="count">The number of items inserted.</param>
/// <param name="updateElementIndex">A method used to update the element indexes.</param>
public void ItemsInserted(int index, int count, Action<Control, int, int> updateElementIndex)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (_elements is null || _elements.Count == 0)
return;
// Get the index within the realized _elements collection.
var first = FirstIndex;
var realizedIndex = index - first;
if (realizedIndex < Count)
{
// The insertion point affects the realized elements. Update the index of the
// elements after the insertion point.
var elementCount = _elements.Count;
var start = Math.Max(realizedIndex, 0);
var newIndex = realizedIndex + count;
for (var i = start; i < elementCount; ++i)
{
if (_elements[i] is Control element)
updateElementIndex(element, newIndex - count, newIndex);
++newIndex;
}
if (realizedIndex < 0)
{
// The insertion point was before the first element, update the first index.
_firstIndex += count;
}
else
{
// The insertion point was within the realized elements, insert an empty space
// in _elements and _sizes.
_elements!.InsertMany(realizedIndex, null, count);
_sizes!.InsertMany(realizedIndex, Size.Infinity, count);
}
}
}
/// <summary>
/// Updates the elements in response to items being removed from the source collection.
/// </summary>
/// <param name="index">The index in the source collection of the remove.</param>
/// <param name="count">The number of items removed.</param>
/// <param name="updateElementIndex">A method used to update the element indexes.</param>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void ItemsRemoved(
int index,
int count,
Action<Control, int, int> updateElementIndex,
Action<Control> recycleElement)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
if (_elements is null || _elements.Count == 0)
return;
// Get the removal start and end index within the realized _elements collection.
var first = FirstIndex;
var last = LastIndex;
var startIndex = index - first;
var endIndex = (index + count) - first;
if (endIndex < 0)
{
// The removed range was before the realized elements. Update the first index and
// the indexes of the realized elements.
_firstIndex -= count;
var newIndex = _firstIndex;
for (var i = 0; i < _elements.Count; ++i)
{
if (_elements[i] is Control element)
updateElementIndex(element, newIndex - count, newIndex);
++newIndex;
}
}
else if (startIndex < _elements.Count)
{
// Recycle and remove the affected elements.
var start = Math.Max(startIndex, 0);
var end = Math.Min(endIndex, _elements.Count);
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
{
_elements[i] = null;
recycleElement(element);
}
}
_elements.RemoveRange(start, end - start);
_sizes!.RemoveRange(start, end - start);
// If the remove started before and ended within our realized elements, then our new
// first index will be the index where the remove started. Mark StartU as unstable
// because we can't rely on it now to estimate element heights.
if (startIndex <= 0 && end < last)
{
_firstIndex = first = index;
}
// Update the indexes of the elements after the removed range.
end = _elements.Count;
var newIndex = first + start;
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
updateElementIndex(element, newIndex + count, newIndex);
++newIndex;
}
}
}
/// <summary>
/// Recycles all elements in response to the source collection being reset.
/// </summary>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void ItemsReset(Action<Control> recycleElement)
{
if (_elements is null || _elements.Count == 0)
return;
for (var i = 0; i < _elements.Count; i++)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e);
}
}
_elements?.Clear();
_sizes?.Clear();
}
/// <summary>
/// Recycles elements before a specific index.
/// </summary>
/// <param name="index">The index in the source collection of new first element.</param>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void RecycleElementsBefore(int index, Action<Control, int> recycleElement)
{
if (index <= FirstIndex || _elements is null || _elements.Count == 0)
return;
if (index > LastIndex)
{
RecycleAllElements(recycleElement);
}
else
{
var endIndex = index - FirstIndex;
for (var i = 0; i < endIndex; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(0, endIndex);
_sizes!.RemoveRange(0, endIndex);
_firstIndex = index;
}
}
/// <summary>
/// Recycles elements after a specific index.
/// </summary>
/// <param name="index">The index in the source collection of new last element.</param>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void RecycleElementsAfter(int index, Action<Control, int> recycleElement)
{
if (index >= LastIndex || _elements is null || _elements.Count == 0)
return;
if (index < FirstIndex)
{
RecycleAllElements(recycleElement);
}
else
{
var startIndex = (index + 1) - FirstIndex;
var count = _elements.Count;
for (var i = startIndex; i < count; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(startIndex, _elements.Count - startIndex);
_sizes!.RemoveRange(startIndex, _sizes.Count - startIndex);
}
}
public void RecycleElement(int index, Action<Control, int> recycleElement)
{
if (index >= LastIndex || _elements is null || _elements.Count == 0)
return;
var startIndex = index - FirstIndex;
var count = 1;
for (var i = startIndex; i < count; ++i)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements.RemoveRange(startIndex, 1);
_sizes!.RemoveRange(startIndex, 1);
}
/// <summary>
/// Recycles all realized elements.
/// </summary>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void RecycleAllElements(Action<Control, int> recycleElement)
{
if (_elements is null || _elements.Count == 0)
return;
for (var i = 0; i < _elements.Count; i++)
{
if (_elements[i] is Control e)
{
_elements[i] = null;
recycleElement(e, i + FirstIndex);
}
}
_elements?.Clear();
_sizes?.Clear();
}
/// <summary>
/// Resets the element list and prepares it for reuse.
/// </summary>
public void ResetForReuse()
{
_elements?.Clear();
_sizes?.Clear();
}
}
}

686
src/Avalonia.Controls/VirtualizingUniformGrid.cs

@ -0,0 +1,686 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Emit;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// A <see cref="VirtualizingPanel"/> with uniform column and row sizes.
/// </summary>
public class VirtualizingUniformGrid : VirtualizingPanel
{
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
/// <summary>
/// Defines the <see cref="Rows"/> property.
/// </summary>
public static readonly StyledProperty<int> RowsProperty =
AvaloniaProperty.Register<VirtualizingUniformGrid, int>(nameof(Rows));
/// <summary>
/// Defines the <see cref="Columns"/> property.
/// </summary>
public static readonly StyledProperty<int> ColumnsProperty =
AvaloniaProperty.Register<VirtualizingUniformGrid, int>(nameof(Columns));
/// <summary>
/// Defines the <see cref="FirstColumn"/> property.
/// </summary>
public static readonly StyledProperty<int> FirstColumnProperty =
AvaloniaProperty.Register<VirtualizingUniformGrid, int>(nameof(FirstColumn));
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private readonly Action<Control, int> _recycleElement;
private readonly Action<Control> _recycleElementOnItemRemoved;
private readonly Action<Control, int, int> _updateElementIndex;
private int _rows;
private int _columns;
private int _scrollToIndex = -1;
private Control? _scrollToElement;
private bool _isInLayout;
private bool _isWaitingForViewportUpdate;
private RealizedGridElements? _measureElements;
private RealizedGridElements? _realizedElements;
private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
private object s_itemIsItsOwnContainer = new object();
private Size _lastEstimatedElementSize = new Size(25, 25);
private MeasureViewport _measuredViewport;
static VirtualizingUniformGrid()
{
AffectsMeasure<VirtualizingUniformGrid>(RowsProperty, ColumnsProperty, FirstColumnProperty);
}
public VirtualizingUniformGrid()
{
_recycleElement = RecycleElement;
_recycleElementOnItemRemoved = RecycleElementOnItemRemoved;
_updateElementIndex = UpdateElementIndex;
EffectiveViewportChanged += OnEffectiveViewportChanged;
}
/// <summary>
/// Specifies the row count. If set to 0, row count will be calculated automatically.
/// </summary>
public int Rows
{
get => GetValue(RowsProperty);
set => SetValue(RowsProperty, value);
}
/// <summary>
/// Specifies the column count. If set to 0, column count will be calculated automatically.
/// </summary>
public int Columns
{
get => GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, value);
}
/// <summary>
/// Specifies, for the first row, the column where the items should start.
/// </summary>
public int FirstColumn
{
get => GetValue(FirstColumnProperty);
set => SetValue(FirstColumnProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
UpdateRowsAndColumns();
var items = Items;
if (items.Count == 0)
return default;
// If we're bringing an item into view, ignore any layout passes until we receive a new
// effective viewport.
if (_isWaitingForViewportUpdate)
return DesiredSize;
_isInLayout = true;
try
{
_realizedElements ??= new();
_measureElements ??= new();
var viewport = CalculateMeasureViewport(items);
if (_viewport.Size == default)
return DesiredSize;
if (viewport.viewportIsDisjunct)
_realizedElements.RecycleAllElements(_recycleElement);
RealizeElements(items, availableSize, ref viewport);
// Now swap the measureElements and realizedElements collection.
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();
_measuredViewport = viewport;
return CalculateDesiredSize();
}
finally
{
_isInLayout = false;
}
}
private Size CalculateDesiredSize()
{
return new Size(_lastEstimatedElementSize.Width * _columns, _lastEstimatedElementSize.Height * _rows);
}
private Rect EstimateViewport()
{
var c = this.GetVisualParent();
var viewport = new Rect();
if (c is null)
{
return viewport;
}
while (c is not null)
{
if ((c.Bounds.Width != 0 || c.Bounds.Height != 0) &&
c.TransformToVisual(this) is Matrix transform)
{
viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height)
.TransformToAABB(transform);
break;
}
c = c?.GetVisualParent();
}
return viewport;
}
protected override Size ArrangeOverride(Size finalSize)
{
var div = Math.DivRem(_measuredViewport.anchorIndex + FirstColumn, _columns, out var rem);
var x = rem;
var y = div;
var width = finalSize.Width / _columns;
var height = finalSize.Height / _rows;
for (var i = _measuredViewport.anchorIndex; i < Children.Count; i++)
{
if (i > _measuredViewport.lastIndex)
break;
var child = Children[i];
if (!child.IsVisible)
{
continue;
}
child.Arrange(new Rect(x * width, y * height, width, height));
x++;
if (x >= _columns)
{
x = 0;
y++;
}
}
return finalSize;
}
private void UpdateRowsAndColumns()
{
_rows = Rows;
_columns = Columns;
if (FirstColumn >= Columns)
{
SetCurrentValue(FirstColumnProperty, 0);
}
var itemCount = FirstColumn + Items.Count;
if (_rows == 0)
{
if (_columns == 0)
{
_rows = _columns = (int)Math.Ceiling(Math.Sqrt(itemCount));
}
else
{
_rows = Math.DivRem(itemCount, _columns, out int rem);
if (rem != 0)
{
_rows++;
}
}
}
else if (_columns == 0)
{
_columns = Math.DivRem(itemCount, _rows, out int rem);
if (rem != 0)
{
_columns++;
}
}
if (_realizedElements != null)
{
_realizedElements.RowCount = _rows;
_realizedElements.ColumnCount = _columns;
_realizedElements.FirstColumn = FirstColumn;
}
if (_measureElements != null)
{
_measureElements.RowCount = _rows;
_measureElements.ColumnCount = _columns;
_measureElements.FirstColumn = FirstColumn;
}
}
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
{
throw new NotImplementedException();
}
private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
{
var oldViewportStart = new Point(_viewport.Top, _viewport.Left);
var oldViewportEnd = new Point(_viewport.Bottom, _viewport.Right);
_viewport = e.EffectiveViewport.Intersect(new(Bounds.Size));
_isWaitingForViewportUpdate = false;
var newViewportStart = new Point(_viewport.Top, _viewport.Left);
var newViewportEnd = new Point(_viewport.Bottom, _viewport.Right);
if (!MathUtilities.AreClose(oldViewportStart.X, newViewportStart.X) ||
!MathUtilities.AreClose(oldViewportEnd.X, newViewportEnd.X) ||
!MathUtilities.AreClose(oldViewportStart.Y, newViewportStart.Y) ||
!MathUtilities.AreClose(oldViewportEnd.Y, newViewportEnd.Y))
{
InvalidateMeasure();
}
}
private void RealizeElements(
IReadOnlyList<object?> items,
Size availableSize,
ref MeasureViewport viewport)
{
Debug.Assert(_measureElements is not null);
Debug.Assert(_realizedElements is not null);
Debug.Assert(items.Count > 0);
var index = viewport.anchorIndex;
var lastIndex = index;
// Start at the anchor element and move forwards, realizing elements.
do
{
if (!IsIndexVisible(index, viewport.anchorCoord, viewport.endCoord))
{
_realizedElements.RecycleElement(index, _recycleElement);
++index;
continue;
}
var e = GetOrCreateElement(items, index);
e.Measure(availableSize);
var size = new Size(e.DesiredSize.Width, e.DesiredSize.Height);
_measureElements!.Add(index, e, size);
_lastEstimatedElementSize = size;
lastIndex = index;
++index;
} while (index < items.Count);
// Store the last index for the desired size calculation.
viewport.lastIndex = lastIndex;
// We can now recycle elements before the first element.
_realizedElements.RecycleElementsBefore(viewport.anchorIndex, _recycleElement);
}
private bool IsIndexVisible(int index, Vector start, Vector end)
{
var div = Math.DivRem(index, _columns, out var rem);
var coord = new Vector(rem, div);
return coord.X >= start.X && coord.Y >= start.Y &&
coord.X <= end.X && coord.Y <= end.Y;
}
private MeasureViewport CalculateMeasureViewport(IReadOnlyList<object?> items)
{
Debug.Assert(_realizedElements is not null);
// If the control has not yet been laid out then the effective viewport won't have been set.
// Try to work it out from an ancestor control.
var viewport = _viewport != s_invalidViewport ? _viewport : EstimateViewport();
// Get the viewport in the orientation direction.
var viewportStart = new Point(viewport.X, viewport.Y);
var viewportEnd = new Point(viewport.Right, viewport.Bottom);
// Get or estimate the anchor element from which to start realization.
var itemCount = items?.Count ?? 0;
var (anchorIndex, anchor, end) = _realizedElements.GetOrEstimateAnchorElementForViewport(
viewportStart,
viewportEnd,
itemCount,
ref _lastEstimatedElementSize);
// Check if the anchor element is not within the currently realized elements.
var disjunct = anchorIndex < _realizedElements.FirstIndex ||
anchorIndex > _realizedElements.LastIndex;
return new MeasureViewport
{
anchorIndex = anchorIndex,
anchorCoord = anchor,
viewportStart = viewportStart,
viewportEnd = viewportEnd,
viewportIsDisjunct = disjunct,
endCoord = end,
};
}
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
private Control? GetRealizedElement(int index)
{
if (_scrollToIndex == index)
return _scrollToElement;
return _realizedElements?.GetElement(index);
}
private Control GetItemAsOwnContainer(object? item, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (!controlItem.IsSet(RecycleKeyProperty))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
{
var element = _unrealizedFocusedElement;
_unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
_unrealizedFocusedElement = null;
_unrealizedFocusedIndex = -1;
return element;
}
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
return recycled;
}
return null;
}
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator!;
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
return container;
}
private void RecycleElement(Control element, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
_scrollViewer?.UnregisterAnchorCandidate(element);
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
else if (element.IsKeyboardFocusWithin)
{
_unrealizedFocusedElement = element;
_unrealizedFocusedIndex = index;
_unrealizedFocusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus;
}
else
{
ItemContainerGenerator!.ClearItemContainer(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
private void RecycleElementOnItemRemoved(Control element)
{
Debug.Assert(ItemContainerGenerator is not null);
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
RemoveInternalChild(element);
}
else
{
ItemContainerGenerator!.ClearItemContainer(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
private void PushToRecyclePool(object recycleKey, Control element)
{
_recyclePool ??= new();
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
}
protected override void OnItemsChanged(IReadOnlyList<object?> items, NotifyCollectionChangedEventArgs e)
{
InvalidateMeasure();
if (_realizedElements is null)
return;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
_realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
break;
case NotifyCollectionChangedAction.Remove:
_realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
_realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
_realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
break;
case NotifyCollectionChangedAction.Reset:
_realizedElements.ItemsReset(_recycleElementOnItemRemoved);
break;
}
}
private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
{
Debug.Assert(ItemContainerGenerator is not null);
ItemContainerGenerator.ItemContainerIndexChanged(element, oldIndex, newIndex);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_scrollViewer = this.FindAncestorOfType<ScrollViewer>();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_scrollViewer = null;
}
protected internal override Control? ScrollIntoView(int index)
{
var items = Items;
if (_isInLayout || index < 0 || index >= items.Count || _realizedElements is null)
return null;
if (GetRealizedElement(index) is Control element)
{
element.BringIntoView();
return element;
}
else if (this.GetVisualRoot() is ILayoutRoot root)
{
// Create and measure the element to be brought into view. Store it in a field so that
// it can be re-used in the layout pass.
_scrollToElement = GetOrCreateElement(items, index);
_scrollToElement.Measure(Size.Infinity);
_scrollToIndex = index;
// Get the expected position of the elment and put it in place.
var anchor = _realizedElements.GetOrEstimateElementU(index);
var rect = new Rect(anchor.X, anchor.Y, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height);
_scrollToElement.Arrange(rect);
// If the item being brought into view was added since the last layout pass then
// our bounds won't be updated, so any containing scroll viewers will not have an
// updated extent. Do a layout pass to ensure that the containing scroll viewers
// will be able to scroll the new item into view.
if (!Bounds.Contains(rect) && !_viewport.Contains(rect))
{
_isWaitingForViewportUpdate = true;
root.LayoutManager.ExecuteLayoutPass();
_isWaitingForViewportUpdate = false;
}
// Try to bring the item into view.
_scrollToElement.BringIntoView();
// If the viewport does not contain the item to scroll to, set _isWaitingForViewportUpdate:
// this should cause the following chain of events:
// - Measure is first done with the old viewport (which will be a no-op, see MeasureOverride)
// - The viewport is then updated by the layout system which invalidates our measure
// - Measure is then done with the new viewport.
_isWaitingForViewportUpdate = !_viewport.Contains(rect);
root.LayoutManager.ExecuteLayoutPass();
// If for some reason the layout system didn't give us a new viewport during the layout, we
// need to do another layout pass as the one that took place was a no-op.
if (_isWaitingForViewportUpdate)
{
_isWaitingForViewportUpdate = false;
InvalidateMeasure();
root.LayoutManager.ExecuteLayoutPass();
}
var result = _scrollToElement;
_scrollToElement = null;
_scrollToIndex = -1;
return result;
}
return null;
}
protected internal override Control? ContainerFromIndex(int index)
{
if (index < 0 || index >= Items.Count)
return null;
if (_realizedElements?.GetElement(index) is { } realized)
return realized;
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1;
protected internal override IEnumerable<Control>? GetRealizedContainers()
{
return _realizedElements?.Elements.Where(x => x is not null)!;
}
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var count = Items.Count;
if (count == 0 || from is not Control fromControl)
return null;
var fromIndex = from != null ? IndexFromContainer(fromControl) : -1;
var toIndex = fromIndex;
// implement
return ScrollIntoView(toIndex);
}
private struct MeasureViewport
{
public int anchorIndex;
public Vector anchorCoord;
public Point viewportStart;
public Point viewportEnd;
public int lastIndex;
public bool viewportIsDisjunct;
public Vector endCoord;
}
}
}
Loading…
Cancel
Save