308 lines
9.0 KiB
C#
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);
|
|
}
|
|
}
|