From 13413579b5677cd8740c41b466a1e11c2c8c3e2e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 14 Jun 2025 01:58:23 +0000 Subject: [PATCH] Switch to using animation request for scroll inertia dispatch (#18997) * switch to using animation request for scroll inertia dispatch * run inertia calcs in dispatcher operation --- .../ScrollGestureRecognizer.cs | 126 ++++++++++-------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 09da0fe2ea..a00a7be6c2 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.Security.Cryptography; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; @@ -20,19 +22,23 @@ namespace Avalonia.Input.GestureRecognizers private bool _scrolling; private Point _trackedRootPoint; private IPointer? _tracking; + private Stopwatch? _stopWatch; private int _gestureId; private Point _pointerPressedPoint; private VelocityTracker? _velocityTracker; // Movement per second - private Vector _inertia; + private Vector? _inertia; private ulong? _lastMoveTimestamp; + private TimeSpan _lastTime; + private TimeSpan _inertiaStartTime; + private int _currentInertiaGestureId; /// /// Defines the property. /// public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect(nameof(CanHorizontallyScroll), + AvaloniaProperty.RegisterDirect(nameof(CanHorizontallyScroll), o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v); /// @@ -47,7 +53,7 @@ namespace Avalonia.Input.GestureRecognizers /// public static readonly DirectProperty IsScrollInertiaEnabledProperty = AvaloniaProperty.RegisterDirect(nameof(IsScrollInertiaEnabled), - o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v); + o => o.IsScrollInertiaEnabled, (o, v) => o.IsScrollInertiaEnabled = v); /// /// Defines the property. @@ -102,6 +108,7 @@ namespace Avalonia.Input.GestureRecognizers { EndGesture(); _tracking = e.Pointer; + _inertia = null; _gestureId = ScrollGestureEventArgs.GetNextFreeId(); _trackedRootPoint = _pointerPressedPoint = point.Position; _velocityTracker = new VelocityTracker(); @@ -121,7 +128,7 @@ namespace Avalonia.Input.GestureRecognizers if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance) _scrolling = true; if (_scrolling) - { + { // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance _trackedRootPoint = new Point( _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance), @@ -147,7 +154,8 @@ namespace Avalonia.Input.GestureRecognizers protected override void PointerCaptureLost(IPointer pointer) { - if (pointer == _tracking) EndGesture(); + if (pointer == _tracking) + EndGesture(); } void EndGesture() @@ -155,15 +163,16 @@ namespace Avalonia.Input.GestureRecognizers _tracking = null; if (_scrolling) { + _stopWatch?.Stop(); + _stopWatch = null; _inertia = default; _scrolling = false; Target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); _gestureId = 0; _lastMoveTimestamp = null; } - - } + } protected override void PointerReleased(PointerReleasedEventArgs e) { @@ -172,7 +181,8 @@ namespace Avalonia.Input.GestureRecognizers _inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero; e.Handled = true; - if (_inertia == default + if (_inertia == null + || _inertia == Vector.Zero || e.Timestamp == 0 || _lastMoveTimestamp == 0 || e.Timestamp - _lastMoveTimestamp > 200 @@ -181,53 +191,63 @@ namespace Avalonia.Input.GestureRecognizers else { _tracking = null; - var savedGestureId = _gestureId; - var st = Stopwatch.StartNew(); - var lastTime = TimeSpan.Zero; - Target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia)); - DispatcherTimer.Run(() => - { - // Another gesture has started, finish the current one - if (_gestureId != savedGestureId) - { - return false; - } - - var elapsedSinceLastTick = st.Elapsed - lastTime; - lastTime = st.Elapsed; - - var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds); - var distance = speed * elapsedSinceLastTick.TotalSeconds; - var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); - Target!.RaiseEvent(scrollGestureEventArgs); - - if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture) - { - EndGesture(); - return false; - } - - // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling - if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) - { - EndGesture(); - return false; - } - else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) - { - EndGesture(); - return false; - } - else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd) - { - EndGesture(); - return false; - } - - return true; - }, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background); + _stopWatch = Stopwatch.StartNew(); + _lastTime = _stopWatch.Elapsed; + _inertiaStartTime = _lastTime; + _currentInertiaGestureId = _gestureId; + Target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia.Value)); + MediaContext.Instance.RequestAnimationFrame(OnAnimationRequested); } } } + + private void OnAnimationRequested(TimeSpan _) + { + // Calculate the current speed and dispatch the next inertia event. This is done asynchronously so we have run the events + // with Input priority + Dispatcher.UIThread.InvokeAsync(() => + { + // Another gesture has started, finish the current one + if (_gestureId != _currentInertiaGestureId || _stopWatch == null || _inertia is not Vector inertia) + { + return; + } + + var timeSpan = _stopWatch.Elapsed; + var elapsedSinceLastTick = timeSpan - _lastTime; + _lastTime = timeSpan; + + var speed = inertia * Math.Pow(InertialResistance, (_lastTime - _inertiaStartTime).TotalSeconds); + var distance = speed * elapsedSinceLastTick.TotalSeconds; + var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); + Target!.RaiseEvent(scrollGestureEventArgs); + + if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture) + { + EndGesture(); + return; + } + + // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling + if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + { + // NO-OP + } + else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + { + EndGesture(); + return; + } + else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd) + { + EndGesture(); + return; + } + + // Reschedule on the next animation frame. TopLevel.RequestAnimationFrame isn't available on the Base project, so we use the global MediaContext + MediaContext.Instance.RequestAnimationFrame(OnAnimationRequested); + }, DispatcherPriority.Input); + + } } }