Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make VirtualizingStackPanel better handle container size changes #16168

Merged
merged 7 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 34 additions & 129 deletions src/Avalonia.Controls/Utils/RealizedStackElements.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Layout;
using Avalonia.Utilities;

namespace Avalonia.Controls.Utils
Expand Down Expand Up @@ -42,16 +43,17 @@ internal class RealizedStackElements
public IReadOnlyList<double> SizeU => _sizes ??= new List<double>();

/// <summary>
/// Gets the position of the first element on the primary axis.
/// Gets the position of the first element on the primary axis, or NaN if the position is
/// unstable.
/// </summary>
public double StartU => _startU;
public double StartU => _startUUnstable ? double.NaN : _startU;

/// <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="u">The position of the elemnt on the primary axis.</param>
/// <param name="u">The position of the element on the primary axis.</param>
/// <param name="sizeU">The size of the element on the primary axis.</param>
public void Add(int index, Control element, double u, double sizeU)
{
Expand Down Expand Up @@ -99,76 +101,6 @@ public void Add(int index, Control element, double u, double sizeU)
return null;
}

/// <summary>
/// Gets or estimates the index and start U position of the anchor element for the
/// specified viewport.
/// </summary>
/// <param name="viewportStartU">The U position of the start of the viewport.</param>
/// <param name="viewportEndU">The U position of the end of the viewport.</param>
/// <param name="itemCount">The number of items in the list.</param>
/// <param name="estimatedElementSizeU">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, double position) GetOrEstimateAnchorElementForViewport(
double viewportStartU,
double viewportEndU,
int itemCount,
ref double estimatedElementSizeU)
{
// We have no elements, nothing to do here.
if (itemCount <= 0)
return (-1, 0);

// If we're at 0 then display the first item.
if (MathUtilities.IsZero(viewportStartU))
return (0, 0);

if (_sizes is not null && !_startUUnstable)
{
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;
}
}

// 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);
}

/// <summary>
/// Gets the position of the element with the requested index on the primary axis, if realized.
/// </summary>
Expand All @@ -193,61 +125,6 @@ public double GetElementU(int index)
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
{
-1 => estimatedElementSizeU,
double v => v,
};

// Store the estimated size for the next layout pass.
estimatedElementSizeU = estimatedSize;

// TODO: Use _startU to work this out.
return index * estimatedSize;
}

/// <summary>
/// Estimates the average U size of all elements in the source collection based on the
/// realized elements.
/// </summary>
/// <returns>
/// The estimated U size of an element, or -1 if not enough information is present to make
/// an estimate.
/// </returns>
public double EstimateElementSizeU()
{
var total = 0.0;
var divisor = 0.0;

// Average the size of the realized elements.
if (_sizes is not null)
{
foreach (var size in _sizes)
{
if (double.IsNaN(size))
continue;
total += size;
++divisor;
}
}

// We don't have any elements on which to base our estimate.
if (divisor == 0 || total == 0)
return -1;

return total / divisor;
}

/// <summary>
/// Gets the index of the specified element.
/// </summary>
Expand Down Expand Up @@ -538,6 +415,34 @@ public void ResetForReuse()
_elements?.Clear();
_sizes?.Clear();
}
}

/// <summary>
/// Validates that <see cref="StartU"/> is still valid.
/// </summary>
/// <param name="orientation">The panel orientation.</param>
/// <remarks>
/// If the U size of any element in the realized elements has changed, then the value of
/// <see cref="StartU"/> should be considered unstable.
/// </remarks>
public void ValidateStartU(Orientation orientation)
{
if (_elements is null || _sizes is null || _startUUnstable)
return;

for (var i = 0; i < _elements.Count; ++i)
{
if (_elements[i] is not { } element)
continue;

var sizeU = orientation == Orientation.Horizontal ?
element.DesiredSize.Width : element.DesiredSize.Height;

if (sizeU != _sizes[i])
{
_startUUnstable = true;
break;
}
}
}
}
}
6 changes: 6 additions & 0 deletions src/Avalonia.Controls/VirtualizingPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ protected void RemoveInternalChildRange(int index, int count)
Children.RemoveRange(index, count);
}

private protected override void InvalidateMeasureOnChildrenChanged()
{
// Don't invalidate measure when children are added or removed: the panel is responsible
// for managing its children.
}

