diff --git a/NOTICE.md b/NOTICE.md
index e97fc654c9..bd26b65d70 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -303,3 +303,34 @@ https://github.com/chromium/chromium
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Flutter
+
+https://github.com/flutter/flutter
+
+//Copyright 2014 The Flutter Authors. All rights reserved.
+
+//Redistribution and use in source and binary forms, with or without modification,
+//are permitted provided that the following conditions are met:
+
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS;
+//OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+//ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
index 64fe275547..cec98ec66b 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
+++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
@@ -17,7 +17,9 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canVerticallyScroll;
private int _gestureId;
private int _scrollStartDistance = 30;
-
+ private Point _pointerPressedPoint;
+ private VelocityTracker? _velocityTracker;
+
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
@@ -91,7 +93,7 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
- _trackedRootPoint = e.GetPosition((Visual?)_target);
+ _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target);
}
}
@@ -111,6 +113,13 @@ namespace Avalonia.Input.GestureRecognizers
_scrolling = true;
if (_scrolling)
{
+ _velocityTracker = new VelocityTracker();
+
+ // 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),
+ _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance));
+
_actions!.Capture(e.Pointer, this);
}
}
@@ -118,14 +127,11 @@ namespace Avalonia.Input.GestureRecognizers
if (_scrolling)
{
var vector = _trackedRootPoint - rootPoint;
- var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ?
- TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
- TimeSpan.Zero;
-
+
+ _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
+
_lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
- if (elapsed.TotalSeconds > 0)
- _inertia = vector / elapsed.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
@@ -150,12 +156,14 @@ namespace Avalonia.Input.GestureRecognizers
}
}
-
-
+
+
public void PointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer == _tracking && _scrolling)
{
+ _inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
+
e.Handled = true;
if (_inertia == default
|| e.Timestamp == 0
@@ -183,9 +191,18 @@ namespace Avalonia.Input.GestureRecognizers
var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
-
-
- if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
+ // 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;
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
new file mode 100644
index 0000000000..ce41aa6308
--- /dev/null
+++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
@@ -0,0 +1,375 @@
+// Code in this file is derived from
+// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart
+
+using System;
+using System.Diagnostics;
+using Avalonia.Utilities;
+
+namespace Avalonia.Input.GestureRecognizers
+{
+ // Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'?
+
+ internal readonly record struct Velocity(Vector PixelsPerSecond)
+ {
+ public Velocity ClampMagnitude(double minValue, double maxValue)
+ {
+ Debug.Assert(minValue >= 0.0);
+ Debug.Assert(maxValue >= 0.0 && maxValue >= minValue);
+ double valueSquared = PixelsPerSecond.SquaredLength;
+ if (valueSquared > maxValue * maxValue)
+ {
+ double length = PixelsPerSecond.Length;
+ return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * maxValue : Vector.Zero);
+ // preventing double.NaN in Vector PixelsPerSecond is important -- if a NaN eventually gets into a
+ // ScrollGestureEventArgs it results in runtime errors.
+ }
+ if (valueSquared < minValue * minValue)
+ {
+ double length = PixelsPerSecond.Length;
+ return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * minValue : Vector.Zero);
+ }
+ return this;
+ }
+ }
+
+ /// A two dimensional velocity estimate.
+ ///
+ /// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
+ /// estimate's [confidence] measures how well the velocity tracker's position
+ /// data fit a straight line, [duration] is the time that elapsed between the
+ /// first and last position sample used to compute the velocity, and [offset]
+ /// is similarly the difference between the first and last positions.
+ ///
+ /// See also:
+ ///
+ /// * [VelocityTracker], which computes [VelocityEstimate]s.
+ /// * [Velocity], which encapsulates (just) a velocity vector and provides some
+ /// useful velocity operations.
+ internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset);
+
+ internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time);
+
+ /// Computes a pointer's velocity based on data from [PointerMoveEvent]s.
+ ///
+ /// The input data is provided by calling [addPosition]. Adding data is cheap.
+ ///
+ /// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will
+ /// compute the velocity based on the data added so far. Only call these when
+ /// you need to use the velocity, as they are comparatively expensive.
+ ///
+ /// The quality of the velocity estimation will be better if more data points
+ /// have been received.
+ internal class VelocityTracker
+ {
+ private const int AssumePointerMoveStoppedMilliseconds = 40;
+ private const int HistorySize = 20;
+ private const int HorizonMilliseconds = 100;
+ private const int MinSampleSize = 3;
+ private const double MinFlingVelocity = 50.0; // Logical pixels / second
+ private const double MaxFlingVelocity = 8000.0;
+
+ private readonly PointAtTime[] _samples = new PointAtTime[HistorySize];
+ private int _index = 0;
+
+ ///
+ /// Adds a position as the given time to the tracker.
+ ///
+ ///
+ ///
+ public void AddPosition(TimeSpan time, Vector position)
+ {
+ _index++;
+ if (_index == HistorySize)
+ {
+ _index = 0;
+ }
+ _samples[_index] = new PointAtTime(true, position, time);
+ }
+
+ /// Returns an estimate of the velocity of the object being tracked by the
+ /// tracker given the current information available to the tracker.
+ ///
+ /// Information is added using [addPosition].
+ ///
+ /// Returns null if there is no data on which to base an estimate.
+ protected virtual VelocityEstimate? GetVelocityEstimate()
+ {
+ Span x = stackalloc double[HistorySize];
+ Span y = stackalloc double[HistorySize];
+ Span w = stackalloc double[HistorySize];
+ Span time = stackalloc double[HistorySize];
+ int sampleCount = 0;
+ int index = _index;
+
+ var newestSample = _samples[index];
+ if (!newestSample.Valid)
+ {
+ return null;
+ }
+
+ var previousSample = newestSample;
+ var oldestSample = newestSample;
+
+ // Starting with the most recent PointAtTime sample, iterate backwards while
+ // the samples represent continuous motion.
+ do
+ {
+ var sample = _samples[index];
+ if (!sample.Valid)
+ {
+ break;
+ }
+
+ double age = (newestSample.Time - sample.Time).TotalMilliseconds;
+ double delta = Math.Abs((sample.Time - previousSample.Time).TotalMilliseconds);
+ previousSample = sample;
+ if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds)
+ {
+ break;
+ }
+
+ oldestSample = sample;
+ var position = sample.Point;
+ x[sampleCount] = position.X;
+ y[sampleCount] = position.Y;
+ w[sampleCount] = 1.0;
+ time[sampleCount] = -age;
+ index = (index == 0 ? HistorySize : index) - 1;
+
+ sampleCount++;
+ } while (sampleCount < HistorySize);
+
+ if (sampleCount >= MinSampleSize)
+ {
+ var xFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), x.Slice(0, sampleCount), w.Slice(0, sampleCount));
+ if (xFit != null)
+ {
+ var yFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), y.Slice(0, sampleCount), w.Slice(0, sampleCount));
+ if (yFit != null)
+ {
+ return new VelocityEstimate( // convert from pixels/ms to pixels/s
+ PixelsPerSecond: new Vector(xFit.Coefficients[1] * 1000, yFit.Coefficients[1] * 1000),
+ Confidence: xFit.Confidence * yFit.Confidence,
+ Duration: newestSample.Time - oldestSample.Time,
+ Offset: newestSample.Point - oldestSample.Point
+ );
+ }
+ }
+ }
+
+ // We're unable to make a velocity estimate but we did have at least one
+ // valid pointer position.
+ return new VelocityEstimate(
+ PixelsPerSecond: Vector.Zero,
+ Confidence: 1.0,
+ Duration: newestSample.Time - oldestSample.Time,
+ Offset: newestSample.Point - oldestSample.Point
+ );
+ }
+
+ ///
+ /// Computes the velocity of the pointer at the time of the last
+ /// provided data point.
+ ///
+ /// This can be expensive. Only call this when you need the velocity.
+ ///
+ /// Returns [Velocity.zero] if there is no data from which to compute an
+ /// estimate or if the estimated velocity is zero.///
+ ///
+ ///
+ internal Velocity GetVelocity()
+ {
+ var estimate = GetVelocityEstimate();
+ if (estimate == null || estimate.PixelsPerSecond.IsDefault)
+ {
+ return new Velocity(Vector.Zero);
+ }
+ return new Velocity(estimate.PixelsPerSecond);
+ }
+
+ internal virtual Velocity GetFlingVelocity()
+ {
+ return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity);
+ }
+ }
+
+ /// An nth degree polynomial fit to a dataset.
+ internal class PolynomialFit
+ {
+ /// Creates a polynomial fit of the given degree.
+ ///
+ /// There are n + 1 coefficients in a fit of degree n.
+ internal PolynomialFit(int degree)
+ {
+ Coefficients = new double[degree + 1];
+ }
+
+ /// The polynomial coefficients of the fit.
+ public double[] Coefficients { get; }
+
+ /// An indicator of the quality of the fit.
+ ///
+ /// Larger values indicate greater quality.
+ public double Confidence { get; set; }
+ }
+
+ internal class LeastSquaresSolver
+ {
+ private const double PrecisionErrorTolerance = 1e-10;
+
+ ///
+ /// Fits a polynomial of the given degree to the data points.
+ /// When there is not enough data to fit a curve null is returned.
+ ///
+ public static PolynomialFit? Solve(int degree, ReadOnlySpan x, ReadOnlySpan y, ReadOnlySpan w)
+ {
+ if (degree > x.Length)
+ {
+ // Not enough data to fit a curve.
+ return null;
+ }
+
+ PolynomialFit result = new PolynomialFit(degree);
+
+ // Shorthands for the purpose of notation equivalence to original C++ code.
+ int m = x.Length;
+ int n = degree + 1;
+
+ // Expand the X vector to a matrix A, pre-multiplied by the weights.
+ _Matrix a = new _Matrix(m, stackalloc double[n * m]);
+ for (int h = 0; h < m; h += 1)
+ {
+ a[0, h] = w[h];
+ for (int i = 1; i < n; i += 1)
+ {
+ a[i, h] = a[i - 1, h] * x[h];
+ }
+ }
+
+ // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
+
+ // Orthonormal basis, column-major order Vector.
+ _Matrix q = new _Matrix(m, stackalloc double[n * m]);
+ // Upper triangular matrix, row-major order.
+ _Matrix r = new _Matrix(n, stackalloc double[n * n]);
+ for (int j = 0; j < n; j += 1)
+ {
+ for (int h = 0; h < m; h += 1)
+ {
+ q[j, h] = a[j, h];
+ }
+ for (int i = 0; i < j; i += 1)
+ {
+ double dot = Multiply(q.GetRow(j), q.GetRow(i));
+ for (int h = 0; h < m; h += 1)
+ {
+ q[j, h] = q[j, h] - dot * q[i, h];
+ }
+ }
+
+ double norm = Norm(q.GetRow(j));
+ if (norm < PrecisionErrorTolerance)
+ {
+ // Vectors are linearly dependent or zero so no solution.
+ return null;
+ }
+
+ double inverseNorm = 1.0 / norm;
+ for (int h = 0; h < m; h += 1)
+ {
+ q[j, h] = q[j, h] * inverseNorm;
+ }
+ for (int i = 0; i < n; i += 1)
+ {
+ r[j, i] = i < j ? 0.0 : Multiply(q.GetRow(j), a.GetRow(i));
+ }
+ }
+
+ // Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
+ // We just work from bottom-right to top-left calculating B's coefficients.
+ // "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe.
+ Span wy = stackalloc double[m];
+ for (int h = 0; h < m; h += 1)
+ {
+ wy[h] = y[h] * w[h];
+ }
+ for (int i = n - 1; i >= 0; i -= 1)
+ {
+ result.Coefficients[i] = Multiply(q.GetRow(i), wy);
+ for (int j = n - 1; j > i; j -= 1)
+ {
+ result.Coefficients[i] -= r[i, j] * result.Coefficients[j];
+ }
+ result.Coefficients[i] /= r[i, i];
+ }
+
+ // Calculate the coefficient of determination (confidence) as:
+ // 1 - (sumSquaredError / sumSquaredTotal)
+ // ...where sumSquaredError is the residual sum of squares (variance of the
+ // error), and sumSquaredTotal is the total sum of squares (variance of the
+ // data) where each has been weighted.
+ double yMean = 0.0;
+ for (int h = 0; h < m; h += 1)
+ {
+ yMean += y[h];
+ }
+ yMean /= m;
+
+ double sumSquaredError = 0.0;
+ double sumSquaredTotal = 0.0;
+ for (int h = 0; h < m; h += 1)
+ {
+ double term = 1.0;
+ double err = y[h] - result.Coefficients[0];
+ for (int i = 1; i < n; i += 1)
+ {
+ term *= x[h];
+ err -= term * result.Coefficients[i];
+ }
+ sumSquaredError += w[h] * w[h] * err * err;
+ double v = y[h] - yMean;
+ sumSquaredTotal += w[h] * w[h] * v * v;
+ }
+
+ result.Confidence = sumSquaredTotal <= PrecisionErrorTolerance ? 1.0 :
+ 1.0 - (sumSquaredError / sumSquaredTotal);
+
+ return result;
+ }
+
+ private static double Multiply(Span v1, Span v2)
+ {
+ double result = 0.0;
+ for (int i = 0; i < v1.Length; i += 1)
+ {
+ result += v1[i] * v2[i];
+ }
+ return result;
+ }
+
+ private static double Norm(Span v)
+ {
+ return Math.Sqrt(Multiply(v, v));
+ }
+
+ private readonly ref struct _Matrix
+ {
+ private readonly int _columns;
+ private readonly Span _elements;
+
+ internal _Matrix(int cols, Span elements)
+ {
+ _columns = cols;
+ _elements = elements;
+ }
+
+ public double this[int row, int col]
+ {
+ get => _elements[row * _columns + col];
+ set => _elements[row * _columns + col] = value;
+ }
+
+ public Span GetRow(int row) => _elements.Slice(row * _columns, _columns);
+ }
+ }
+}