From a598f52374e6c06bdeee53bee2d29fce57ded636 Mon Sep 17 00:00:00 2001 From: CHINBOY Date: Mon, 22 Sep 2025 18:47:52 +0800 Subject: [PATCH] Add Carousel --- .../Controls/IWaveSpiritAwareControl.cs | 1 - .../AtomUIOSSControlThemesProvider.axaml | 1 + src/AtomUI.Controls/Carousel/Carousel.cs | 226 ++++++++++ src/AtomUI.Controls/Carousel/CarouselEnums.cs | 15 + src/AtomUI.Controls/Carousel/CarouselPage.cs | 8 + .../Carousel/CarouselPageIndicator.cs | 8 + .../Carousel/CarouselPagination.cs | 8 + src/AtomUI.Controls/Carousel/CarouselToken.cs | 68 +++ .../Themes/CarouselPageIndicatorTheme.axaml | 13 + .../Carousel/Themes/CarouselPageTheme.axaml | 19 + .../Themes/CarouselPageThemeConstants.cs | 6 + .../Themes/CarouselPaginationTheme.axaml | 13 + .../Carousel/Themes/CarouselTheme.axaml | 29 ++ .../Carousel/Themes/CarouselThemes.axaml | 10 + .../Carousel/VirtualizingCarouselPanel.cs | 402 ++++++++++++++++++ .../ControlTokenTypePool.g.cs | 1 + .../TokenResourceConst.g.cs | 11 + 17 files changed, 838 insertions(+), 1 deletion(-) create mode 100644 src/AtomUI.Controls/Carousel/Carousel.cs create mode 100644 src/AtomUI.Controls/Carousel/CarouselEnums.cs create mode 100644 src/AtomUI.Controls/Carousel/CarouselPage.cs create mode 100644 src/AtomUI.Controls/Carousel/CarouselPageIndicator.cs create mode 100644 src/AtomUI.Controls/Carousel/CarouselPagination.cs create mode 100644 src/AtomUI.Controls/Carousel/CarouselToken.cs create mode 100644 src/AtomUI.Controls/Carousel/Themes/CarouselPageIndicatorTheme.axaml create mode 100644 src/AtomUI.Controls/Carousel/Themes/CarouselPageTheme.axaml create mode 100644 src/AtomUI.Controls/Carousel/Themes/CarouselPageThemeConstants.cs create mode 100644 src/AtomUI.Controls/Carousel/Themes/CarouselPaginationTheme.axaml create mode 100644 src/AtomUI.Controls/Carousel/Themes/CarouselTheme.axaml create mode 100644 src/AtomUI.Controls/Carousel/Themes/CarouselThemes.axaml create mode 100644 src/AtomUI.Controls/Carousel/VirtualizingCarouselPanel.cs diff --git a/src/AtomUI.Base/Controls/IWaveSpiritAwareControl.cs b/src/AtomUI.Base/Controls/IWaveSpiritAwareControl.cs index ad5f76cb1..8e518847e 100644 --- a/src/AtomUI.Base/Controls/IWaveSpiritAwareControl.cs +++ b/src/AtomUI.Base/Controls/IWaveSpiritAwareControl.cs @@ -6,7 +6,6 @@ namespace AtomUI.Controls; public interface IWaveSpiritAwareControl : IMotionAwareControl { bool IsWaveSpiritEnabled { get; } - // Color WaveSpiritColor { get; } } public abstract class WaveSpiritAwareControlProperty : MotionAwareControlProperty diff --git a/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml b/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml index edf709ff3..453be35a8 100644 --- a/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml +++ b/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml @@ -17,6 +17,7 @@ + diff --git a/src/AtomUI.Controls/Carousel/Carousel.cs b/src/AtomUI.Controls/Carousel/Carousel.cs new file mode 100644 index 000000000..2b0d498d4 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Carousel.cs @@ -0,0 +1,226 @@ +using AtomUI.Controls.Themes; +using AtomUI.Theme; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; + +namespace AtomUI.Controls; + +public class Carousel : SelectingItemsControl, + IControlSharedTokenResourcesHost, + IMotionAwareControl +{ + #region 公共属性定义 + public static readonly StyledProperty IsShowNavButtonsProperty = + AvaloniaProperty.Register(nameof(IsShowNavButtons)); + + public static readonly StyledProperty IsAutoPlayProperty = + AvaloniaProperty.Register(nameof(IsAutoPlay)); + + public static readonly StyledProperty AutoPlaySpeedProperty = + AvaloniaProperty.Register(nameof(AutoPlaySpeed), TimeSpan.FromMilliseconds(3000)); + + public static readonly StyledProperty PaginationPositionProperty = + AvaloniaProperty.Register(nameof(PaginationPosition), CarouselPaginationPosition.Bottom); + + public static readonly StyledProperty IsShowPaginationProperty = + AvaloniaProperty.Register(nameof(IsShowPagination)); + + public static readonly StyledProperty IsDraggableProperty = + AvaloniaProperty.Register(nameof(IsDraggable)); + + public static readonly StyledProperty IsInfiniteProperty = + AvaloniaProperty.Register(nameof(IsInfinite)); + + public static readonly StyledProperty PageTransitionDurationProperty = + AvaloniaProperty.Register(nameof(PageTransitionDuration)); + + public static readonly StyledProperty PageInEasingProperty = + AvaloniaProperty.Register(nameof(PageInEasing)); + + public static readonly StyledProperty PageOutEasingProperty = + AvaloniaProperty.Register(nameof(PageOutEasing)); + + public static readonly StyledProperty TransitionEffectProperty = + AvaloniaProperty.Register(nameof(TransitionEffect)); + + public static readonly StyledProperty IsMotionEnabledProperty = + MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); + + public bool IsShowNavButtons + { + get => GetValue(IsShowNavButtonsProperty); + set => SetValue(IsShowNavButtonsProperty, value); + } + + public bool IsAutoPlay + { + get => GetValue(IsAutoPlayProperty); + set => SetValue(IsAutoPlayProperty, value); + } + + public TimeSpan AutoPlaySpeed + { + get => GetValue(AutoPlaySpeedProperty); + set => SetValue(AutoPlaySpeedProperty, value); + } + + public CarouselPaginationPosition PaginationPosition + { + get => GetValue(PaginationPositionProperty); + set => SetValue(PaginationPositionProperty, value); + } + + public bool IsShowPagination + { + get => GetValue(IsShowPaginationProperty); + set => SetValue(IsShowPaginationProperty, value); + } + + public bool IsDraggable + { + get => GetValue(IsDraggableProperty); + set => SetValue(IsDraggableProperty, value); + } + + public bool IsInfinite + { + get => GetValue(IsInfiniteProperty); + set => SetValue(IsInfiniteProperty, value); + } + + public TimeSpan PageTransitionDuration + { + get => GetValue(PageTransitionDurationProperty); + set => SetValue(PageTransitionDurationProperty, value); + } + + public Easing PageInEasing + { + get => GetValue(PageInEasingProperty); + set => SetValue(PageInEasingProperty, value); + } + + public Easing PageOutEasing + { + get => GetValue(PageOutEasingProperty); + set => SetValue(PageOutEasingProperty, value); + } + + public CarouselTransitionEffect TransitionEffect + { + get => GetValue(TransitionEffectProperty); + set => SetValue(TransitionEffectProperty, value); + } + + public bool IsMotionEnabled + { + get => GetValue(IsMotionEnabledProperty); + set => SetValue(IsMotionEnabledProperty, value); + } + #endregion + + #region 内部属性定义 + + internal static readonly StyledProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(PageTransition)); + + internal static readonly DirectProperty PreviousNavButtonVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(PreviousNavButtonVisible), + o => o.PreviousNavButtonVisible, + (o, v) => o.PreviousNavButtonVisible = v); + + internal static readonly DirectProperty NextNavButtonVisibleProperty = + AvaloniaProperty.RegisterDirect( + nameof(NextNavButtonVisible), + o => o.NextNavButtonVisible, + (o, v) => o.NextNavButtonVisible = v); + + private static readonly FuncTemplate DefaultPanel = + new(() => new VirtualizingCarouselPanel()); + + public IPageTransition? PageTransition + { + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); + } + + private bool _previousNavButtonVisible; + + internal bool PreviousNavButtonVisible + { + get => _previousNavButtonVisible; + set => SetAndRaise(PreviousNavButtonVisibleProperty, ref _previousNavButtonVisible, value); + } + + private bool _nextNavButtonVisible; + + internal bool NextNavButtonVisible + { + get => _nextNavButtonVisible; + set => SetAndRaise(NextNavButtonVisibleProperty, ref _nextNavButtonVisible, value); + } + + Control IMotionAwareControl.PropertyBindTarget => this; + Control IControlSharedTokenResourcesHost.HostControl => this; + string IControlSharedTokenResourcesHost.TokenId => CarouselToken.ID; + + #endregion + + private IScrollable? _scroller; + + static Carousel() + { + SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + } + + public void Next() + { + if (SelectedIndex < ItemCount - 1) + { + ++SelectedIndex; + } + } + + public void Previous() + { + if (SelectedIndex > 0) + { + --SelectedIndex; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + var result = base.ArrangeOverride(finalSize); + + if (_scroller is not null) + { + _scroller.Offset = new(SelectedIndex, 0); + } + + return result; + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _scroller = e.NameScope.Find(CarouselThemeConstants.ScrollViewerPart); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SelectedIndexProperty && _scroller is not null) + { + var value = change.GetNewValue(); + _scroller.Offset = new(value, 0); + } + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/CarouselEnums.cs b/src/AtomUI.Controls/Carousel/CarouselEnums.cs new file mode 100644 index 000000000..0a5720c67 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/CarouselEnums.cs @@ -0,0 +1,15 @@ +namespace AtomUI.Controls; + +public enum CarouselPaginationPosition +{ + Top, + Bottom, + Left, + Right +} + +public enum CarouselTransitionEffect +{ + Scroll, + Fade +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/CarouselPage.cs b/src/AtomUI.Controls/Carousel/CarouselPage.cs new file mode 100644 index 000000000..1437c7f84 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/CarouselPage.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace AtomUI.Controls; + +public class CarouselPage : ContentControl +{ + +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/CarouselPageIndicator.cs b/src/AtomUI.Controls/Carousel/CarouselPageIndicator.cs new file mode 100644 index 000000000..e45f4d84b --- /dev/null +++ b/src/AtomUI.Controls/Carousel/CarouselPageIndicator.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls.Primitives; + +namespace AtomUI.Controls; + +internal class CarouselPageIndicator : TemplatedControl +{ + +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/CarouselPagination.cs b/src/AtomUI.Controls/Carousel/CarouselPagination.cs new file mode 100644 index 000000000..460635d44 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/CarouselPagination.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls.Primitives; + +namespace AtomUI.Controls; + +internal class CarouselPagination : TemplatedControl +{ + +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/CarouselToken.cs b/src/AtomUI.Controls/Carousel/CarouselToken.cs new file mode 100644 index 000000000..8d5b088fe --- /dev/null +++ b/src/AtomUI.Controls/Carousel/CarouselToken.cs @@ -0,0 +1,68 @@ +using AtomUI.Theme.TokenSystem; + +namespace AtomUI.Controls; + +[ControlDesignToken] +internal class CarouselToken : AbstractControlDesignToken +{ + public const string ID = "Carousel"; + + public CarouselToken() + : base(ID) + { + } + + /// + /// 指示点宽度 + /// Width of indicator + /// + public double IndicatorWidth { get; set; } + + /// + /// 指示点高度 + /// Height of indicator + /// + public double IndicatorHeight { get; set; } + + /// + /// 指示点之间的间距 + /// gap between indicator + /// + public double IndicatorGap { get; set; } + + /// + /// 指示点距离边缘的距离 + /// dot offset to Carousel edge + /// + public double IndicatorOffset { get; set; } + + /// + /// 激活态指示点宽度 + /// Width of active indicator + /// + public double IndicatorActiveWidth { get; set; } + + /// + /// 切换箭头大小 + /// Size of arrows + /// + public double ArrowSize { get; set; } + + /// + /// 切换箭头边距 + /// arrows offset to Carousel edge + /// + public double ArrowOffset { get; set; } + + public override void CalculateFromAlias() + { + base.CalculateFromAlias(); + IndicatorActiveWidth = 24; + ArrowSize = 16; + ArrowOffset = SharedToken.SpacingXS; + IndicatorWidth = 16; + IndicatorHeight = 3; + IndicatorGap = SharedToken.SpacingXXS; + IndicatorOffset = 12; + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/Themes/CarouselPageIndicatorTheme.axaml b/src/AtomUI.Controls/Carousel/Themes/CarouselPageIndicatorTheme.axaml new file mode 100644 index 000000000..1bf7588f6 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Themes/CarouselPageIndicatorTheme.axaml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/Themes/CarouselPageTheme.axaml b/src/AtomUI.Controls/Carousel/Themes/CarouselPageTheme.axaml new file mode 100644 index 000000000..a61a3a6e1 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Themes/CarouselPageTheme.axaml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/Themes/CarouselPageThemeConstants.cs b/src/AtomUI.Controls/Carousel/Themes/CarouselPageThemeConstants.cs new file mode 100644 index 000000000..a5ffe25a0 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Themes/CarouselPageThemeConstants.cs @@ -0,0 +1,6 @@ +namespace AtomUI.Controls.Themes; + +internal static class CarouselThemeConstants +{ + public const string ScrollViewerPart = "PART_ScrollViewer"; +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/Themes/CarouselPaginationTheme.axaml b/src/AtomUI.Controls/Carousel/Themes/CarouselPaginationTheme.axaml new file mode 100644 index 000000000..fddf584be --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Themes/CarouselPaginationTheme.axaml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/Themes/CarouselTheme.axaml b/src/AtomUI.Controls/Carousel/Themes/CarouselTheme.axaml new file mode 100644 index 000000000..b6fb2a7d8 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Themes/CarouselTheme.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/Themes/CarouselThemes.axaml b/src/AtomUI.Controls/Carousel/Themes/CarouselThemes.axaml new file mode 100644 index 000000000..31ef7efb2 --- /dev/null +++ b/src/AtomUI.Controls/Carousel/Themes/CarouselThemes.axaml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Carousel/VirtualizingCarouselPanel.cs b/src/AtomUI.Controls/Carousel/VirtualizingCarouselPanel.cs new file mode 100644 index 000000000..fe711c11e --- /dev/null +++ b/src/AtomUI.Controls/Carousel/VirtualizingCarouselPanel.cs @@ -0,0 +1,402 @@ +using System.Collections.Specialized; +using System.Diagnostics; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace AtomUI.Controls; + +internal class VirtualizingCarouselPanel: VirtualizingPanel, ILogicalScrollable +{ + private static readonly AttachedProperty RecycleKeyProperty = + AvaloniaProperty.RegisterAttached("RecycleKey"); + + private static readonly object ItemIsItsOwnContainer = new object(); + private Size _extent; + private Vector _offset; + private Size _viewport; + private Dictionary>? _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(); + + 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 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) == ItemIsItsOwnContainer) + return c; + return null; + } + + protected override IEnumerable? GetRealizedContainers() + { + return _realized is not null ? new[] { _realized } : null; + } + + protected override int IndexFromContainer(Control container) + { + return container == _realized ? _realizedIndex : -1; + } + + protected 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: + 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 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, 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 == 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; + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs index ef806a600..e2171cc4b 100644 --- a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs +++ b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs @@ -19,6 +19,7 @@ namespace AtomUI.Theme tokenTypes.Add(typeof(AtomUI.Controls.ButtonToken)); tokenTypes.Add(typeof(AtomUI.Controls.CalendarToken)); tokenTypes.Add(typeof(AtomUI.Controls.CardToken)); + tokenTypes.Add(typeof(AtomUI.Controls.CarouselToken)); tokenTypes.Add(typeof(AtomUI.Controls.CheckBoxToken)); tokenTypes.Add(typeof(AtomUI.Controls.ChromeToken)); tokenTypes.Add(typeof(AtomUI.Controls.CollapseToken)); diff --git a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs index 835563325..f2037c1fa 100644 --- a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs +++ b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs @@ -211,6 +211,17 @@ namespace AtomUI.Theme.Styling public static readonly TokenResourceKey TabsMarginBottom = new TokenResourceKey("Card.TabsMarginBottom"); } + public static class CarouselTokenKey + { + public static readonly TokenResourceKey ArrowOffset = new TokenResourceKey("Carousel.ArrowOffset"); + public static readonly TokenResourceKey ArrowSize = new TokenResourceKey("Carousel.ArrowSize"); + public static readonly TokenResourceKey IndicatorActiveWidth = new TokenResourceKey("Carousel.IndicatorActiveWidth"); + public static readonly TokenResourceKey IndicatorGap = new TokenResourceKey("Carousel.IndicatorGap"); + public static readonly TokenResourceKey IndicatorHeight = new TokenResourceKey("Carousel.IndicatorHeight"); + public static readonly TokenResourceKey IndicatorOffset = new TokenResourceKey("Carousel.IndicatorOffset"); + public static readonly TokenResourceKey IndicatorWidth = new TokenResourceKey("Carousel.IndicatorWidth"); + } + public static class CheckBoxTokenKey { public static readonly TokenResourceKey CheckedMarkSize = new TokenResourceKey("CheckBox.CheckedMarkSize");