mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-22 09:23:53 +08:00
Add Carousel
This commit is contained in:
@@ -6,7 +6,6 @@ namespace AtomUI.Controls;
|
||||
public interface IWaveSpiritAwareControl : IMotionAwareControl
|
||||
{
|
||||
bool IsWaveSpiritEnabled { get; }
|
||||
// Color WaveSpiritColor { get; }
|
||||
}
|
||||
|
||||
public abstract class WaveSpiritAwareControlProperty : MotionAwareControlProperty
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<ResourceInclude Source="CheckBox/Themes/CheckBoxThemes.axaml" />
|
||||
<ResourceInclude Source="Chrome/Themes/ChromeThemes.axaml" />
|
||||
<ResourceInclude Source="Card/Themes/CardThemes.axaml" />
|
||||
<ResourceInclude Source="Carousel/Themes/CarouselThemes.axaml" />
|
||||
<ResourceInclude Source="Collapse/Themes/CollapseItemThemes.axaml" />
|
||||
<ResourceInclude Source="ComboBox/Themes/ComboBoxThemes.axaml" />
|
||||
<ResourceInclude Source="DatePicker/Themes/DatePickerThemes.axaml" />
|
||||
|
||||
@@ -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<bool> IsShowNavButtonsProperty =
|
||||
AvaloniaProperty.Register<Carousel, bool>(nameof(IsShowNavButtons));
|
||||
|
||||
public static readonly StyledProperty<bool> IsAutoPlayProperty =
|
||||
AvaloniaProperty.Register<Carousel, bool>(nameof(IsAutoPlay));
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> AutoPlaySpeedProperty =
|
||||
AvaloniaProperty.Register<Carousel, TimeSpan>(nameof(AutoPlaySpeed), TimeSpan.FromMilliseconds(3000));
|
||||
|
||||
public static readonly StyledProperty<CarouselPaginationPosition> PaginationPositionProperty =
|
||||
AvaloniaProperty.Register<Carousel, CarouselPaginationPosition>(nameof(PaginationPosition), CarouselPaginationPosition.Bottom);
|
||||
|
||||
public static readonly StyledProperty<bool> IsShowPaginationProperty =
|
||||
AvaloniaProperty.Register<Carousel, bool>(nameof(IsShowPagination));
|
||||
|
||||
public static readonly StyledProperty<bool> IsDraggableProperty =
|
||||
AvaloniaProperty.Register<Carousel, bool>(nameof(IsDraggable));
|
||||
|
||||
public static readonly StyledProperty<bool> IsInfiniteProperty =
|
||||
AvaloniaProperty.Register<Carousel, bool>(nameof(IsInfinite));
|
||||
|
||||
public static readonly StyledProperty<TimeSpan> PageTransitionDurationProperty =
|
||||
AvaloniaProperty.Register<Carousel, TimeSpan>(nameof(PageTransitionDuration));
|
||||
|
||||
public static readonly StyledProperty<Easing> PageInEasingProperty =
|
||||
AvaloniaProperty.Register<Carousel, Easing>(nameof(PageInEasing));
|
||||
|
||||
public static readonly StyledProperty<Easing> PageOutEasingProperty =
|
||||
AvaloniaProperty.Register<Carousel, Easing>(nameof(PageOutEasing));
|
||||
|
||||
public static readonly StyledProperty<CarouselTransitionEffect> TransitionEffectProperty =
|
||||
AvaloniaProperty.Register<Carousel, CarouselTransitionEffect>(nameof(TransitionEffect));
|
||||
|
||||
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
|
||||
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<Carousel>();
|
||||
|
||||
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<IPageTransition?> PageTransitionProperty =
|
||||
AvaloniaProperty.Register<Carousel, IPageTransition?>(nameof(PageTransition));
|
||||
|
||||
internal static readonly DirectProperty<Carousel, bool> PreviousNavButtonVisibleProperty =
|
||||
AvaloniaProperty.RegisterDirect<Carousel, bool>(
|
||||
nameof(PreviousNavButtonVisible),
|
||||
o => o.PreviousNavButtonVisible,
|
||||
(o, v) => o.PreviousNavButtonVisible = v);
|
||||
|
||||
internal static readonly DirectProperty<Carousel, bool> NextNavButtonVisibleProperty =
|
||||
AvaloniaProperty.RegisterDirect<Carousel, bool>(
|
||||
nameof(NextNavButtonVisible),
|
||||
o => o.NextNavButtonVisible,
|
||||
(o, v) => o.NextNavButtonVisible = v);
|
||||
|
||||
private static readonly FuncTemplate<Panel?> 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<Carousel>(SelectionMode.AlwaysSelected);
|
||||
ItemsPanelProperty.OverrideDefaultValue<Carousel>(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<IScrollable>(CarouselThemeConstants.ScrollViewerPart);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property == SelectedIndexProperty && _scroller is not null)
|
||||
{
|
||||
var value = change.GetNewValue<int>();
|
||||
_scroller.Offset = new(value, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
public enum CarouselPaginationPosition
|
||||
{
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
public enum CarouselTransitionEffect
|
||||
{
|
||||
Scroll,
|
||||
Fade
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
public class CarouselPage : ContentControl
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
internal class CarouselPageIndicator : TemplatedControl
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
internal class CarouselPagination : TemplatedControl
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示点宽度
|
||||
/// Width of indicator
|
||||
/// </summary>
|
||||
public double IndicatorWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指示点高度
|
||||
/// Height of indicator
|
||||
/// </summary>
|
||||
public double IndicatorHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指示点之间的间距
|
||||
/// gap between indicator
|
||||
/// </summary>
|
||||
public double IndicatorGap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指示点距离边缘的距离
|
||||
/// dot offset to Carousel edge
|
||||
/// </summary>
|
||||
public double IndicatorOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 激活态指示点宽度
|
||||
/// Width of active indicator
|
||||
/// </summary>
|
||||
public double IndicatorActiveWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 切换箭头大小
|
||||
/// Size of arrows
|
||||
/// </summary>
|
||||
public double ArrowSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 切换箭头边距
|
||||
/// arrows offset to Carousel edge
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:CarouselPageIndicator}" TargetType="atom:CarouselPageIndicator">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,19 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:CarouselPage}" TargetType="atom:CarouselPage">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ContentPresenter Name="ContentPresenter"
|
||||
Margin="{TemplateBinding Margin}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AtomUI.Controls.Themes;
|
||||
|
||||
internal static class CarouselThemeConstants
|
||||
{
|
||||
public const string ScrollViewerPart = "PART_ScrollViewer";
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:CarouselPagination}" TargetType="atom:CarouselPagination">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,29 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:Carousel}" TargetType="atom:Carousel">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Panel>
|
||||
<ScrollViewer Name="{x:Static atom:CarouselThemeConstants.ScrollViewerPart}"
|
||||
Background="{TemplateBinding Background}"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
VerticalScrollBarVisibility="Hidden">
|
||||
<ItemsPresenter
|
||||
Name="PART_ItemsPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||
</ScrollViewer>
|
||||
<atom:IconButton Name="PreviousPageButton"/>
|
||||
<atom:IconButton Name="NextPageButton"/>
|
||||
<atom:CarouselPagination Name="Pagination"/>
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,10 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:ClassModifier="internal">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="CarouselPageIndicatorTheme.axaml"/>
|
||||
<ResourceInclude Source="CarouselPaginationTheme.axaml"/>
|
||||
<ResourceInclude Source="CarouselPageTheme.axaml"/>
|
||||
<ResourceInclude Source="CarouselTheme.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -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<object?> RecycleKeyProperty =
|
||||
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
|
||||
|
||||
private static readonly object 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;
|
||||
|
||||
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<Control>? 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<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, 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;
|
||||
}
|
||||
}
|
||||
+1
@@ -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));
|
||||
|
||||
+11
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user