forked from amkovkov/GranuSightSoftware2
feat: SectionPanel
Add scroll flugs Add carusel scroll Add animation
This commit is contained in:
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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, *">
|
||||||
|
|||||||
+168
-47
@@ -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;
|
||||||
}
|
}
|
||||||
|
else if (Children.Count() > 1)
|
||||||
ApplyOffset(finalSize);
|
Dispatcher.UIThread.Post(async () => _ = ApplyOffset(finalSize, true), DispatcherPriority.Render);
|
||||||
|
|
||||||
return finalSize;
|
return finalSize;
|
||||||
}
|
}
|
||||||
private void ApplyOffset(Size size)
|
|
||||||
{
|
|
||||||
double offset = Direction switch
|
|
||||||
{
|
|
||||||
SectionDirection.UpDown => -CurrentIndex * size.Height,
|
|
||||||
SectionDirection.DownUp => CurrentIndex * size.Height,
|
|
||||||
SectionDirection.LeftRight => -CurrentIndex * size.Width,
|
|
||||||
SectionDirection.RightLeft => CurrentIndex * size.Width,
|
|
||||||
_ => 0
|
|
||||||
};
|
|
||||||
|
|
||||||
var transform = RenderTransform as TranslateTransform;
|
private async Task ApplyOffset(Size size, bool animate)
|
||||||
|
{
|
||||||
|
int indexShift = CurrentIndex - _previousIndex;
|
||||||
|
|
||||||
|
while (indexShift != 0)
|
||||||
|
{
|
||||||
|
int indexSingleShift = indexShift / Math.Abs(indexShift);
|
||||||
|
|
||||||
|
var firstIndex = _childIndeces.Min(ch => ch.Value);
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user