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);
+
+ }
}
}