A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

416 lines
14 KiB

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
{
/// <summary>
/// A panel used by <see cref="Carousel"/> to display the current item.
/// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly object s_itemIsItsOwnContainer = new object();
private Size _extent;
private Vector _offset;
private Size _viewport;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
private int _transitionFromIndex = -1;
private CancellationTokenSource? _transition;
private EventHandler? _scrollInvalidated;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
bool ILogicalScrollable.CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set => _canHorizontallyScroll = value;
}
bool ILogicalScrollable.CanVerticallyScroll
{
get => _canVerticallyScroll;
set => _canVerticallyScroll = value;
}
bool IScrollable.CanHorizontallyScroll => _canHorizontallyScroll;
bool IScrollable.CanVerticallyScroll => _canVerticallyScroll;
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();
var forward = (_realizedIndex > _transitionFromIndex);
if (Items.Count > 2)
{
forward = forward || (_transitionFromIndex == Items.Count - 1 && _realizedIndex == 0);
forward = forward && !(_transitionFromIndex == 0 && _realizedIndex == Items.Count - 1);
}
transition.Start(_transitionFrom, to, forward, _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)
{
if (index < 0 || index >= Items.Count)
return null;
if (index == _realizedIndex)
return _realized;
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
protected internal override IEnumerable<Control>? 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<object?> 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:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Move:
if (e.OldStartingIndex < 0)
{
goto case NotifyCollectionChangedAction.Reset;
}
Remove(e.OldStartingIndex, e.OldItems!.Count);
var insertIndex = e.NewStartingIndex;
if (e.NewStartingIndex > e.OldStartingIndex)
{
insertIndex -= e.OldItems.Count - 1;
}
Add(insertIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Reset:
if (_realized is not null)
{
RecycleElement(_realized);
_realized = null;
_realizedIndex = -1;
}
break;
}
InvalidateMeasure();
}
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
private Control? GetRealizedElement(int index)
{
return _realizedIndex == index ? _realized : null;
}
private Control GetItemAsOwnContainer(object? item, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (!controlItem.IsSet(RecycleKeyProperty))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && 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(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator!;
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
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);
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
else
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.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;
}
}
}