Toolkit.UI.Controls.Avalonia

This commit is contained in:
TheXamlGuy
2024-04-13 11:41:33 +01:00
parent 62a7e94e19
commit 862e7b2e34
97 changed files with 8558 additions and 0 deletions
@@ -0,0 +1,95 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Toolkit.UI.Controls.Avalonia">
<Design.PreviewWith>
<Border Padding="20" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type controls:CarouselView}" TargetType="controls:CarouselView">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="Container" Background="{TemplateBinding Background}">
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Rectangle
x:Name="Indicator"
Width="10"
Height="10"
VerticalAlignment="Top" />
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type controls:CarouselViewItem}" TargetType="controls:CarouselViewItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter
x:Name="Content"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="{StaticResource ControlContentThemeFontSize}"
Foreground="{TemplateBinding Foreground}" />
</ControlTemplate>
</Setter>
<Style Selector="^:selected /template/ ContentPresenter#Content">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0.4" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="0.9" />
<Setter Property="ScaleTransform.ScaleY" Value="0.9" />
</KeyFrame>
<KeyFrame Cue="100%" KeySpline="0,0 0,1">
<Setter Property="ScaleTransform.ScaleX" Value="1.0" />
<Setter Property="ScaleTransform.ScaleY" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:not(:selected) /template/ ContentPresenter#Content">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0.4" />
</KeyFrame>
</Animation>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1.0" />
<Setter Property="ScaleTransform.ScaleY" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%" KeySpline="0,0 0,1">
<Setter Property="ScaleTransform.ScaleX" Value="0.9" />
<Setter Property="ScaleTransform.ScaleY" Value="0.9" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -0,0 +1,313 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
using System.Collections.Specialized;
using System.Numerics;
namespace Toolkit.UI.Controls.Avalonia;
public class CarouselView :
ItemsControl
{
private readonly TimeSpan animationDuration = TimeSpan.FromMilliseconds(500);
private readonly List<ExpressionAnimation> animations = [];
private readonly int columnCount = 5;
private readonly List<CompositionVisual> itemVisuals = [];
private readonly ScopedBatchHelper scopedBatch = new();
private readonly double spacing = 12;
private Compositor? compositor;
private Grid? container;
private Vector3D finalOffset;
private float horizontalDelta;
private Rectangle? indicator;
private Vector3DKeyFrameAnimation? indicatorAnimation;
private CompositionVisual? indicatorVisual;
private bool isAnimating;
private bool isPressed;
private List<Border>? items;
private Point? lastPosition;
private int newIndex;
private int SelectedIndex;
private Point? startPosition;
private CompositionVisual? touchAreaVisual;
protected override void OnApplyTemplate(TemplateAppliedEventArgs args)
{
container = args.NameScope.Get<Grid>("Container");
if (container is not null)
{
items = container.Children.OfType<Border>().ToList();
foreach (Border item in items)
{
if (item.Child is CarouselViewItem contentControl)
{
contentControl.ContentTemplate = ItemTemplate;
}
}
}
indicator = args.NameScope.Get<Rectangle>("Indicator");
if (indicator is not null)
{
indicatorVisual = ElementComposition.GetElementVisual(indicator);
}
ItemsView.CollectionChanged -= OnCollectionChanged;
ItemsView.CollectionChanged += OnCollectionChanged;
base.OnApplyTemplate(args);
}
protected override void OnSizeChanged(SizeChangedEventArgs args)
{
base.OnSizeChanged(args);
ArrangeItems(newIndex, isAnimating: false);
}
protected override void OnLoaded(RoutedEventArgs args)
{
if (container is not null
&& items is not null
&& indicator is not null)
{
indicatorVisual = ElementComposition.GetElementVisual(indicator);
touchAreaVisual = ElementComposition.GetElementVisual(container);
if (touchAreaVisual is not null)
{
compositor = touchAreaVisual.Compositor;
}
itemVisuals.Clear();
foreach (Border item in items)
{
if (ElementComposition.GetElementVisual(item) is CompositionVisual visual)
{
itemVisuals.Add(visual);
}
}
ArrangeItems(newIndex);
}
base.OnLoaded(args);
}
protected override void OnPointerMoved(PointerEventArgs args)
{
if (isPressed && indicatorVisual is not null && startPosition.HasValue)
{
lastPosition = args.GetPosition(container);
horizontalDelta = (float)(lastPosition.Value.X - startPosition.Value.X);
indicatorVisual.Offset = new Vector3(horizontalDelta, 0.0f, 0.0f);
}
base.OnPointerMoved(args);
}
protected override void OnPointerPressed(PointerPressedEventArgs args)
{
if (!isPressed && indicatorVisual is not null)
{
if (!isAnimating)
{
horizontalDelta = 0;
isPressed = true;
startPosition = args.GetPosition(container);
indicatorVisual.Offset = new Vector3(horizontalDelta, 0.0f, 0.0f);
PrepareAnimations();
for (int i = 0; i < itemVisuals.Count; i++)
{
itemVisuals[i].StartAnimation("Offset", animations[i]);
}
}
}
base.OnPointerPressed(args);
}
protected override void OnPointerReleased(PointerReleasedEventArgs args)
{
if (isPressed && container is not null
&& items is not null
&& indicatorVisual is not null)
{
isPressed = false;
double itemWidth = items[0].Bounds.Width;
double threshold = itemWidth / 3;
int oldSelectedIndex = newIndex;
double offset = indicatorVisual.Offset.X;
if (offset <= -threshold)
{
newIndex = (newIndex + 1) % 5;
SelectedIndex = (SelectedIndex + 1) % ItemsView.Count;
}
if (offset >= threshold)
{
newIndex = (newIndex + 4) % 5;
SelectedIndex = (SelectedIndex + ItemsView.Count - 1) % ItemsView.Count;
}
ArrangeItems(newIndex, oldSelectedIndex, true);
}
base.OnPointerReleased(args);
}
private void ArrangeItems(int newIndex,
int oldIndex = -1,
bool isAnimating = false)
{
if (compositor is not null
&& container is not null
&& items is not null
&& indicatorVisual is not null)
{
double containerHeight = Bounds.Height;
double containerWidth = Bounds.Width;
container.Height = containerHeight;
double targetSize = containerHeight;
foreach (Border item in items)
{
if (item.Child is CarouselViewItem content)
{
content.Width = targetSize;
content.Height = targetSize;
}
item.Width = targetSize;
item.Height = targetSize;
}
double centreLeft = (containerWidth - targetSize) / 2;
double leftLeft = -targetSize + centreLeft;
double rightLeft = containerWidth - centreLeft;
double[] offsets =
[
leftLeft - targetSize + spacing * 1,
leftLeft + spacing * 2,
centreLeft + spacing * 3,
rightLeft + spacing * 4,
rightLeft + targetSize + spacing * 5
];
double centreOffset = spacing * (columnCount - 1) / 2 + spacing;
if (!isAnimating)
{
for (int i = 0; i < columnCount; i++)
{
itemVisuals[(newIndex + i - 2 + columnCount) % columnCount].Offset =
new Vector3((float)(offsets[i] - centreOffset), 0, 100);
}
SetItems();
}
else
{
int difference = newIndex - oldIndex;
finalOffset = difference switch
{
0 => new Vector3D(0, 0, 0),
1 => new Vector3D((float)(-targetSize - spacing), 0, 0),
-1 => new Vector3D((float)(targetSize + spacing), 0, 0),
_ => new Vector3D((float)(targetSize * Math.Sign(difference) +
spacing * Math.Sign(difference)), 0, 0)
};
indicatorAnimation = compositor.CreateVector3DKeyFrameAnimation();
indicatorAnimation.InsertKeyFrame(1.0f, finalOffset);
indicatorAnimation.Duration = animationDuration;
indicatorAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue;
SetItems();
scopedBatch.Completed += () =>
{
this.isAnimating = false;
for (int i = 0; i < columnCount; i++)
{
itemVisuals[(newIndex + i - 2 + columnCount) % columnCount].Offset =
new Vector3((float)(offsets[i] - centreOffset), 0, 0);
}
};
indicatorVisual.StartAnimation("Offset", indicatorAnimation);
scopedBatch.Start(animationDuration);
this.isAnimating = true;
}
}
}
private void OnCollectionChanged(object? sender,
NotifyCollectionChangedEventArgs args) => ArrangeItems(newIndex);
private void PrepareAnimations()
{
animations.Clear();
if (compositor is not null && indicatorVisual is not null && itemVisuals is not null)
{
for (int i = 0; i < itemVisuals.Count; i++)
{
ExpressionAnimation animation = compositor.CreateExpressionAnimation();
animation.Expression = $"Source.Offset + Vector3({itemVisuals[i].Offset.X}, 0, 0)";
animation.SetReferenceParameter("Source", indicatorVisual);
animations.Add(animation);
}
}
}
private void SetItems()
{
if (items is not null)
{
int itemCount = ItemsView.Count;
if (itemCount == 0)
{
return;
}
int[] selectedIndexOffsets = new int[columnCount];
int[] indexOffsets = new int[columnCount];
SelectedIndex = SelectedIndex < 0 ? 0 : SelectedIndex;
for (int i = -2; i <= 2; i++)
{
selectedIndexOffsets[i + 2] = (SelectedIndex + i + itemCount) % itemCount;
indexOffsets[i + 2] = (newIndex + i + columnCount) % columnCount;
}
for (int i = 0; i < columnCount; i++)
{
int index = selectedIndexOffsets[i];
if (itemCount == 1)
{
index = 0;
}
if (items[indexOffsets[i]] is Border border && border.Child is
CarouselViewItem content)
{
content.Content = ItemsView[index];
content.SetSelected(indexOffsets.Length / 2 == i);
}
}
}
}
}
@@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Toolkit.UI.Controls.Avalonia;
public class CarouselViewItem :
ContentControl
{
internal void SetSelected(bool selected)
{
PseudoClasses.Set(":selected", selected);
}
}