internal void Attach(ItemsControl itemsControl)
{
if (ItemsControl is not null)
Expand Down
117 changes: 110 additions & 7 deletions src/Avalonia.Controls/VirtualizingStackPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
Expand Down Expand Up @@ -159,6 +160,7 @@ protected override Size MeasureOverride(Size availableSize)

try
{
_realizedElements?.ValidateStartU(Orientation);
grokys marked this conversation as resolved.
Show resolved Hide resolved
_realizedElements ??= new();
_measureElements ??= new();

Expand All @@ -179,6 +181,10 @@ protected override Size MeasureOverride(Size availableSize)
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();

// If there is a focused element is outside the visible viewport (i.e.
// _focusedElement is non-null), ensure it's measured.
_focusedElement?.Measure(availableSize);

return CalculateDesiredSize(orientation, items.Count, viewport);
}
finally
Expand Down Expand Up @@ -215,6 +221,16 @@ protected override Size ArrangeOverride(Size finalSize)
}
}

// Ensure that the focused element is in the correct position.
if (_focusedElement is not null && _focusedIndex >= 0)
{
u = GetOrEstimateElementU(_focusedIndex);
var rect = orientation == Orientation.Horizontal ?
new Rect(u, 0, _focusedElement.DesiredSize.Width, finalSize.Height) :
new Rect(0, u, finalSize.Width, _focusedElement.DesiredSize.Height);
_focusedElement.Arrange(rect);
}

return finalSize;
}
finally
Expand Down Expand Up @@ -389,7 +405,7 @@ protected internal override int IndexFromContainer(Control container)
scrollToElement.Measure(Size.Infinity);

// Get the expected position of the element and put it in place.
var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
var anchorU = GetOrEstimateElementU(index);
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);
Expand Down Expand Up @@ -472,11 +488,12 @@ private MeasureViewport CalculateMeasureViewport(IReadOnlyList<object?> items)
}
else
{
(anchorIndex, anchorU) = _realizedElements.GetOrEstimateAnchorElementForViewport(
GetOrEstimateAnchorElementForViewport(
viewportStart,
viewportEnd,
items.Count,
ref _lastEstimatedElementSizeU);
out anchorIndex,
out anchorU);
}

// Check if the anchor element is not within the currently realized elements.
Expand Down Expand Up @@ -531,12 +548,98 @@ private double EstimateElementSizeU()
if (_realizedElements is null)
return _lastEstimatedElementSizeU;

var result = _realizedElements.EstimateElementSizeU();
if (result >= 0)
_lastEstimatedElementSizeU = result;
return _lastEstimatedElementSizeU;
var orientation = Orientation;
var total = 0.0;
var divisor = 0.0;

// Average the desired size of the realized, measured elements.
foreach (var element in _realizedElements.Elements)
{
if (element is null || !element.IsMeasureValid)
continue;
var sizeU = orientation == Orientation.Horizontal ?
element.DesiredSize.Width :
element.DesiredSize.Height;
total += sizeU;
++divisor;
}

// Check we have enough information on which to base our estimate.
if (divisor == 0 || total == 0)
return _lastEstimatedElementSizeU;

// Store and return the estimate.
return _lastEstimatedElementSizeU = total / divisor;
}

private void GetOrEstimateAnchorElementForViewport(
double viewportStartU,
double viewportEndU,
int itemCount,
out int index,
out double position)
{
// We have no elements, or we're at the start of the viewport.
if (itemCount <= 0 || MathUtilities.IsZero(viewportStartU))
{
index = 0;
position = 0;
return;
}

// If we have realised elements and a valid StartU then try to use this information to
// get the anchor element.
if (_realizedElements?.StartU is { } u && !double.IsNaN(u))
{
var orientation = Orientation;

for (var i = 0; i < _realizedElements.Elements.Count; ++i)
{
if (_realizedElements.Elements[i] is not { } element)
continue;

var sizeU = orientation == Orientation.Horizontal ?
element.DesiredSize.Width :
element.DesiredSize.Height;
var endU = u + sizeU;

if (endU > viewportStartU && u < viewportEndU)
{
index = _realizedElements.FirstIndex + i;
position = u;
return;
}

u = endU;
}
}

// 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 element size.
var estimatedSize = EstimateElementSizeU();

// Estimate the element at the start of the viewport.
var startIndex = Math.Min((int)(viewportStartU / estimatedSize), itemCount - 1);
index = startIndex;
position = startIndex * estimatedSize;
}

private double GetOrEstimateElementU(int index)
{
// Return the position of the existing element if realized.
var u = _realizedElements?.GetElementU(index) ?? double.NaN;

if (!double.IsNaN(u))
return u;

// Estimate the element size.
var estimatedSize = EstimateElementSizeU();

// TODO: Use _startU to work this out.
return index * estimatedSize;
}


private void RealizeElements(
IReadOnlyList<object?> items,
Size availableSize,
Expand Down
Loading
Loading