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