Browse Source

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
pull/19062/head
Emmanuel Hansen 1 year ago
committed by GitHub
parent
commit
13413579b5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 126
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

126
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;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v);
/// <summary>
@ -47,7 +53,7 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v);
o => o.IsScrollInertiaEnabled, (o, v) => o.IsScrollInertiaEnabled = v);
/// <summary>
/// Defines the <see cref="ScrollStartDistance"/> 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);
}
}
}

Loading…
Cancel
Save