in src/Avalonia.Controls/VirtualizingStackPanel.cs [955:1096]
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;
var oldExtendedViewportStart = vertical ? _extendedViewport.Top : _extendedViewport.Left;
var oldExtendedViewportEnd = vertical ? _extendedViewport.Bottom : _extendedViewport.Right;
// Update current viewport
_viewport = e.EffectiveViewport.Intersect(new(Bounds.Size));
_isWaitingForViewportUpdate = false;
// Calculate buffer sizes based on viewport dimensions
var viewportSize = vertical ? _viewport.Height : _viewport.Width;
var bufferSize = viewportSize * _bufferFactor;
// Calculate extended viewport with relative buffers
var extendedViewportStart = vertical ?
Math.Max(0, _viewport.Top - bufferSize) :
Math.Max(0, _viewport.Left - bufferSize);
var extendedViewportEnd = vertical ?
Math.Min(Bounds.Height, _viewport.Bottom + bufferSize) :
Math.Min(Bounds.Width, _viewport.Right + bufferSize);
// special case:
// If we are at the start of the list, append 2 * CacheLength additional items
// If we are at the end of the list, prepend 2 * CacheLength additional items
// - this way we always maintain "2 * CacheLength * element" items.
if (vertical)
{
var spaceAbove = _viewport.Top - bufferSize;
var spaceBelow = Bounds.Height - (_viewport.Bottom + bufferSize);
if (spaceAbove < 0 && spaceBelow >= 0)
extendedViewportEnd = Math.Min(Bounds.Height, extendedViewportEnd + Math.Abs(spaceAbove));
if (spaceAbove >= 0 && spaceBelow < 0)
extendedViewportStart = Math.Max(0, extendedViewportStart - Math.Abs(spaceBelow));
}
else
{
var spaceLeft = _viewport.Left - bufferSize;
var spaceRight = Bounds.Width - (_viewport.Right + bufferSize);
if (spaceLeft < 0 && spaceRight >= 0)
extendedViewportEnd = Math.Min(Bounds.Width, extendedViewportEnd + Math.Abs(spaceLeft));
if(spaceLeft >= 0 && spaceRight < 0)
extendedViewportStart = Math.Max(0, extendedViewportStart - Math.Abs(spaceRight));
}
Rect extendedViewPort;
if (vertical)
{
extendedViewPort = new Rect(
_viewport.X,
extendedViewportStart,
_viewport.Width,
extendedViewportEnd - extendedViewportStart);
}
else
{
extendedViewPort = new Rect(
extendedViewportStart,
_viewport.Y,
extendedViewportEnd - extendedViewportStart,
_viewport.Height);
}
// Determine if we need a new measure
var newViewportStart = vertical ? _viewport.Top : _viewport.Left;
var newViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
var newExtendedViewportStart = vertical ? extendedViewPort.Top : extendedViewPort.Left;
var newExtendedViewportEnd = vertical ? extendedViewPort.Bottom : extendedViewPort.Right;
var needsMeasure = false;
// Case 1: Viewport has changed significantly
if (!MathUtilities.AreClose(oldViewportStart, newViewportStart) ||
!MathUtilities.AreClose(oldViewportEnd, newViewportEnd))
{
// Case 1a: The new viewport exceeds the old extended viewport
if (newViewportStart < oldExtendedViewportStart ||
newViewportEnd > oldExtendedViewportEnd)
{
needsMeasure = true;
}
// Case 1b: The extended viewport has changed significantly
else if (!MathUtilities.AreClose(oldExtendedViewportStart, newExtendedViewportStart) ||
!MathUtilities.AreClose(oldExtendedViewportEnd, newExtendedViewportEnd))
{
// Check if we're about to scroll into an area where we don't have realized elements
// This would be the case if we're near the edge of our current extended viewport
var nearingEdge = false;
if (_realizedElements != null)
{
var firstRealizedElementU = _realizedElements.StartU;
var lastRealizedElementU = _realizedElements.StartU;
for (var i = 0; i < _realizedElements.Count; i++)
{
lastRealizedElementU += _realizedElements.SizeU[i];
}
// If scrolling up/left and nearing the top/left edge of realized elements
if (newViewportStart < oldViewportStart &&
newViewportStart - newExtendedViewportStart < bufferSize)
{
// Edge case: We're at item 0 with excess measurement space.
// Skip re-measuring since we're at the list start and it won't change the result.
// This prevents redundant Measure-Arrange cycles when at list beginning.
nearingEdge = !_hasReachedStart;
}
// If scrolling down/right and nearing the bottom/right edge of realized elements
if (newViewportEnd > oldViewportEnd &&
newExtendedViewportEnd - newViewportEnd < bufferSize)
{
// Edge case: We're at the last item with excess measurement space.
// Skip re-measuring since we're at the list end and it won't change the result.
// This prevents redundant Measure-Arrange cycles when at list beginning.
nearingEdge = !_hasReachedEnd;
}
}
else
{
nearingEdge = true;
}
needsMeasure = nearingEdge;
}
}
if (needsMeasure)
{
// only store the new "old" extended viewport if we _did_ actually measure
_extendedViewport = extendedViewPort;
InvalidateMeasure();
}
}