// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
///
/// Manages view recycling for virtualized lists and collections.
/// Implements a pool-based recycling strategy to minimize allocations.
///
public class VirtualizationManager where T : SkiaView
{
private readonly Dictionary _activeViews = new();
private readonly Queue _recyclePool = new();
private readonly Func _viewFactory;
private readonly Action? _viewRecycler;
private readonly int _maxPoolSize;
private int _firstVisibleIndex = -1;
private int _lastVisibleIndex = -1;
///
/// Number of views currently active (bound to data).
///
public int ActiveViewCount => _activeViews.Count;
///
/// Number of views in the recycle pool.
///
public int PooledViewCount => _recyclePool.Count;
///
/// Current visible range.
///
public (int First, int Last) VisibleRange => (_firstVisibleIndex, _lastVisibleIndex);
///
/// Creates a new virtualization manager.
///
/// Factory function to create new views.
/// Optional function to reset views before recycling.
/// Maximum number of views to keep in the recycle pool.
public VirtualizationManager(
Func viewFactory,
Action? viewRecycler = null,
int maxPoolSize = 20)
{
_viewFactory = viewFactory ?? throw new ArgumentNullException(nameof(viewFactory));
_viewRecycler = viewRecycler;
_maxPoolSize = maxPoolSize;
}
///
/// Updates the visible range and recycles views that scrolled out of view.
///
/// Index of first visible item.
/// Index of last visible item.
public void UpdateVisibleRange(int firstVisible, int lastVisible)
{
if (firstVisible == _firstVisibleIndex && lastVisible == _lastVisibleIndex)
return;
// Recycle views that scrolled out of view
var toRecycle = new List();
foreach (var kvp in _activeViews)
{
if (kvp.Key < firstVisible || kvp.Key > lastVisible)
{
toRecycle.Add(kvp.Key);
}
}
foreach (var index in toRecycle)
{
RecycleView(index);
}
_firstVisibleIndex = firstVisible;
_lastVisibleIndex = lastVisible;
}
///
/// Gets or creates a view for the specified index.
///
/// Item index.
/// Action to bind data to the view.
/// A view bound to the data.
public T GetOrCreateView(int index, Action bindData)
{
if (_activeViews.TryGetValue(index, out var existing))
{
return existing;
}
// Get from pool or create new
T view;
if (_recyclePool.Count > 0)
{
view = _recyclePool.Dequeue();
}
else
{
view = _viewFactory();
}
// Bind data
bindData(view);
_activeViews[index] = view;
return view;
}
///
/// Gets an existing view for the index, or null if not active.
///
public T? GetActiveView(int index)
{
return _activeViews.TryGetValue(index, out var view) ? view : default;
}
///
/// Recycles a view at the specified index.
///
private void RecycleView(int index)
{
if (!_activeViews.TryGetValue(index, out var view))
return;
_activeViews.Remove(index);
// Reset the view
_viewRecycler?.Invoke(view);
// Add to pool if not full
if (_recyclePool.Count < _maxPoolSize)
{
_recyclePool.Enqueue(view);
}
else
{
// Pool is full, dispose the view
view.Dispose();
}
}
///
/// Clears all active views and the recycle pool.
///
public void Clear()
{
foreach (var view in _activeViews.Values)
{
view.Dispose();
}
_activeViews.Clear();
while (_recyclePool.Count > 0)
{
_recyclePool.Dequeue().Dispose();
}
_firstVisibleIndex = -1;
_lastVisibleIndex = -1;
}
///
/// Removes a specific item and recycles its view.
///
public void RemoveItem(int index)
{
RecycleView(index);
// Shift indices for items after the removed one
var toShift = _activeViews
.Where(kvp => kvp.Key > index)
.OrderBy(kvp => kvp.Key)
.ToList();
foreach (var kvp in toShift)
{
_activeViews.Remove(kvp.Key);
_activeViews[kvp.Key - 1] = kvp.Value;
}
}
///
/// Inserts an item and shifts existing indices.
///
public void InsertItem(int index)
{
// Shift indices for items at or after the insert position
var toShift = _activeViews
.Where(kvp => kvp.Key >= index)
.OrderByDescending(kvp => kvp.Key)
.ToList();
foreach (var kvp in toShift)
{
_activeViews.Remove(kvp.Key);
_activeViews[kvp.Key + 1] = kvp.Value;
}
}
}
///
/// Extension methods for virtualization.
///
public static class VirtualizationExtensions
{
///
/// Calculates visible item range for a vertical list.
///
/// Current scroll offset.
/// Height of visible area.
/// Height of each item (fixed).
/// Spacing between items.
/// Total number of items.
/// Tuple of (firstVisible, lastVisible) indices.
public static (int first, int last) CalculateVisibleRange(
float scrollOffset,
float viewportHeight,
float itemHeight,
float itemSpacing,
int totalItems)
{
if (totalItems == 0)
return (-1, -1);
var rowHeight = itemHeight + itemSpacing;
var first = Math.Max(0, (int)(scrollOffset / rowHeight));
var last = Math.Min(totalItems - 1, (int)((scrollOffset + viewportHeight) / rowHeight) + 1);
return (first, last);
}
///
/// Calculates visible item range for variable height items.
///
/// Current scroll offset.
/// Height of visible area.
/// Function to get height of item at index.
/// Spacing between items.
/// Total number of items.
/// Tuple of (firstVisible, lastVisible) indices.
public static (int first, int last) CalculateVisibleRangeVariable(
float scrollOffset,
float viewportHeight,
Func getItemHeight,
float itemSpacing,
int totalItems)
{
if (totalItems == 0)
return (-1, -1);
int first = 0;
float cumulativeHeight = 0;
// Find first visible
for (int i = 0; i < totalItems; i++)
{
var itemHeight = getItemHeight(i);
if (cumulativeHeight + itemHeight > scrollOffset)
{
first = i;
break;
}
cumulativeHeight += itemHeight + itemSpacing;
}
// Find last visible
int last = first;
var endOffset = scrollOffset + viewportHeight;
for (int i = first; i < totalItems; i++)
{
var itemHeight = getItemHeight(i);
if (cumulativeHeight > endOffset)
{
break;
}
last = i;
cumulativeHeight += itemHeight + itemSpacing;
}
return (first, last);
}
///
/// Calculates visible item range for a grid layout.
///
public static (int firstRow, int lastRow) CalculateVisibleGridRange(
float scrollOffset,
float viewportHeight,
float rowHeight,
float rowSpacing,
int totalRows)
{
if (totalRows == 0)
return (-1, -1);
var effectiveRowHeight = rowHeight + rowSpacing;
var first = Math.Max(0, (int)(scrollOffset / effectiveRowHeight));
var last = Math.Min(totalRows - 1, (int)((scrollOffset + viewportHeight) / effectiveRowHeight) + 1);
return (first, last);
}
}