using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Avalonia.Animation; using Avalonia.Controls.Primitives; using Avalonia.Input; namespace Avalonia.Controls { /// /// A panel used by to display the current item. /// public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable { private static readonly AttachedProperty ItemIsOwnContainerProperty = AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); private Size _extent; private Vector _offset; private Size _viewport; private Stack? _recyclePool; private Control? _realized; private int _realizedIndex = -1; private Control? _transitionFrom; private int _transitionFromIndex = -1; private CancellationTokenSource? _transition; private EventHandler? _scrollInvalidated; bool ILogicalScrollable.CanHorizontallyScroll { get; set; } bool ILogicalScrollable.CanVerticallyScroll { get; set; } bool ILogicalScrollable.IsLogicalScrollEnabled => true; Size ILogicalScrollable.ScrollSize => new(1, 1); Size ILogicalScrollable.PageScrollSize => new(1, 1); Size IScrollable.Extent => Extent; Size IScrollable.Viewport => Viewport; Vector IScrollable.Offset { get => _offset; set { if ((int)_offset.X != value.X) InvalidateMeasure(); _offset = value; } } private Size Extent { get => _extent; set { if (_extent != value) { _extent = value; _scrollInvalidated?.Invoke(this, EventArgs.Empty); } } } private Size Viewport { get => _viewport; set { if (_viewport != value) { _viewport = value; _scrollInvalidated?.Invoke(this, EventArgs.Empty); } } } event EventHandler? ILogicalScrollable.ScrollInvalidated { add => _scrollInvalidated += value; remove => _scrollInvalidated -= value; } bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) => false; Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from) => null; void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e); protected override Size MeasureOverride(Size availableSize) { var items = Items; var index = (int)_offset.X; if (index != _realizedIndex) { if (_realized is not null) { var cancelTransition = _transition is not null; // Cancel any already running transition, and recycle the element we're transitioning from. if (cancelTransition) { _transition!.Cancel(); _transition = null; if (_transitionFrom is not null) RecycleElement(_transitionFrom); _transitionFrom = null; _transitionFromIndex = -1; } if (cancelTransition || GetTransition() is null) { // If don't have a transition or we've just canceled a transition then recycle the element // we're moving from. RecycleElement(_realized); } else { // We have a transition to do: record the current element as the element we're transitioning // from and we'll start the transition in the arrange pass. _transitionFrom = _realized; _transitionFromIndex = _realizedIndex; } _realized = null; _realizedIndex = -1; } // Get or create an element for the new item. if (index >= 0 && index < items.Count) { _realized = GetOrCreateElement(items, index); _realizedIndex = index; } } if (_realized is null) { Extent = Viewport = new(0, 0); _transitionFrom = null; _transitionFromIndex = -1; return default; } _realized.Measure(availableSize); Extent = new(items.Count, 1); Viewport = new(1, 1); return _realized.DesiredSize; } protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); if (_transition is null && _transitionFrom is not null && _realized is { } to && GetTransition() is { } transition) { _transition = new CancellationTokenSource(); transition.Start(_transitionFrom, to, _realizedIndex > _transitionFromIndex, _transition.Token) .ContinueWith(TransitionFinished, TaskScheduler.FromCurrentSynchronizationContext()); } return result; } protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap) => null; protected internal override Control? ContainerFromIndex(int index) { return index == _realizedIndex ? _realized : null; } protected internal override IEnumerable? GetRealizedContainers() { return _realized is not null ? new[] { _realized } : null; } protected internal override int IndexFromContainer(Control container) { return container == _realized ? _realizedIndex : -1; } protected internal override Control? ScrollIntoView(int index) { return null; } protected override void OnItemsChanged(IReadOnlyList items, NotifyCollectionChangedEventArgs e) { base.OnItemsChanged(items, e); void Add(int index, int count) { if (index <= _realizedIndex) _realizedIndex += count; } void Remove(int index, int count) { var end = index + (count - 1); if (_realized is not null && index <= _realizedIndex && end >= _realizedIndex) { RecycleElement(_realized); _realized = null; _realizedIndex = -1; } else if (index < _realizedIndex) { _realizedIndex -= count; } } switch (e.Action) { case NotifyCollectionChangedAction.Add: Add(e.NewStartingIndex, e.NewItems!.Count); break; case NotifyCollectionChangedAction.Remove: Remove(e.OldStartingIndex, e.OldItems!.Count); break; case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: Remove(e.OldStartingIndex, e.OldItems!.Count); Add(e.NewStartingIndex, e.NewItems!.Count); break; case NotifyCollectionChangedAction.Reset: if (_realized is not null) { RecycleElement(_realized); _realized = null; _realizedIndex = -1; } break; } InvalidateMeasure(); } private Control GetOrCreateElement(IReadOnlyList items, int index) { return GetRealizedElement(index) ?? GetItemIsOwnContainer(items, index) ?? GetRecycledElement(items, index) ?? CreateElement(items, index); } private Control? GetRealizedElement(int index) { return _realizedIndex == index ? _realized : null; } private Control? GetItemIsOwnContainer(IReadOnlyList items, int index) { Debug.Assert(ItemContainerGenerator is not null); var item = items[index]; if (item is Control controlItem) { var generator = ItemContainerGenerator; if (controlItem.IsSet(ItemIsOwnContainerProperty)) { controlItem.IsVisible = true; generator.ItemContainerPrepared(controlItem, item, index); return controlItem; } else if (generator.IsItemItsOwnContainer(controlItem)) { generator.PrepareItemContainer(controlItem, controlItem, index); AddInternalChild(controlItem); controlItem.SetValue(ItemIsOwnContainerProperty, true); generator.ItemContainerPrepared(controlItem, item, index); return controlItem; } } return null; } private Control? GetRecycledElement(IReadOnlyList items, int index) { Debug.Assert(ItemContainerGenerator is not null); var generator = ItemContainerGenerator; var item = items[index]; if (_recyclePool?.Count > 0) { var recycled = _recyclePool.Pop(); recycled.IsVisible = true; generator.PrepareItemContainer(recycled, item, index); generator.ItemContainerPrepared(recycled, item, index); return recycled; } return null; } private Control CreateElement(IReadOnlyList items, int index) { Debug.Assert(ItemContainerGenerator is not null); var generator = ItemContainerGenerator; var item = items[index]; var container = generator.CreateContainer(); generator.PrepareItemContainer(container, item, index); AddInternalChild(container); generator.ItemContainerPrepared(container, item, index); return container; } private void RecycleElement(Control element) { Debug.Assert(ItemContainerGenerator is not null); if (element.IsSet(ItemIsOwnContainerProperty)) { element.IsVisible = false; } else { ItemContainerGenerator.ClearItemContainer(element); _recyclePool ??= new(); _recyclePool.Push(element); element.IsVisible = false; } } private IPageTransition? GetTransition() => (ItemsControl as Carousel)?.PageTransition; private void TransitionFinished(Task task) { if (task.IsCanceled) return; if (_transitionFrom is not null) RecycleElement(_transitionFrom); _transition = null; _transitionFrom = null; _transitionFromIndex = -1; } } }