From fb04570279895dbb3b2eb387f436b80a506fd94b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 30 May 2023 08:47:16 +0000 Subject: [PATCH] ad virtualizing uniform grid --- .../Utils/RealizedGridElements.cs | 38 +++--- .../VirtualizingUniformGrid.cs | 126 +++++++++++++----- 2 files changed, 114 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/Utils/RealizedGridElements.cs b/src/Avalonia.Controls/Utils/RealizedGridElements.cs index 17ee76cbd7..62af09b8a4 100644 --- a/src/Avalonia.Controls/Utils/RealizedGridElements.cs +++ b/src/Avalonia.Controls/Utils/RealizedGridElements.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Utils } /// - /// Gets or estimates the index and start U position of the anchor element for the + /// Gets or estimates the index and grid position of the anchor element for the /// specified viewport. /// /// The position of the start of the viewport. @@ -105,22 +105,23 @@ namespace Avalonia.Controls.Utils /// /// 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 + /// - The index of the last visible element + /// - The position of the anchor on the grid + /// - The position of the last visible element on the grid /// /// /// 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. /// - public (int index, Vector coord, Vector lastCoord) GetOrEstimateAnchorElementForViewport( + public (int index, int last, 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 (itemCount <= 0 || ColumnCount == 0 || RowCount == 0) + return (-1, -1, Vector.Zero, Vector.Zero); if (_sizes is not null) { @@ -133,20 +134,23 @@ namespace Avalonia.Controls.Utils 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 x = Math.Min((int)(Math.Min(viewportStart.X, MaxWidth) / estimatedElementSize.Width), ColumnCount - 1); + var y = Math.Min((int)(Math.Min(viewportStart.Y, MaxHeight) / estimatedElementSize.Height), RowCount - 1); - 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); + var lastY = Math.Min((int)(Math.Min(viewportEnd.Y, MaxHeight) / estimatedElementSize.Height), RowCount - 1); + var lastX = Math.Min((int)(Math.Min(viewportEnd.X, MaxWidth) / estimatedElementSize.Width), ColumnCount - 1); - return (Math.Max(y * ColumnCount + x - FirstColumn, 0), new Vector(x, y), new Vector(lastX, lastY)); + return (Math.Max(y * ColumnCount + x - FirstColumn, 0), + MathUtilities.Clamp(lastY * ColumnCount + lastX - FirstColumn, 0, itemCount), + new Vector(x, y), + new Vector(lastY > 0 ? ColumnCount : lastX, lastY)); } /// - /// Gets the position of the element with the requested index on the primary axis, if realized. + /// Gets the grid position of the element with the requested index. /// /// - /// The position of the element, or NaN if the element is not realized. + /// The position of the element kn the grid. /// public Vector GetElementCoord(int index) { @@ -156,11 +160,6 @@ namespace Avalonia.Controls.Utils } - public Vector GetOrEstimateElementU(int index) - { - return GetElementCoord(index); - } - /// /// Gets the index of the specified element. /// @@ -275,8 +274,7 @@ namespace Avalonia.Controls.Utils _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. + // first index will be the index where the remove started. if (startIndex <= 0 && end < last) { _firstIndex = first = index; diff --git a/src/Avalonia.Controls/VirtualizingUniformGrid.cs b/src/Avalonia.Controls/VirtualizingUniformGrid.cs index 828afbc262..742fa53b9b 100644 --- a/src/Avalonia.Controls/VirtualizingUniformGrid.cs +++ b/src/Avalonia.Controls/VirtualizingUniformGrid.cs @@ -3,7 +3,6 @@ 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; @@ -127,7 +126,6 @@ namespace Avalonia.Controls if (_viewport.Size == default) return DesiredSize; - if (viewport.viewportIsDisjunct) _realizedElements.RecycleAllElements(_recycleElement); @@ -188,25 +186,36 @@ namespace Avalonia.Controls var width = finalSize.Width / _columns; var height = finalSize.Height / _rows; - for (var i = _measuredViewport.anchorIndex; i < Children.Count; i++) + foreach (var child in _realizedElements!.Elements) { - if (i > _measuredViewport.lastIndex) - break; - - var child = Children[i]; - if (!child.IsVisible) + while (true) { - continue; - } + if (child is not null) + { + var coord = new Vector(x, y); - child.Arrange(new Rect(x * width, y * height, width, height)); + x++; - x++; + if (x >= _columns) + { + x = 0; + y++; + } - if (x >= _columns) - { - x = 0; - y++; + if (IsCoordVisible(coord, _measuredViewport.anchorCoord, _measuredViewport.endCoord)) + { + child.Arrange(new Rect(coord.X * width, coord.Y * height, width, height)); + + _scrollViewer?.RegisterAnchorCandidate(child); + + break; + } + + if (coord == new Vector(_columns - 1, _rows - 1)) + break; + } + else + break; } } @@ -268,7 +277,13 @@ namespace Avalonia.Controls private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e) { - throw new NotImplementedException(); + if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement) + return; + + _unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus; + RecycleElement(_unrealizedFocusedElement, _unrealizedFocusedIndex); + _unrealizedFocusedElement = null; + _unrealizedFocusedIndex = -1; } private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) @@ -301,7 +316,8 @@ namespace Avalonia.Controls Debug.Assert(items.Count > 0); var index = viewport.anchorIndex; - var lastIndex = index; + _realizedElements.RecycleElementsBefore(viewport.anchorIndex, _recycleElement); + _realizedElements.RecycleElementsAfter(viewport.lastIndex, _recycleElement); // Start at the anchor element and move forwards, realizing elements. do @@ -322,26 +338,33 @@ namespace Avalonia.Controls _lastEstimatedElementSize = size; - lastIndex = index; - ++index; - } while (index < items.Count); - - // Store the last index for the desired size calculation. - viewport.lastIndex = lastIndex; + // Calculate the last index and coordinates again, as the first child size is known. + if (index == 0) + { + (_, int last, _, var lastCoord) = _measureElements.GetOrEstimateAnchorElementForViewport(_viewport.TopLeft, _viewport.BottomRight, items.Count, ref _lastEstimatedElementSize); + viewport.lastIndex = last; + viewport.endCoord = lastCoord; + } - // We can now recycle elements before the first element. - _realizedElements.RecycleElementsBefore(viewport.anchorIndex, _recycleElement); + ++index; + } while (index < items.Count && index <= viewport.lastIndex); } private bool IsIndexVisible(int index, Vector start, Vector end) { - var div = Math.DivRem(index, _columns, out var rem); + var div = Math.DivRem(index + FirstColumn, _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 bool IsCoordVisible(Vector coord, Vector start, Vector end) + { + return coord.X >= start.X && coord.Y >= start.Y && + coord.X <= end.X && coord.Y <= end.Y; + } + private MeasureViewport CalculateMeasureViewport(IReadOnlyList items) { Debug.Assert(_realizedElements is not null); @@ -356,7 +379,7 @@ namespace Avalonia.Controls // Get or estimate the anchor element from which to start realization. var itemCount = items?.Count ?? 0; - var (anchorIndex, anchor, end) = _realizedElements.GetOrEstimateAnchorElementForViewport( + var (anchorIndex, lastIndex, anchor, end) = _realizedElements.GetOrEstimateAnchorElementForViewport( viewportStart, viewportEnd, itemCount, @@ -374,6 +397,7 @@ namespace Avalonia.Controls viewportEnd = viewportEnd, viewportIsDisjunct = disjunct, endCoord = end, + lastIndex = lastIndex }; } @@ -598,7 +622,7 @@ namespace Avalonia.Controls _scrollToIndex = index; // Get the expected position of the elment and put it in place. - var anchor = _realizedElements.GetOrEstimateElementU(index); + var anchor = _realizedElements.GetElementCoord(index); var rect = new Rect(anchor.X, anchor.Y, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height); _scrollToElement.Arrange(rect); @@ -666,9 +690,51 @@ namespace Avalonia.Controls if (count == 0 || from is not Control fromControl) return null; + var fromIndex = from != null ? IndexFromContainer(fromControl) : -1; var toIndex = fromIndex; - // implement + + 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: + --toIndex; + break; + case NavigationDirection.Right: + ++toIndex; + break; + case NavigationDirection.Up: + toIndex -= _columns; + break; + case NavigationDirection.Down: + toIndex += _columns; + 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); }