Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 73 additions & 53 deletions src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;

Expand All @@ -20,19 +22,23 @@ public class ScrollGestureRecognizer : GestureRecognizer
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>
Expand All @@ -47,7 +53,7 @@ public class ScrollGestureRecognizer : GestureRecognizer
/// </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.
Expand Down Expand Up @@ -102,6 +108,7 @@ protected override void PointerPressed(PointerPressedEventArgs e)
{
EndGesture();
_tracking = e.Pointer;
_inertia = null;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = _pointerPressedPoint = point.Position;
_velocityTracker = new VelocityTracker();
Expand All @@ -121,7 +128,7 @@ protected override void PointerMoved(PointerEventArgs e)
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),
Expand All @@ -147,23 +154,25 @@ protected override void PointerMoved(PointerEventArgs e)

protected override void PointerCaptureLost(IPointer pointer)
{
if (pointer == _tracking) EndGesture();
if (pointer == _tracking)
EndGesture();
}

void EndGesture()
{
_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)
{
Expand All @@ -172,7 +181,8 @@ protected override void PointerReleased(PointerReleasedEventArgs e)
_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
Expand All @@ -181,53 +191,63 @@ protected override void PointerReleased(PointerReleasedEventArgs e)
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