diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
index ba1f07a632..8dbfb2c957 100644
--- a/src/Avalonia.Controls/Utils/RealizedStackElements.cs
+++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Avalonia.Utilities;
namespace Avalonia.Controls.Utils
{
@@ -99,63 +100,120 @@ namespace Avalonia.Controls.Utils
}
///
- /// Gets the index and start U position of the element at the specified U position.
+ /// Gets or estimates the index and start U position of the anchor element for the
+ /// specified viewport.
///
- /// The U position.
+ /// The U position of the start of the viewport.
+ /// The U position of the end of the viewport.
+ /// The number of items in the list.
+ /// The current estimated element size.
///
/// A tuple containing:
- /// - The index of the item at the specified U position, or -1 if the item could not be
- /// determined
- /// - The U position of the start of the item, if determined
+ /// - 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
///
- public (int index, double position) GetIndexAt(double u)
+ ///
+ /// 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, double position) GetOrEstimateAnchorElementForViewport(
+ double viewportStartU,
+ double viewportEndU,
+ int itemCount,
+ ref double estimatedElementSizeU)
{
- if (_elements is null || _sizes is null || _startU > u || _startUUnstable)
+ // We have no elements, nothing to do here.
+ if (itemCount <= 0)
return (-1, 0);
- var index = 0;
- var position = _startU;
+ // If we're at 0 then display the first item.
+ if (MathUtilities.IsZero(viewportStartU))
+ return (0, 0);
- while (index < _elements.Count)
+ if (_sizes is not null && !_startUUnstable)
{
- var size = _sizes[index];
- if (double.IsNaN(size))
- break;
- if (u >= position && u < position + size)
- return (index + FirstIndex, position);
- position += size;
- ++index;
+ var u = _startU;
+
+ for (var i = 0; i < _sizes.Count; ++i)
+ {
+ var size = _sizes[i];
+
+ if (double.IsNaN(size))
+ break;
+
+ var endU = u + size;
+
+ if (endU > viewportStartU && u < viewportEndU)
+ return (FirstIndex + i, u);
+
+ u = endU;
+ }
}
- return (-1, 0);
+ // We don't have any realized elements in the requested viewport, or can't rely on
+ // StartU being valid. Estimate the index using only the estimated size. First,
+ // estimate the element size, using defaultElementSizeU if we don't have any realized
+ // elements.
+ var estimatedSize = EstimateElementSizeU() switch
+ {
+ -1 => estimatedElementSizeU,
+ double v => v,
+ };
+
+ // Store the estimated size for the next layout pass.
+ estimatedElementSizeU = estimatedSize;
+
+ // Estimate the element at the start of the viewport.
+ var index = Math.Min((int)(viewportStartU / estimatedSize), itemCount - 1);
+ return (index, index * estimatedSize);
}
///
- /// Gets the element at the specified position on the primary axis, if realized.
+ /// Gets the position of the element with the requested index on the primary axis, if realized.
///
- /// The position.
///
- /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the
- /// primary axis.
+ /// The position of the element, or NaN if the element is not realized.
///
- public (int index, double position) GetElementAt(double position)
+ public double GetElementU(int index)
{
- if (_sizes is null || position < StartU)
- return (-1, 0);
+ if (index < FirstIndex || _sizes is null)
+ return double.NaN;
+
+ var endIndex = index - FirstIndex;
+
+ if (endIndex >= _sizes.Count)
+ return double.NaN;
var u = StartU;
- var i = FirstIndex;
- foreach (var size in _sizes)
+ for (var i = 0; i < endIndex; ++i)
+ u += _sizes[i];
+
+ return u;
+ }
+
+ public double GetOrEstimateElementU(int index, ref double estimatedElementSizeU)
+ {
+ // Return the position of the existing element if realized.
+ var u = GetElementU(index);
+
+ if (!double.IsNaN(u))
+ return u;
+
+ // Estimate the element size, using defaultElementSizeU if we don't have any realized
+ // elements.
+ var estimatedSize = EstimateElementSizeU() switch
{
- var endU = u + size;
- if (position < endU)
- return (i, u);
- u += size;
- ++i;
- }
+ -1 => estimatedElementSizeU,
+ double v => v,
+ };
+
+ // Store the estimated size for the next layout pass.
+ estimatedElementSizeU = estimatedSize;
- return (-1, 0);
+ // TODO: Use _startU to work this out.
+ return index * estimatedSize;
}
///
@@ -171,13 +229,6 @@ namespace Avalonia.Controls.Utils
var total = 0.0;
var divisor = 0.0;
- // Start by averaging the size of the elements before the first realized element.
- if (FirstIndex >= 0 && !_startUUnstable)
- {
- total += _startU;
- divisor += FirstIndex;
- }
-
// Average the size of the realized elements.
if (_sizes is not null)
{
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index 7cca0986ad..9378bc391d 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -60,8 +60,8 @@ namespace Avalonia.Controls
private readonly Action _recycleElement;
private readonly Action _recycleElementOnItemRemoved;
private readonly Action _updateElementIndex;
- private int _anchorIndex = -1;
- private Control? _anchorElement;
+ private int _scrollToIndex = -1;
+ private Control? _scrollToElement;
private bool _isInLayout;
private bool _isWaitingForViewportUpdate;
private double _lastEstimatedElementSizeU = 25;
@@ -142,48 +142,39 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
- if (!IsEffectivelyVisible)
+ 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 items = Items;
var orientation = Orientation;
_realizedElements ??= new();
_measureElements ??= new();
- // If we're bringing an item into view, ignore any layout passes until we receive a new
- // effective viewport.
- if (_isWaitingForViewportUpdate)
- {
- var sizeV = orientation == Orientation.Horizontal ? DesiredSize.Height : DesiredSize.Width;
- return CalculateDesiredSize(orientation, items, sizeV);
- }
-
// 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);
- // Recycle elements outside of the expected range.
- _realizedElements.RecycleElementsBefore(viewport.firstIndex, _recycleElement);
- _realizedElements.RecycleElementsAfter(viewport.lastIndex, _recycleElement);
-
// Do the measure, creating/recycling elements as necessary to fill the viewport. Don't
// write to _realizedElements yet, only _measureElements.
- GenerateElements(availableSize, ref viewport);
-
- // Now we know what definitely fits, recycle anything left over.
- _realizedElements.RecycleElementsAfter(_measureElements.LastIndex, _recycleElement);
+ RealizeElements(items, availableSize, ref viewport);
- // And swap the measureElements and realizedElements collection.
+ // Now swap the measureElements and realizedElements collection.
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();
- return CalculateDesiredSize(orientation, items, viewport.measuredV);
+ return CalculateDesiredSize(orientation, items.Count, viewport);
}
finally
{
@@ -338,7 +329,7 @@ namespace Avalonia.Controls
{
var items = Items;
- if (_isInLayout || index < 0 || index >= items.Count)
+ if (_isInLayout || index < 0 || index >= items.Count || _realizedElements is null)
return null;
if (GetRealizedElement(index) is Control element)
@@ -350,16 +341,16 @@ namespace Avalonia.Controls
{
// 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.
- _anchorElement = GetOrCreateElement(items, index);
- _anchorElement.Measure(Size.Infinity);
- _anchorIndex = index;
+ _scrollToElement = GetOrCreateElement(items, index);
+ _scrollToElement.Measure(Size.Infinity);
+ _scrollToIndex = index;
// Get the expected position of the elment and put it in place.
- var anchorU = GetOrEstimateElementPosition(index);
+ var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
var rect = Orientation == Orientation.Horizontal ?
- new Rect(anchorU, 0, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height) :
- new Rect(0, anchorU, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height);
- _anchorElement.Arrange(rect);
+ 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
@@ -373,7 +364,7 @@ namespace Avalonia.Controls
}
// Try to bring the item into view.
- _anchorElement.BringIntoView();
+ _scrollToElement.BringIntoView();
// If the viewport does not contain the item to scroll to, set _isWaitingForViewportUpdate:
// this should cause the following chain of events:
@@ -392,9 +383,9 @@ namespace Avalonia.Controls
root.LayoutManager.ExecuteLayoutPass();
}
- var result = _anchorElement;
- _anchorElement = null;
- _anchorIndex = -1;
+ var result = _scrollToElement;
+ _scrollToElement = null;
+ _scrollToIndex = -1;
return result;
}
@@ -418,46 +409,34 @@ namespace Avalonia.Controls
var viewportStart = Orientation == Orientation.Horizontal ? viewport.X : viewport.Y;
var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom;
- var (firstIndex, firstIndexU) = _realizedElements.GetIndexAt(viewportStart);
- var (lastIndex, _) = _realizedElements.GetIndexAt(viewportEnd);
- var estimatedElementSize = -1.0;
var itemCount = items?.Count ?? 0;
- var maxIndex = Math.Max(itemCount - 1, 0);
-
- if (firstIndex == -1)
- {
- estimatedElementSize = EstimateElementSizeU();
- firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex);
- firstIndexU = firstIndex * estimatedElementSize;
- }
-
- if (lastIndex == -1)
- {
- if (estimatedElementSize == -1)
- estimatedElementSize = EstimateElementSizeU();
- lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex);
- }
+ var (anchorIndex, anchorU) = _realizedElements.GetOrEstimateAnchorElementForViewport(
+ viewportStart,
+ viewportEnd,
+ itemCount,
+ ref _lastEstimatedElementSizeU);
return new MeasureViewport
{
- firstIndex = firstIndex,
- lastIndex = lastIndex,
+ anchorIndex = anchorIndex,
+ anchorU = anchorU,
viewportUStart = viewportStart,
viewportUEnd = viewportEnd,
- startU = firstIndexU,
};
}
- private Size CalculateDesiredSize(Orientation orientation, IReadOnlyList