maui-linux/Services/VirtualizationManager.cs

308 lines
9.0 KiB
C#

// 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;
/// <summary>
/// Manages view recycling for virtualized lists and collections.
/// Implements a pool-based recycling strategy to minimize allocations.
/// </summary>
public class VirtualizationManager<T> where T : SkiaView
{
private readonly Dictionary<int, T> _activeViews = new();
private readonly Queue<T> _recyclePool = new();
private readonly Func<T> _viewFactory;
private readonly Action<T>? _viewRecycler;
private readonly int _maxPoolSize;
private int _firstVisibleIndex = -1;
private int _lastVisibleIndex = -1;
/// <summary>
/// Number of views currently active (bound to data).
/// </summary>
public int ActiveViewCount => _activeViews.Count;
/// <summary>
/// Number of views in the recycle pool.
/// </summary>
public int PooledViewCount => _recyclePool.Count;
/// <summary>
/// Current visible range.
/// </summary>
public (int First, int Last) VisibleRange => (_firstVisibleIndex, _lastVisibleIndex);
/// <summary>
/// Creates a new virtualization manager.
/// </summary>
/// <param name="viewFactory">Factory function to create new views.</param>
/// <param name="viewRecycler">Optional function to reset views before recycling.</param>
/// <param name="maxPoolSize">Maximum number of views to keep in the recycle pool.</param>
public VirtualizationManager(
Func<T> viewFactory,
Action<T>? viewRecycler = null,
int maxPoolSize = 20)
{
_viewFactory = viewFactory ?? throw new ArgumentNullException(nameof(viewFactory));
_viewRecycler = viewRecycler;
_maxPoolSize = maxPoolSize;
}
/// <summary>
/// Updates the visible range and recycles views that scrolled out of view.
/// </summary>
/// <param name="firstVisible">Index of first visible item.</param>
/// <param name="lastVisible">Index of last visible item.</param>
public void UpdateVisibleRange(int firstVisible, int lastVisible)
{
if (firstVisible == _firstVisibleIndex && lastVisible == _lastVisibleIndex)
return;
// Recycle views that scrolled out of view
var toRecycle = new List<int>();
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;
}
/// <summary>
/// Gets or creates a view for the specified index.
/// </summary>
/// <param name="index">Item index.</param>
/// <param name="bindData">Action to bind data to the view.</param>
/// <returns>A view bound to the data.</returns>
public T GetOrCreateView(int index, Action<T> 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;
}
/// <summary>
/// Gets an existing view for the index, or null if not active.
/// </summary>
public T? GetActiveView(int index)
{
return _activeViews.TryGetValue(index, out var view) ? view : default;
}
/// <summary>
/// Recycles a view at the specified index.
/// </summary>
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();
}
}
/// <summary>
/// Clears all active views and the recycle pool.
/// </summary>
public void Clear()
{
foreach (var view in _activeViews.Values)
{
view.Dispose();
}
_activeViews.Clear();
while (_recyclePool.Count > 0)
{
_recyclePool.Dequeue().Dispose();
}
_firstVisibleIndex = -1;
_lastVisibleIndex = -1;
}
/// <summary>
/// Removes a specific item and recycles its view.
/// </summary>
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;
}
}
/// <summary>
/// Inserts an item and shifts existing indices.
/// </summary>
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;
}
}
}
/// <summary>
/// Extension methods for virtualization.
/// </summary>
public static class VirtualizationExtensions
{
/// <summary>
/// Calculates visible item range for a vertical list.
/// </summary>
/// <param name="scrollOffset">Current scroll offset.</param>
/// <param name="viewportHeight">Height of visible area.</param>
/// <param name="itemHeight">Height of each item (fixed).</param>
/// <param name="itemSpacing">Spacing between items.</param>
/// <param name="totalItems">Total number of items.</param>
/// <returns>Tuple of (firstVisible, lastVisible) indices.</returns>
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);
}
/// <summary>
/// Calculates visible item range for variable height items.
/// </summary>
/// <param name="scrollOffset">Current scroll offset.</param>
/// <param name="viewportHeight">Height of visible area.</param>
/// <param name="getItemHeight">Function to get height of item at index.</param>
/// <param name="itemSpacing">Spacing between items.</param>
/// <param name="totalItems">Total number of items.</param>
/// <returns>Tuple of (firstVisible, lastVisible) indices.</returns>
public static (int first, int last) CalculateVisibleRangeVariable(
float scrollOffset,
float viewportHeight,
Func<int, float> 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);
}
/// <summary>
/// Calculates visible item range for a grid layout.
/// </summary>
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);
}
}