Skip to content

Commit

Permalink
Use a new method to lay out items.
Browse files Browse the repository at this point in the history
  • Loading branch information
grokys committed Apr 3, 2023
1 parent 240f695 commit 549131a
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 180 deletions.
173 changes: 38 additions & 135 deletions src/Avalonia.Controls/Utils/RealizedStackElements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,68 +100,56 @@ public void Add(int index, Control element, double u, double sizeU)
}

/// <summary>
/// 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.
/// </summary>
/// <param name="u">The U position.</param>
/// <returns>
/// 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
/// </returns>
public (int index, double position) GetIndexAt(double u)
{
if (_elements is null || _sizes is null || _startU > u || _startUUnstable)
return (-1, 0);

var index = 0;
var position = _startU;

while (index < _elements.Count)
{
var size = _sizes[index];
if (double.IsNaN(size))
break;
if (u >= position && u < position + size)
return (index + FirstIndex, position);
position += size;
++index;
}

return (-1, 0);
}

/// <summary>
/// Gets or estimates the index and start U position of the element at the specified U
/// position.
/// </summary>
/// <param name="u">The U position.</param>
/// <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 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
/// </returns>
public (int index, double position) GetOrEstimateIndexAt(
double u,
/// <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 the position is 0 we know the first element's going to be there.
if (MathUtilities.IsZero(u))
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;

// Try to get an already realized element at the specified position.
if (GetIndexAt(u) is { index: >= 0 } found)
return found;
var endU = u + size;

// Estimate the element size, using defaultElementSizeU if we don't have any realized
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
{
Expand All @@ -172,65 +160,9 @@ public void Add(int index, Control element, double u, double sizeU)
// Store the estimated size for the next layout pass.
estimatedElementSizeU = estimatedSize;

if (FirstIndex == -1 || _startUUnstable)
{
// We don't have any realized elements or can't rely on StartU being valid.
// Estimate the index using only the estimated size.
var index = Math.Min((int)(u / estimatedSize), itemCount - 1);
return (index, index * estimatedSize);
}
else if (u < _startU)
{
// The position is before the realized elements, estimate the index using the first
// realized element.
var distance = _startU - u;
var index = MathUtilities.Clamp(
_firstIndex - (int)Math.Ceiling(distance / estimatedSize),
0,
itemCount - 1);

if (index == 0)
return (0, 0);
else
return (index, _startU - ((_firstIndex - index) * estimatedSize));
}
else
{
// The position is after the realized elements, estimate the index using the last
// realized element.
var (lastIndex, endU) = GetLastElementU();
var distance = u - endU;
var index = Math.Min(lastIndex + (int)(distance / estimatedSize), itemCount - 1);
return (index, endU + ((index - lastIndex) * estimatedSize));
}
}

/// <summary>
/// Gets the element at the specified position on the primary axis, if realized.
/// </summary>
/// <param name="position">The position.</param>
/// <returns>
/// A tuple containing the index of the element (or -1 if not found) and the position of the element on the
/// primary axis.
/// </returns>
public (int index, double position) GetElementAt(double position)
{
if (_sizes is null || position < StartU)
return (-1, 0);

var u = StartU;
var i = FirstIndex;

foreach (var size in _sizes)
{
var endU = u + size;
if (position < endU)
return (i, u);
u += size;
++i;
}

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

/// <summary>
Expand Down Expand Up @@ -280,35 +212,6 @@ public double GetOrEstimateElementU(int index, ref double estimatedElementSizeU)
return index * estimatedSize;
}

/// <summary>
/// Gets the position the last realized element on the primary axis.
/// </summary>
/// <returns>
/// A tuple containing the index of the element (or -1 if none available) and the position
/// of the element on the primary axis.
/// </returns>
public (int index, double position) GetLastElementU()
{
if (_sizes is null || _sizes.Count == 0)
return (-1, 0);

var index = FirstIndex;
var u = StartU;
var count = _sizes.Count;

for (var i = 0; i < count; ++i)
{
var size = _sizes[i];

if (double.IsNaN(size))
return (index, u);
++index;
u += size;
}

return (index, u);
}

/// <summary>
/// Estimates the average U size of all elements in the source collection based on the
/// realized elements.
Expand Down
102 changes: 58 additions & 44 deletions src/Avalonia.Controls/VirtualizingStackPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
Expand Down Expand Up @@ -141,41 +142,35 @@ public bool AreVerticalSnapPointsRegular

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)
return DesiredSize;

// 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);
RealizeElements(items, availableSize, ref viewport);

// Now we know what definitely fits, recycle anything left over.
_realizedElements.RecycleElementsAfter(_measureElements.LastIndex, _recycleElement);

// And swap the measureElements and realizedElements collection.
// Now swap the measureElements and realizedElements collection.
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();

Expand Down Expand Up @@ -415,22 +410,18 @@ private MeasureViewport CalculateMeasureViewport(IReadOnlyList<object?> items)
var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom;

var itemCount = items?.Count ?? 0;
var (firstIndex, firstIndexU) = _realizedElements.GetOrEstimateIndexAt(
var (anchorIndex, anchorU) = _realizedElements.GetOrEstimateAnchorElementForViewport(
viewportStart,
itemCount,
ref _lastEstimatedElementSizeU);
var (lastIndex, _) = _realizedElements.GetOrEstimateIndexAt(
viewportEnd,
itemCount,
ref _lastEstimatedElementSizeU);

return new MeasureViewport
{
firstIndex = firstIndex,
lastIndex = lastIndex,
anchorIndex = anchorIndex,
anchorU = anchorU,
viewportUStart = viewportStart,
viewportUEnd = viewportEnd,
startU = firstIndexU,
};
}

Expand All @@ -442,7 +433,7 @@ private Size CalculateDesiredSize(Orientation orientation, int itemCount, in Mea
if (viewport.lastIndex >= 0)
{
var remaining = itemCount - viewport.lastIndex - 1;
sizeU = viewport.endU + (remaining * _lastEstimatedElementSizeU);
sizeU = viewport.realizedEndU + (remaining * _lastEstimatedElementSizeU);
}

return orientation == Orientation.Horizontal ? new(sizeU, sizeV) : new(sizeV, sizeU);
Expand Down Expand Up @@ -486,27 +477,25 @@ private Rect EstimateViewport()
return viewport;
}

private void GenerateElements(Size availableSize, ref MeasureViewport viewport)
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 items = Items;
var index = viewport.anchorIndex;
var horizontal = Orientation == Orientation.Horizontal;
var index = viewport.firstIndex;
var u = viewport.startU;

// Nothing to do here.
if (items.Count == 0)
{
viewport.endU = 0;
return;
}
var u = viewport.anchorU;

// The layout is likely invalid. Don't create any elements and instead rely on our previous
// element size estimates to calculate a new desired size and trigger a new layout pass.
if (index < 0 || index >= items.Count)
return;
// 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);
Expand All @@ -522,8 +511,33 @@ private void GenerateElements(Size availableSize, ref MeasureViewport viewport)
++index;
} while (u < viewport.viewportUEnd && index < items.Count);

viewport.endU = u;
// 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)
Expand Down Expand Up @@ -874,13 +888,13 @@ public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment

private struct MeasureViewport
{
public int firstIndex;
public int lastIndex;
public int anchorIndex;
public double anchorU;
public double viewportUStart;
public double viewportUEnd;
public double measuredV;
public double startU;
public double endU;
public double realizedEndU;
public int lastIndex;
}
}
}
Loading

0 comments on commit 549131a

Please sign in to comment.