first commit
This commit is contained in:
133
Nitrox.Launcher/Models/Behaviors/SmoothScrollBehavior.cs
Normal file
133
Nitrox.Launcher/Models/Behaviors/SmoothScrollBehavior.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace Nitrox.Launcher.Models.Behaviors;
|
||||
|
||||
public abstract class SmoothScrollBehavior
|
||||
{
|
||||
private static CancellationTokenSource animationTokenSource;
|
||||
private static readonly Easing smoothScrollEasing = new ExponentialEaseOut();
|
||||
private static readonly Animation animation = new()
|
||||
{
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
Easing = smoothScrollEasing,
|
||||
Children =
|
||||
{
|
||||
new KeyFrame
|
||||
{
|
||||
Cue = new Cue(0),
|
||||
Setters = { new Setter(ScrollViewer.OffsetProperty, 0) }
|
||||
},
|
||||
new KeyFrame
|
||||
{
|
||||
Cue = new Cue(1),
|
||||
Setters = { new Setter(ScrollViewer.OffsetProperty, 0) }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly AttachedProperty<bool> SmoothScrollProperty =
|
||||
AvaloniaProperty.RegisterAttached<SmoothScrollBehavior, ScrollViewer, bool>("SmoothScroll");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target offset which was last used as smooth scrolling target.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// lastOffsetProperty is needed here since the ScrollViewer.Offset property is already set to the target offset when
|
||||
/// the PointerWheelChanged event is raised
|
||||
/// </remarks>
|
||||
private static readonly AttachedProperty<Vector> lastOffsetProperty =
|
||||
AvaloniaProperty.RegisterAttached<SmoothScrollBehavior, ScrollViewer, Vector>("LastOffset", new Vector(0, 0));
|
||||
|
||||
static SmoothScrollBehavior()
|
||||
{
|
||||
SmoothScrollProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<bool>>(OnEnableSmoothScrollingChanged));
|
||||
ScrollViewer.OffsetProperty.Changed.Subscribe(new AnonymousObserver<AvaloniaPropertyChangedEventArgs<Vector>>(OnScrollOffsetChanged));
|
||||
}
|
||||
|
||||
private static void OnScrollOffsetChanged(AvaloniaPropertyChangedEventArgs<Vector> args)
|
||||
{
|
||||
if (args.Sender is not ScrollViewer scrollViewer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This keeps LastOffset in sync with programmatic changes to offset so there won't be any huge and ugly scroll jumps.
|
||||
if (animationTokenSource is null or { IsCancellationRequested: true })
|
||||
{
|
||||
SetLastOffset(scrollViewer, args.OldValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool GetSmoothScroll(ScrollViewer element) => element.GetValue(SmoothScrollProperty);
|
||||
|
||||
public static void SetSmoothScroll(ScrollViewer element, bool value) => element.SetValue(SmoothScrollProperty, value);
|
||||
|
||||
private static Vector GetLastOffset(ScrollViewer element) => element.GetValue(lastOffsetProperty);
|
||||
|
||||
private static void SetLastOffset(ScrollViewer element, Vector value) => element.SetValue(lastOffsetProperty, value);
|
||||
|
||||
private static void OnEnableSmoothScrollingChanged(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
if (args.Sender is not ScrollViewer scrollViewer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.NewValue.GetValueOrDefault())
|
||||
{
|
||||
scrollViewer.AddHandler(InputElement.PointerWheelChangedEvent, OnPointerWheelChanged, handledEventsToo: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
scrollViewer.RemoveHandler(InputElement.PointerWheelChangedEvent, OnPointerWheelChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnPointerWheelChanged(object sender, PointerWheelEventArgs e)
|
||||
{
|
||||
if (sender is not ScrollViewer scrollViewer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel ongoing animations
|
||||
if (animationTokenSource is { IsCancellationRequested: false })
|
||||
{
|
||||
animationTokenSource.Cancel();
|
||||
animationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
// Get new offset (already set on each ScrollViewer as attached property)
|
||||
Vector lastOffset = GetLastOffset(scrollViewer);
|
||||
Vector newOffset = scrollViewer.Offset;
|
||||
if (lastOffset != newOffset)
|
||||
{
|
||||
animationTokenSource = new CancellationTokenSource();
|
||||
SetLastOffset(scrollViewer, newOffset);
|
||||
AnimateScrollToTargetAsync(scrollViewer, lastOffset, newOffset, animationTokenSource.Token).ContinueWithHandleError();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task AnimateScrollToTargetAsync(ScrollViewer scrollViewer, Vector previousOffset, Vector targetOffset, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
((Setter)animation.Children[0].Setters[0]).Value = previousOffset;
|
||||
((Setter)animation.Children[1].Setters[0]).Value = targetOffset;
|
||||
await animation.RunAsync(scrollViewer, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user