csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
927 lines
36 KiB
927 lines
36 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Utils;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Utilities;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Controls
|
|
{
|
|
/// <summary>
|
|
/// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
|
|
/// </summary>
|
|
public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo
|
|
{
|
|
/// <summary>
|
|
/// Defines the <see cref="Orientation"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<Orientation> OrientationProperty =
|
|
StackPanel.OrientationProperty.AddOwner<VirtualizingStackPanel>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
|
|
AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreHorizontalSnapPointsRegular));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
|
|
AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreVerticalSnapPointsRegular));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
|
|
RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
|
|
nameof(HorizontalSnapPointsChanged),
|
|
RoutingStrategies.Bubble);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="VerticalSnapPointsChanged"/> event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
|
|
RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
|
|
nameof(VerticalSnapPointsChanged),
|
|
RoutingStrategies.Bubble);
|
|
|
|
private static readonly AttachedProperty<object?> RecycleKeyProperty =
|
|
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
|
|
|
|
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
|
|
private static readonly object s_itemIsItsOwnContainer = new object();
|
|
private readonly Action<Control, int> _recycleElement;
|
|
private readonly Action<Control> _recycleElementOnItemRemoved;
|
|
private readonly Action<Control, int, int> _updateElementIndex;
|
|
private int _scrollToIndex = -1;
|
|
private Control? _scrollToElement;
|
|
private bool _isInLayout;
|
|
private bool _isWaitingForViewportUpdate;
|
|
private double _lastEstimatedElementSizeU = 25;
|
|
private RealizedStackElements? _measureElements;
|
|
private RealizedStackElements? _realizedElements;
|
|
private ScrollViewer? _scrollViewer;
|
|
private Rect _viewport = s_invalidViewport;
|
|
private Dictionary<object, Stack<Control>>? _recyclePool;
|
|
private Control? _unrealizedFocusedElement;
|
|
private int _unrealizedFocusedIndex = -1;
|
|
|
|
public VirtualizingStackPanel()
|
|
{
|
|
_recycleElement = RecycleElement;
|
|
_recycleElementOnItemRemoved = RecycleElementOnItemRemoved;
|
|
_updateElementIndex = UpdateElementIndex;
|
|
EffectiveViewportChanged += OnEffectiveViewportChanged;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the axis along which items are laid out.
|
|
/// </summary>
|
|
/// <value>
|
|
/// One of the enumeration values that specifies the axis along which items are laid out.
|
|
/// The default is Vertical.
|
|
/// </value>
|
|
public Orientation Orientation
|
|
{
|
|
get => GetValue(OrientationProperty);
|
|
set => SetValue(OrientationProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the measurements for horizontal snap points change.
|
|
/// </summary>
|
|
public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
|
|
{
|
|
add => AddHandler(HorizontalSnapPointsChangedEvent, value);
|
|
remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when the measurements for vertical snap points change.
|
|
/// </summary>
|
|
public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
|
|
{
|
|
add => AddHandler(VerticalSnapPointsChangedEvent, value);
|
|
remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the horizontal snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
|
|
/// </summary>
|
|
public bool AreHorizontalSnapPointsRegular
|
|
{
|
|
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
|
|
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the vertical snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
|
|
/// </summary>
|
|
public bool AreVerticalSnapPointsRegular
|
|
{
|
|
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
|
|
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the index of the first realized element, or -1 if no elements are realized.
|
|
/// </summary>
|
|
public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1;
|
|
|
|
/// <summary>
|
|
/// Gets the index of the last realized element, or -1 if no elements are realized.
|
|
/// </summary>
|
|
public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1;
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
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
|
|
{
|
|
var orientation = Orientation;
|
|
|
|
_realizedElements ??= new();
|
|
_measureElements ??= new();
|
|
|
|
// We handle horizontal and vertical layouts here so X and Y are abstracted to:
|
|
// - Horizontal layouts: U = horizontal, V = vertical
|
|
// - Vertical layouts: U = vertical, V = horizontal
|
|
var viewport = CalculateMeasureViewport(items);
|
|
|
|
// If the viewport is disjunct then we can recycle everything.
|
|
if (viewport.viewportIsDisjunct)
|
|
_realizedElements.RecycleAllElements(_recycleElement);
|
|
|
|
// Do the measure, creating/recycling elements as necessary to fill the viewport. Don't
|
|
// write to _realizedElements yet, only _measureElements.
|
|
RealizeElements(items, availableSize, ref viewport);
|
|
|
|
// Now swap the measureElements and realizedElements collection.
|
|
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
|
|
_measureElements.ResetForReuse();
|
|
|
|
return CalculateDesiredSize(orientation, items.Count, viewport);
|
|
}
|
|
finally
|
|
{
|
|
_isInLayout = false;
|
|
}
|
|
}
|
|
|
|
protected override Size ArrangeOverride(Size finalSize)
|
|
{
|
|
if (_realizedElements is null)
|
|
return default;
|
|
|
|
_isInLayout = true;
|
|
|
|
try
|
|
{
|
|
var orientation = Orientation;
|
|
var u = _realizedElements!.StartU;
|
|
|
|
for (var i = 0; i < _realizedElements.Count; ++i)
|
|
{
|
|
var e = _realizedElements.Elements[i];
|
|
|
|
if (e is not null)
|
|
{
|
|
var sizeU = _realizedElements.SizeU[i];
|
|
var rect = orientation == Orientation.Horizontal ?
|
|
new Rect(u, 0, sizeU, finalSize.Height) :
|
|
new Rect(0, u, finalSize.Width, sizeU);
|
|
e.Arrange(rect);
|
|
_scrollViewer?.RegisterAnchorCandidate(e);
|
|
u += orientation == Orientation.Horizontal ? rect.Width : rect.Height;
|
|
}
|
|
}
|
|
|
|
return finalSize;
|
|
}
|
|
finally
|
|
{
|
|
_isInLayout = false;
|
|
|
|
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
|
|
}
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
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 horiz = Orientation == Orientation.Horizontal;
|
|
var fromIndex = from != null ? IndexFromContainer(fromControl) : -1;
|
|
var toIndex = fromIndex;
|
|
|
|
switch (direction)
|
|
{
|
|
case NavigationDirection.First:
|
|
toIndex = 0;
|
|
break;
|
|
case NavigationDirection.Last:
|
|
toIndex = count - 1;
|
|
break;
|
|
case NavigationDirection.Next:
|
|
++toIndex;
|
|
break;
|
|
case NavigationDirection.Previous:
|
|
--toIndex;
|
|
break;
|
|
case NavigationDirection.Left:
|
|
if (horiz)
|
|
--toIndex;
|
|
break;
|
|
case NavigationDirection.Right:
|
|
if (horiz)
|
|
++toIndex;
|
|
break;
|
|
case NavigationDirection.Up:
|
|
if (!horiz)
|
|
--toIndex;
|
|
break;
|
|
case NavigationDirection.Down:
|
|
if (!horiz)
|
|
++toIndex;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if (fromIndex == toIndex)
|
|
return from;
|
|
|
|
if (wrap)
|
|
{
|
|
if (toIndex < 0)
|
|
toIndex = count - 1;
|
|
else if (toIndex >= count)
|
|
toIndex = 0;
|
|
}
|
|
|
|
return ScrollIntoView(toIndex);
|
|
}
|
|
|
|
protected internal override IEnumerable<Control>? GetRealizedContainers()
|
|
{
|
|
return _realizedElements?.Elements.Where(x => x is not 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 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 anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
|
|
var rect = Orientation == Orientation.Horizontal ?
|
|
new Rect(anchorU, 0, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height) :
|
|
new Rect(0, anchorU, _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;
|
|
}
|
|
|
|
internal IReadOnlyList<Control?> GetRealizedElements()
|
|
{
|
|
return _realizedElements?.Elements ?? Array.Empty<Control>();
|
|
}
|
|
|
|
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 = Orientation == Orientation.Horizontal ? viewport.X : viewport.Y;
|
|
var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom;
|
|
|
|
// Get or estimate the anchor element from which to start realization.
|
|
var itemCount = items?.Count ?? 0;
|
|
var (anchorIndex, anchorU) = _realizedElements.GetOrEstimateAnchorElementForViewport(
|
|
viewportStart,
|
|
viewportEnd,
|
|
itemCount,
|
|
ref _lastEstimatedElementSizeU);
|
|
|
|
// 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,
|
|
anchorU = anchorU,
|
|
viewportUStart = viewportStart,
|
|
viewportUEnd = viewportEnd,
|
|
viewportIsDisjunct = disjunct,
|
|
};
|
|
}
|
|
|
|
private Size CalculateDesiredSize(Orientation orientation, int itemCount, in MeasureViewport viewport)
|
|
{
|
|
var sizeU = 0.0;
|
|
var sizeV = viewport.measuredV;
|
|
|
|
if (viewport.lastIndex >= 0)
|
|
{
|
|
var remaining = itemCount - viewport.lastIndex - 1;
|
|
sizeU = viewport.realizedEndU + (remaining * _lastEstimatedElementSizeU);
|
|
}
|
|
|
|
return orientation == Orientation.Horizontal ? new(sizeU, sizeV) : new(sizeV, sizeU);
|
|
}
|
|
|
|
private double EstimateElementSizeU()
|
|
{
|
|
if (_realizedElements is null)
|
|
return _lastEstimatedElementSizeU;
|
|
|
|
var result = _realizedElements.EstimateElementSizeU();
|
|
if (result >= 0)
|
|
_lastEstimatedElementSizeU = result;
|
|
return _lastEstimatedElementSizeU;
|
|
}
|
|
|
|
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.Intersect(new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity));
|
|
}
|
|
|
|
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 horizontal = Orientation == Orientation.Horizontal;
|
|
var u = viewport.anchorU;
|
|
|
|
// If the anchor element is at the beginning of, or before, the start of the viewport
|
|
// then we can recycle all elements before it.
|
|
if (u <= viewport.anchorU)
|
|
_realizedElements.RecycleElementsBefore(viewport.anchorIndex, _recycleElement);
|
|
|
|
// Start at the anchor element and move forwards, realizing elements.
|
|
do
|
|
{
|
|
var e = GetOrCreateElement(items, index);
|
|
e.Measure(availableSize);
|
|
|
|
var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
|
|
var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width;
|
|
|
|
_measureElements!.Add(index, e, u, sizeU);
|
|
viewport.measuredV = Math.Max(viewport.measuredV, sizeV);
|
|
|
|
u += sizeU;
|
|
++index;
|
|
} while (u < viewport.viewportUEnd && index < items.Count);
|
|
|
|
// Store the last index and end U position for the desired size calculation.
|
|
viewport.lastIndex = index - 1;
|
|
viewport.realizedEndU = u;
|
|
|
|
// We can now recycle elements after the last element.
|
|
_realizedElements.RecycleElementsAfter(viewport.lastIndex, _recycleElement);
|
|
|
|
// Next move backwards from the anchor element, realizing elements.
|
|
index = viewport.anchorIndex - 1;
|
|
u = viewport.anchorU;
|
|
|
|
while (u > viewport.viewportUStart && index >= 0)
|
|
{
|
|
var e = GetOrCreateElement(items, index);
|
|
e.Measure(availableSize);
|
|
|
|
var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
|
|
var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width;
|
|
u -= sizeU;
|
|
|
|
_measureElements!.Add(index, e, u, sizeU);
|
|
viewport.measuredV = Math.Max(viewport.measuredV, sizeV);
|
|
--index;
|
|
}
|
|
|
|
// We can now recycle elements before the first element.
|
|
_realizedElements.RecycleElementsBefore(index + 1, _recycleElement);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
|
|
{
|
|
Debug.Assert(ItemContainerGenerator is not null);
|
|
|
|
ItemContainerGenerator.ItemContainerIndexChanged(element, oldIndex, newIndex);
|
|
}
|
|
|
|
private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
|
|
{
|
|
var vertical = Orientation == Orientation.Vertical;
|
|
var oldViewportStart = vertical ? _viewport.Top : _viewport.Left;
|
|
var oldViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
|
|
|
|
_viewport = e.EffectiveViewport.Intersect(new(Bounds.Size));
|
|
_isWaitingForViewportUpdate = false;
|
|
|
|
var newViewportStart = vertical ? _viewport.Top : _viewport.Left;
|
|
var newViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
|
|
|
|
if (!MathUtilities.AreClose(oldViewportStart, newViewportStart) ||
|
|
!MathUtilities.AreClose(oldViewportEnd, newViewportEnd))
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
}
|
|
|
|
private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)
|
|
return;
|
|
|
|
_unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
|
|
RecycleElement(_unrealizedFocusedElement, _unrealizedFocusedIndex);
|
|
_unrealizedFocusedElement = null;
|
|
_unrealizedFocusedIndex = -1;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
|
|
{
|
|
var snapPoints = new List<double>();
|
|
|
|
switch (orientation)
|
|
{
|
|
case Orientation.Horizontal:
|
|
if (AreHorizontalSnapPointsRegular)
|
|
throw new InvalidOperationException();
|
|
if (Orientation == Orientation.Horizontal)
|
|
{
|
|
var averageElementSize = EstimateElementSizeU();
|
|
double snapPoint = 0;
|
|
for (var i = 0; i < Items.Count; i++)
|
|
{
|
|
var container = ContainerFromIndex(i);
|
|
if (container != null)
|
|
{
|
|
switch (snapPointsAlignment)
|
|
{
|
|
case SnapPointsAlignment.Near:
|
|
snapPoint = container.Bounds.Left;
|
|
break;
|
|
case SnapPointsAlignment.Center:
|
|
snapPoint = container.Bounds.Center.X;
|
|
break;
|
|
case SnapPointsAlignment.Far:
|
|
snapPoint = container.Bounds.Right;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (snapPoint == 0)
|
|
{
|
|
switch (snapPointsAlignment)
|
|
{
|
|
case SnapPointsAlignment.Center:
|
|
snapPoint = averageElementSize / 2;
|
|
break;
|
|
case SnapPointsAlignment.Far:
|
|
snapPoint = averageElementSize;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
snapPoint += averageElementSize;
|
|
}
|
|
|
|
snapPoints.Add(snapPoint);
|
|
}
|
|
}
|
|
break;
|
|
case Orientation.Vertical:
|
|
if (AreVerticalSnapPointsRegular)
|
|
throw new InvalidOperationException();
|
|
if (Orientation == Orientation.Vertical)
|
|
{
|
|
var averageElementSize = EstimateElementSizeU();
|
|
double snapPoint = 0;
|
|
for (var i = 0; i < Items.Count; i++)
|
|
{
|
|
var container = ContainerFromIndex(i);
|
|
if (container != null)
|
|
{
|
|
switch (snapPointsAlignment)
|
|
{
|
|
case SnapPointsAlignment.Near:
|
|
snapPoint = container.Bounds.Top;
|
|
break;
|
|
case SnapPointsAlignment.Center:
|
|
snapPoint = container.Bounds.Center.Y;
|
|
break;
|
|
case SnapPointsAlignment.Far:
|
|
snapPoint = container.Bounds.Bottom;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (snapPoint == 0)
|
|
{
|
|
switch (snapPointsAlignment)
|
|
{
|
|
case SnapPointsAlignment.Center:
|
|
snapPoint = averageElementSize / 2;
|
|
break;
|
|
case SnapPointsAlignment.Far:
|
|
snapPoint = averageElementSize;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
snapPoint += averageElementSize;
|
|
}
|
|
|
|
snapPoints.Add(snapPoint);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return snapPoints;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
|
|
{
|
|
offset = 0f;
|
|
var firstRealizedChild = _realizedElements?.Elements.FirstOrDefault();
|
|
|
|
if (firstRealizedChild == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
double snapPoint = 0;
|
|
|
|
switch (Orientation)
|
|
{
|
|
case Orientation.Horizontal:
|
|
if (!AreHorizontalSnapPointsRegular)
|
|
throw new InvalidOperationException();
|
|
|
|
snapPoint = firstRealizedChild.Bounds.Width;
|
|
switch (snapPointsAlignment)
|
|
{
|
|
case SnapPointsAlignment.Near:
|
|
offset = 0;
|
|
break;
|
|
case SnapPointsAlignment.Center:
|
|
offset = (firstRealizedChild.Bounds.Right - firstRealizedChild.Bounds.Left) / 2;
|
|
break;
|
|
case SnapPointsAlignment.Far:
|
|
offset = firstRealizedChild.Bounds.Width;
|
|
break;
|
|
}
|
|
break;
|
|
case Orientation.Vertical:
|
|
if (!AreVerticalSnapPointsRegular)
|
|
throw new InvalidOperationException();
|
|
snapPoint = firstRealizedChild.Bounds.Height;
|
|
switch (snapPointsAlignment)
|
|
{
|
|
case SnapPointsAlignment.Near:
|
|
offset = 0;
|
|
break;
|
|
case SnapPointsAlignment.Center:
|
|
offset = (firstRealizedChild.Bounds.Bottom - firstRealizedChild.Bounds.Top) / 2;
|
|
break;
|
|
case SnapPointsAlignment.Far:
|
|
offset = firstRealizedChild.Bounds.Height;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return snapPoint;
|
|
}
|
|
|
|
private struct MeasureViewport
|
|
{
|
|
public int anchorIndex;
|
|
public double anchorU;
|
|
public double viewportUStart;
|
|
public double viewportUEnd;
|
|
public double measuredV;
|
|
public double realizedEndU;
|
|
public int lastIndex;
|
|
public bool viewportIsDisjunct;
|
|
}
|
|
}
|
|
}
|
|
|