From 46eca014a2ba5cd4528ac755172c821695a43661 Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Wed, 28 Dec 2022 15:46:54 +0100 Subject: [PATCH 01/10] ScrollGestureRecognizer: fix skipping ScrollStartDistance on start of scroll; fix end scrolling too early using InertialScrollSpeedEnd; adding VelocityTracker that uses the last 20 pointermoves for calculating scroll velocity on pointerreleased. --- .../ScrollGestureRecognizer.cs | 43 +- .../GestureRecognizers/VelocityTracker.cs | 424 ++++++++++++++++++ 2 files changed, 454 insertions(+), 13 deletions(-) create mode 100644 src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 64fe275547..e2deea0fde 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(); // TODO: Should be platform specific -- this default tracker is for Android. + + // 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..909010d43d --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -0,0 +1,424 @@ +// Code in this file is derived from +// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart + +//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. + +using System; +using System.Diagnostics; + +namespace Avalonia.Input.GestureRecognizers +{ + // TODO: add 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'? + + public 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. + public 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. + public 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 (defined in flutter\lib\src\gesture\constants.dart) + 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() + { + double[] x = new double[HistorySize]; + double[] y = new double[HistorySize]; + double[] w = new double[HistorySize]; + double[] time = new 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.AsSpan(0, sampleCount), x.AsSpan(0, sampleCount), w.AsSpan(0, sampleCount)); + if (xFit != null) + { + var yFit = LeastSquaresSolver.Solve(2, time.AsSpan(0, sampleCount), y.AsSpan(0, sampleCount), w.AsSpan(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./// + /// + /// + public Velocity GetVelocity() + { + var estimate = GetVelocityEstimate(); + if (estimate == null || estimate.PixelsPerSecond.IsDefault) + { + return new Velocity(Vector.Zero); + } + return new Velocity(estimate.PixelsPerSecond); + } + + public 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(n, m); + for (int h = 0; h < m; h += 1) + { + a.Set(0, h, w[h]); + for (int i = 1; i < n; i += 1) + { + a.Set(i, h, a.Get(i - 1, h) * x[h]); + } + } + + // Apply the Gram-Schmidt process to A to obtain its QR decomposition. + + // Orthonormal basis, column-major ordVectorer. + _Matrix q = new _Matrix(n, m); + // Upper triangular matrix, row-major order. + _Matrix r = new _Matrix(n, n); + for (int j = 0; j < n; j += 1) + { + for (int h = 0; h < m; h += 1) + { + q.Set(j, h, a.Get(j, h)); + } + for (int i = 0; i < j; i += 1) + { + double dot = q.GetRow(j) * q.GetRow(i); + for (int h = 0; h < m; h += 1) + { + q.Set(j, h, q.Get(j, h) - dot * q.Get(i, h)); + } + } + + double norm = q.GetRow(j).Norm(); + 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.Set(j, h, q.Get(j, h) * inverseNorm); + } + for (int i = 0; i < n; i += 1) + { + r.Set(j, i, i < j ? 0.0 : 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. + _Vector wy = new _Vector(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] = q.GetRow(i) * wy; + for (int j = n - 1; j > i; j -= 1) + { + result.Coefficients[i] -= r.Get(i, j) * result.Coefficients[j]; + } + result.Coefficients[i] /= r.Get(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 readonly struct _Vector + { + private readonly int _offset; + private readonly int _length; + private readonly double[] _elements; + + internal _Vector(int size) + { + _offset = 0; + _length = size; + _elements = new double[size]; + } + + internal _Vector(double[] values, int offset, int length) + { + _offset = offset; + _length = length; + _elements = values; + } + + public double this[int i] + { + get => _elements[i + _offset]; + set => _elements[i + _offset] = value; + } + + public static double operator *(_Vector a, _Vector b) + { + double result = 0.0; + for (int i = 0; i < a._length; i += 1) + { + result += a[i] * b[i]; + } + return result; + } + + public double Norm() => Math.Sqrt(this * this); + } + + private readonly struct _Matrix + { + private readonly int _columns; + private readonly double[] _elements; + + internal _Matrix(int rows, int cols) + { + _columns = cols; + _elements = new double[rows * cols]; + } + + public double Get(int row, int col) => _elements[row * _columns + col]; + public void Set(int row, int col, double value) + { + _elements[row * _columns + col] = value; + } + + public _Vector GetRow(int row) => new(_elements, row * _columns, _columns); + } + } +} From 8d5d382c2f134779d8f49e17a71064f664029e06 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 28 Dec 2022 23:52:34 -0500 Subject: [PATCH 02/10] Reuse GetVelocityEstimate arrays --- .../Input/GestureRecognizers/VelocityTracker.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index 909010d43d..526d36ba6c 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -94,6 +94,11 @@ namespace Avalonia.Input.GestureRecognizers private const double MinFlingVelocity = 50.0; // Logical pixels / second (defined in flutter\lib\src\gesture\constants.dart) private const double MaxFlingVelocity = 8000.0; + private static double[] x = new double[HistorySize]; + private static double[] y = new double[HistorySize]; + private static double[] w = new double[HistorySize]; + private static double[] time = new double[HistorySize]; + private readonly PointAtTime[] _samples = new PointAtTime[HistorySize]; private int _index = 0; @@ -120,10 +125,6 @@ namespace Avalonia.Input.GestureRecognizers /// Returns null if there is no data on which to base an estimate. protected virtual VelocityEstimate? GetVelocityEstimate() { - double[] x = new double[HistorySize]; - double[] y = new double[HistorySize]; - double[] w = new double[HistorySize]; - double[] time = new double[HistorySize]; int sampleCount = 0; int index = _index; From 4645dcaa12510fe32e35e735a228c9b95e349726 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 28 Dec 2022 23:53:54 -0500 Subject: [PATCH 03/10] Use C# indegers --- .../GestureRecognizers/VelocityTracker.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index 526d36ba6c..9e220bc85e 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -266,10 +266,10 @@ namespace Avalonia.Input.GestureRecognizers _Matrix a = new _Matrix(n, m); for (int h = 0; h < m; h += 1) { - a.Set(0, h, w[h]); + a[0, h] = w[h]; for (int i = 1; i < n; i += 1) { - a.Set(i, h, a.Get(i - 1, h) * x[h]); + a[i, h] = a[i - 1, h] * x[h]; } } @@ -283,14 +283,14 @@ namespace Avalonia.Input.GestureRecognizers { for (int h = 0; h < m; h += 1) { - q.Set(j, h, a.Get(j, h)); + q[j, h] = a[j, h]; } for (int i = 0; i < j; i += 1) { double dot = q.GetRow(j) * q.GetRow(i); for (int h = 0; h < m; h += 1) { - q.Set(j, h, q.Get(j, h) - dot * q.Get(i, h)); + q[j, h] = q[j, h] - dot * q[i, h]; } } @@ -304,7 +304,7 @@ namespace Avalonia.Input.GestureRecognizers double inverseNorm = 1.0 / norm; for (int h = 0; h < m; h += 1) { - q.Set(j, h, q.Get(j, h) * inverseNorm); + q[j, h] = q[j, h] * inverseNorm; } for (int i = 0; i < n; i += 1) { @@ -324,9 +324,9 @@ namespace Avalonia.Input.GestureRecognizers result.Coefficients[i] = q.GetRow(i) * wy; for (int j = n - 1; j > i; j -= 1) { - result.Coefficients[i] -= r.Get(i, j) * result.Coefficients[j]; + result.Coefficients[i] -= r[i, j] * result.Coefficients[j]; } - result.Coefficients[i] /= r.Get(i, i); + result.Coefficients[i] /= r[i, i]; } // Calculate the coefficient of determination (confidence) as: @@ -413,10 +413,10 @@ namespace Avalonia.Input.GestureRecognizers _elements = new double[rows * cols]; } - public double Get(int row, int col) => _elements[row * _columns + col]; - public void Set(int row, int col, double value) + public double this[int row, int col] { - _elements[row * _columns + col] = value; + get => _elements[row * _columns + col]; + set => _elements[row * _columns + col] = value; } public _Vector GetRow(int row) => new(_elements, row * _columns, _columns); From 6d6ec49dd3ff93e76625027efe2e4f3897321dd3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 29 Dec 2022 00:09:58 -0500 Subject: [PATCH 04/10] Use Span instead of _Vector --- .../GestureRecognizers/VelocityTracker.cs | 59 ++++++------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index 9e220bc85e..d290faf114 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -275,7 +275,7 @@ namespace Avalonia.Input.GestureRecognizers // Apply the Gram-Schmidt process to A to obtain its QR decomposition. - // Orthonormal basis, column-major ordVectorer. + // Orthonormal basis, column-major order Vector. _Matrix q = new _Matrix(n, m); // Upper triangular matrix, row-major order. _Matrix r = new _Matrix(n, n); @@ -287,14 +287,14 @@ namespace Avalonia.Input.GestureRecognizers } for (int i = 0; i < j; i += 1) { - double dot = q.GetRow(j) * q.GetRow(i); + 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 = q.GetRow(j).Norm(); + double norm = Norm(q.GetRow(j)); if (norm < PrecisionErrorTolerance) { // Vectors are linearly dependent or zero so no solution. @@ -308,20 +308,21 @@ namespace Avalonia.Input.GestureRecognizers } for (int i = 0; i < n; i += 1) { - r.Set(j, i, i < j ? 0.0 : q.GetRow(j) * a.GetRow(i)); + 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. - _Vector wy = new _Vector(m); + // "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] = q.GetRow(i) * wy; + 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]; @@ -363,43 +364,19 @@ namespace Avalonia.Input.GestureRecognizers return result; } - private readonly struct _Vector + private static double Multiply(Span v1, Span v2) { - private readonly int _offset; - private readonly int _length; - private readonly double[] _elements; - - internal _Vector(int size) - { - _offset = 0; - _length = size; - _elements = new double[size]; - } - - internal _Vector(double[] values, int offset, int length) - { - _offset = offset; - _length = length; - _elements = values; - } - - public double this[int i] - { - get => _elements[i + _offset]; - set => _elements[i + _offset] = value; - } - - public static double operator *(_Vector a, _Vector b) + double result = 0.0; + for (int i = 0; i < v1.Length; i += 1) { - double result = 0.0; - for (int i = 0; i < a._length; i += 1) - { - result += a[i] * b[i]; - } - return result; + result += v1[i] * v2[i]; } - - public double Norm() => Math.Sqrt(this * this); + return result; + } + + private static double Norm(Span v) + { + return Math.Sqrt(Multiply(v, v)); } private readonly struct _Matrix @@ -419,7 +396,7 @@ namespace Avalonia.Input.GestureRecognizers set => _elements[row * _columns + col] = value; } - public _Vector GetRow(int row) => new(_elements, row * _columns, _columns); + public Span GetRow(int row) => _elements.AsSpan(row * _columns, _columns); } } } From ebdf7df1624fe826211c0facb8968be19000c8c6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 29 Dec 2022 00:21:00 -0500 Subject: [PATCH 05/10] Use stackalloc instead of reused arrays, so this code can potentially work with multitouch This reverts commit 8d5d382c2f134779d8f49e17a71064f664029e06. --- .../Input/GestureRecognizers/VelocityTracker.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index d290faf114..d4705bcdca 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -30,6 +30,7 @@ using System; using System.Diagnostics; +using Avalonia.Utilities; namespace Avalonia.Input.GestureRecognizers { @@ -94,11 +95,6 @@ namespace Avalonia.Input.GestureRecognizers private const double MinFlingVelocity = 50.0; // Logical pixels / second (defined in flutter\lib\src\gesture\constants.dart) private const double MaxFlingVelocity = 8000.0; - private static double[] x = new double[HistorySize]; - private static double[] y = new double[HistorySize]; - private static double[] w = new double[HistorySize]; - private static double[] time = new double[HistorySize]; - private readonly PointAtTime[] _samples = new PointAtTime[HistorySize]; private int _index = 0; @@ -125,6 +121,10 @@ namespace Avalonia.Input.GestureRecognizers /// 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; @@ -168,10 +168,10 @@ namespace Avalonia.Input.GestureRecognizers if (sampleCount >= MinSampleSize) { - var xFit = LeastSquaresSolver.Solve(2, time.AsSpan(0, sampleCount), x.AsSpan(0, sampleCount), w.AsSpan(0, sampleCount)); + 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.AsSpan(0, sampleCount), y.AsSpan(0, sampleCount), w.AsSpan(0, sampleCount)); + 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 From 899dc69d7283d86c9d5189d5d4fa51e1d383b0e2 Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Thu, 29 Dec 2022 10:02:11 +0100 Subject: [PATCH 06/10] removed unnecessary comments --- .../ScrollGestureRecognizer.cs | 2 +- .../GestureRecognizers/VelocityTracker.cs | 31 ++----------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index e2deea0fde..cec98ec66b 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -113,7 +113,7 @@ namespace Avalonia.Input.GestureRecognizers _scrolling = true; if (_scrolling) { - _velocityTracker = new VelocityTracker(); // TODO: Should be platform specific -- this default tracker is for Android. + _velocityTracker = new VelocityTracker(); // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance _trackedRootPoint = new Point( diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index d4705bcdca..9482542b45 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -1,40 +1,13 @@ // Code in this file is derived from // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart -//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. - using System; using System.Diagnostics; using Avalonia.Utilities; namespace Avalonia.Input.GestureRecognizers { - // TODO: add 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'? + // Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'? public readonly record struct Velocity(Vector PixelsPerSecond) { @@ -92,7 +65,7 @@ namespace Avalonia.Input.GestureRecognizers private const int HistorySize = 20; private const int HorizonMilliseconds = 100; private const int MinSampleSize = 3; - private const double MinFlingVelocity = 50.0; // Logical pixels / second (defined in flutter\lib\src\gesture\constants.dart) + private const double MinFlingVelocity = 50.0; // Logical pixels / second private const double MaxFlingVelocity = 8000.0; private readonly PointAtTime[] _samples = new PointAtTime[HistorySize]; From 9332fef8af52bc5eea6555354613187612eccbea Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Thu, 29 Dec 2022 10:05:11 +0100 Subject: [PATCH 07/10] flutter added to notice.md --- NOTICE.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) 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. From 0ad6dec3a9e43e7533298f7166b6cd475c5ec781 Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Thu, 29 Dec 2022 10:17:51 +0100 Subject: [PATCH 08/10] VelocityTracker types marked as internal --- .../Input/GestureRecognizers/VelocityTracker.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index 9482542b45..f72ac61c4b 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -9,7 +9,7 @@ namespace Avalonia.Input.GestureRecognizers { // Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'? - public readonly record struct Velocity(Vector PixelsPerSecond) + internal readonly record struct Velocity(Vector PixelsPerSecond) { public Velocity ClampMagnitude(double minValue, double maxValue) { @@ -45,7 +45,7 @@ namespace Avalonia.Input.GestureRecognizers /// * [VelocityTracker], which computes [VelocityEstimate]s. /// * [Velocity], which encapsulates (just) a velocity vector and provides some /// useful velocity operations. - public record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset); + internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset); internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time); @@ -59,7 +59,7 @@ namespace Avalonia.Input.GestureRecognizers /// /// The quality of the velocity estimation will be better if more data points /// have been received. - public class VelocityTracker + internal class VelocityTracker { private const int AssumePointerMoveStoppedMilliseconds = 40; private const int HistorySize = 20; @@ -177,7 +177,7 @@ namespace Avalonia.Input.GestureRecognizers /// estimate or if the estimated velocity is zero./// /// /// - public Velocity GetVelocity() + internal Velocity GetVelocity() { var estimate = GetVelocityEstimate(); if (estimate == null || estimate.PixelsPerSecond.IsDefault) @@ -187,7 +187,7 @@ namespace Avalonia.Input.GestureRecognizers return new Velocity(estimate.PixelsPerSecond); } - public virtual Velocity GetFlingVelocity() + internal virtual Velocity GetFlingVelocity() { return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity); } From 157ac02dc661f2ddb23b615545ed825626eae9de Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Thu, 29 Dec 2022 10:58:26 +0100 Subject: [PATCH 09/10] _Matrix memory optimized using stack --- .../Input/GestureRecognizers/VelocityTracker.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index f72ac61c4b..206a73f436 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -236,7 +236,7 @@ namespace Avalonia.Input.GestureRecognizers int n = degree + 1; // Expand the X vector to a matrix A, pre-multiplied by the weights. - _Matrix a = new _Matrix(n, m); + _Matrix a = new _Matrix(m, stackalloc double[n * m]); for (int h = 0; h < m; h += 1) { a[0, h] = w[h]; @@ -249,9 +249,9 @@ namespace Avalonia.Input.GestureRecognizers // Apply the Gram-Schmidt process to A to obtain its QR decomposition. // Orthonormal basis, column-major order Vector. - _Matrix q = new _Matrix(n, m); + _Matrix q = new _Matrix(m, stackalloc double[n * m]); // Upper triangular matrix, row-major order. - _Matrix r = new _Matrix(n, n); + _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) @@ -352,15 +352,15 @@ namespace Avalonia.Input.GestureRecognizers return Math.Sqrt(Multiply(v, v)); } - private readonly struct _Matrix + private readonly ref struct _Matrix { private readonly int _columns; - private readonly double[] _elements; + private readonly Span _elements; - internal _Matrix(int rows, int cols) + internal _Matrix(int cols, Span elements) { _columns = cols; - _elements = new double[rows * cols]; + _elements = elements;// new double[rows * cols]; } public double this[int row, int col] @@ -369,7 +369,7 @@ namespace Avalonia.Input.GestureRecognizers set => _elements[row * _columns + col] = value; } - public Span GetRow(int row) => _elements.AsSpan(row * _columns, _columns); + public Span GetRow(int row) => _elements.Slice(row * _columns, _columns); } } } From c7c819295f82706d6f6ec0546ce9c83d1e095605 Mon Sep 17 00:00:00 2001 From: Martijn Sneijders Date: Thu, 29 Dec 2022 11:26:28 +0100 Subject: [PATCH 10/10] removed comment --- src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs index 206a73f436..ce41aa6308 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -360,7 +360,7 @@ namespace Avalonia.Input.GestureRecognizers internal _Matrix(int cols, Span elements) { _columns = cols; - _elements = elements;// new double[rows * cols]; + _elements = elements; } public double this[int row, int col]