feat: SectionPanel

Add scroll flugs
Add carusel scroll
Add animation
This commit is contained in:
2026-02-10 15:03:23 +03:00
parent b638d4e752
commit 40aa38d353
4 changed files with 174 additions and 51 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
namespace GSS2.Core.Extentions; namespace GSS2.Core.Extentions;
static class EnumerableExtensions public static class EnumerableExtensions
{ {
public static IEnumerable<(int i, T value)> Enumerate<T>(this IEnumerable<T> values) => values.Select((value, i) => (i, value)); public static IEnumerable<(int i, T value)> Enumerate<T>(this IEnumerable<T> values) => values.Select((value, i) => (i, value));
} }
+2
View File
@@ -17,6 +17,8 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty] private IRelayCommand _buttonUpClick; [ObservableProperty] private IRelayCommand _buttonUpClick;
[ObservableProperty] private IRelayCommand _buttonDownClick; [ObservableProperty] private IRelayCommand _buttonDownClick;
[ObservableProperty] private int _currentIndex; [ObservableProperty] private int _currentIndex;
[ObservableProperty] private bool _canScrollForward;
[ObservableProperty] private bool _canScrollBackward;
public MainViewModel(ILogger<MainViewModel> logger, CameraViewModel cameraViewModel, ResourcesViewModel resourcesViewModel, CameraService cameraService) public MainViewModel(ILogger<MainViewModel> logger, CameraViewModel cameraViewModel, ResourcesViewModel resourcesViewModel, CameraService cameraService)
{ {
+3 -3
View File
@@ -7,9 +7,9 @@
x:DataType="local_vm:MainViewModel" x:DataType="local_vm:MainViewModel"
x:Class="GSS2.Test.Views.MainView"> x:Class="GSS2.Test.Views.MainView">
<Grid ColumnDefinitions="*,100" RowDefinitions="*,*"> <Grid ColumnDefinitions="*,100" RowDefinitions="*,*">
<Button Grid.Row="0" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Command="{Binding ButtonUpClick}"/> <Button Grid.Row="0" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" IsEnabled="{Binding CanScrollBackward}" Command="{Binding ButtonUpClick}"/>
<Button Grid.Row="1" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Command="{Binding ButtonDownClick}"/> <Button Grid.Row="1" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" IsEnabled="{Binding CanScrollForward}" Command="{Binding ButtonDownClick}"/>
<core:SectionPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" CurrentIndex="{Binding CurrentIndex}"> <core:SectionPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" ClipToBounds="True" CanScrollForward="{Binding CanScrollForward}" CanScrollBackward="{Binding CanScrollBackward}" CurrentIndex="{Binding CurrentIndex}">
<ContentControl VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Content="{Binding ResourcesViewModel}" /> <ContentControl VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Content="{Binding ResourcesViewModel}" />
<ContentControl VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Content="{Binding CameraViewModel}" /> <ContentControl VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Content="{Binding CameraViewModel}" />
<Grid ColumnDefinitions="100, *"> <Grid ColumnDefinitions="100, *">
+161 -40
View File
@@ -1,96 +1,217 @@
using Avalonia; using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using GSS2.Core.Extentions;
namespace GSS2.UI.Core; namespace GSS2.UI.Core;
public enum SectionDirection
{
UpDown,
DownUp,
LeftRight,
RightLeft
}
public class SectionPanel : Panel public class SectionPanel : Panel
{ {
public static readonly StyledProperty<int> CurrentIndexProperty = AvaloniaProperty.Register<SectionPanel, int>(nameof(CurrentIndex), 0); public static readonly StyledProperty<int> CurrentIndexProperty = AvaloniaProperty.Register<SectionPanel, int>(
name: nameof(CurrentIndex),
defaultValue: 0,
coerce: (obj, value) =>
{
if (obj is not SectionPanel sectionPanel)
return value;
if (sectionPanel.Cyclic)
return value;
return Math.Clamp(value, 0, sectionPanel.Children.Count() - 1);
});
public int CurrentIndex public int CurrentIndex
{ {
get => GetValue(CurrentIndexProperty); get => GetValue(CurrentIndexProperty);
set => SetValue(CurrentIndexProperty, value); set => SetValue(CurrentIndexProperty, value);
} }
public static readonly StyledProperty<double> AnimationSpeedProperty = AvaloniaProperty.Register<SectionPanel, double>(nameof(AnimationSpeed), 300); public static readonly StyledProperty<bool> EnableAnimationProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(EnableAnimation), true);
public bool EnableAnimation
{
get => GetValue(EnableAnimationProperty);
set => SetValue(EnableAnimationProperty, value);
}
public static readonly StyledProperty<double> AnimationSpeedProperty = AvaloniaProperty.Register<SectionPanel, double>(nameof(AnimationSpeed), 500);
public double AnimationSpeed public double AnimationSpeed
{ {
get => GetValue(AnimationSpeedProperty); get => GetValue(AnimationSpeedProperty);
set => SetValue(AnimationSpeedProperty, value); set => SetValue(AnimationSpeedProperty, value);
} }
public static readonly StyledProperty<SectionDirection> DirectionProperty = AvaloniaProperty.Register<SectionPanel, SectionDirection>(nameof(Direction), SectionDirection.UpDown); public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<SectionPanel, Orientation>(nameof(Orientation), Orientation.Vertical);
public SectionDirection Direction public Orientation Orientation
{ {
get => GetValue(DirectionProperty); get => GetValue(OrientationProperty);
set => SetValue(DirectionProperty, value); set => SetValue(OrientationProperty, value);
} }
public static readonly StyledProperty<bool> CyclicProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(Cyclic), false);
public bool Cyclic
{
get => GetValue(CyclicProperty);
set => SetValue(CyclicProperty, value);
}
public static readonly StyledProperty<bool> CanScrollForwardProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(CanScrollForward), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource);
public bool CanScrollForward
{
get => GetValue(CanScrollForwardProperty);
set => SetValue(CanScrollForwardProperty, value);
}
public static readonly StyledProperty<bool> CanScrollBackwardProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(CanScrollBackward), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource);
public bool CanScrollBackward
{
get => GetValue(CanScrollBackwardProperty);
set => SetValue(CanScrollBackwardProperty, value);
}
private readonly Dictionary<Control, int> _childIndeces = new Dictionary<Control, int>();
private bool _initialized = false;
private int _previousIndex = 0;
public SectionPanel() public SectionPanel()
: base() : base()
{ {
CurrentIndexProperty.Changed.AddClassHandler<SectionPanel>((panel, _) => panel.InvalidateArrange()); CurrentIndexProperty.Changed.AddClassHandler<SectionPanel>((panel, ea) =>
{
CanScrollForward = Cyclic || CurrentIndex != Children.Count() - 1;
CanScrollBackward = Cyclic || CurrentIndex != 0;
panel.InvalidateArrange();
});
Children.CollectionChanged += (_, ea) =>
{
CanScrollForward = Cyclic || CurrentIndex != Children.Count() - 1;
CanScrollBackward = Cyclic || CurrentIndex != 0;
};
} }
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
foreach (var child in Children) foreach (var child in Children)
child.Measure(availableSize); child.Measure(availableSize);
Console.WriteLine(availableSize);
return availableSize; return availableSize;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
for (int i = 0; i < Children.Count; i++) if (!_initialized)
{ {
var child = Children[i]; foreach (var (i, child) in Children.Enumerate())
var rect = Direction switch
{ {
SectionDirection.UpDown => new Rect(0, i * finalSize.Height, finalSize.Width, finalSize.Height), var rect = Orientation switch
SectionDirection.DownUp => new Rect(0, -i * finalSize.Height, finalSize.Width, finalSize.Height), {
SectionDirection.LeftRight => new Rect(i * finalSize.Width, 0, finalSize.Width, finalSize.Height), Orientation.Vertical => new Rect(0, i * finalSize.Height, finalSize.Width, finalSize.Height),
SectionDirection.RightLeft => new Rect(-i * finalSize.Width, 0, finalSize.Width, finalSize.Height), Orientation.Horizontal => new Rect(i * finalSize.Width, 0, finalSize.Width, finalSize.Height),
_ => default _ => default
}; };
_childIndeces.Add(child, i);
child.Arrange(rect); child.Arrange(rect);
} }
_initialized = true;
ApplyOffset(finalSize); }
else if (Children.Count() > 1)
Dispatcher.UIThread.Post(async () => _ = ApplyOffset(finalSize, true), DispatcherPriority.Render);
return finalSize; return finalSize;
} }
private void ApplyOffset(Size size)
private async Task ApplyOffset(Size size, bool animate)
{ {
double offset = Direction switch int indexShift = CurrentIndex - _previousIndex;
while (indexShift != 0)
{ {
SectionDirection.UpDown => -CurrentIndex * size.Height, int indexSingleShift = indexShift / Math.Abs(indexShift);
SectionDirection.DownUp => CurrentIndex * size.Height,
SectionDirection.LeftRight => -CurrentIndex * size.Width, var firstIndex = _childIndeces.Min(ch => ch.Value);
SectionDirection.RightLeft => CurrentIndex * size.Width, var lastIndex = _childIndeces.Max(ch => ch.Value);
if (indexSingleShift == -1 &&
_previousIndex - 1 < firstIndex)
{
var newIndex = firstIndex - 1;
var (child, _) = _childIndeces.MaxBy(ch => ch.Value);
InstantTransform(child, -Children.Count(), size);
_childIndeces[child] = newIndex;
}
if (indexSingleShift == 1 &&
_previousIndex + 1 > lastIndex)
{
var newIndex = lastIndex + 1;
var (child, _) = _childIndeces.MinBy(ch => ch.Value);
InstantTransform(child, Children.Count(), size);
_childIndeces[child] = newIndex;
}
List<Task> animations = new List<Task>();
foreach (var (i, child) in Children.Enumerate())
{
double offset = Orientation switch
{
Orientation.Vertical => -indexSingleShift * size.Height,
Orientation.Horizontal => -indexSingleShift * size.Width,
_ => 0 _ => 0
}; };
var transform = RenderTransform as TranslateTransform; if (!animate || !EnableAnimation)
InstantTransform(child, offset);
else
animations.Add(AnimationTransform(child, offset, Math.Abs(indexShift)));
}
if (animations.Count() > 0)
await Task.WhenAll(animations);
indexShift -= indexSingleShift;
_previousIndex += indexSingleShift;
}
}
private void InstantTransform(Control control, double offset, Size? size = null)
{
var transform = control.RenderTransform as TranslateTransform;
if (transform is null) if (transform is null)
{ {
transform = new TranslateTransform(); transform = new TranslateTransform();
RenderTransform = transform; control.RenderTransform = transform;
} }
if (control.RenderTransformOrigin != RelativePoint.TopLeft)
control.RenderTransformOrigin = RelativePoint.TopLeft;
if (Direction is SectionDirection.LeftRight or SectionDirection.RightLeft) if (Orientation is Orientation.Vertical)
transform.X = offset; transform.Y += offset * (size is null ? 1 : size.Value.Height);
else else
transform.Y = offset; transform.X += offset * (size is null ? 1 : size.Value.Width);
}
private Task AnimationTransform(Control control, double offset, double speedRatio)
{
var transform = control.RenderTransform as TranslateTransform;
if (transform is null)
{
transform = new TranslateTransform();
control.RenderTransform = transform;
}
if (control.RenderTransformOrigin != RelativePoint.TopLeft)
control.RenderTransformOrigin = RelativePoint.TopLeft;
var animation = new Animation
{
Duration = TimeSpan.FromMilliseconds(Math.Clamp(AnimationSpeed / speedRatio, 120, 1000)),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame
{
Cue = new Cue(1),
Setters =
{
Orientation is Orientation.Vertical ?
new Setter(TranslateTransform.YProperty, transform.Y + offset) :
new Setter(TranslateTransform.XProperty, transform.X + offset)
}
}
}
};
return animation.RunAsync(control);
} }
} }