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