From 4ec75580fb6212465d7bebaae37223c0ffcacde2 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 10 Sep 2021 18:02:02 -0500 Subject: [PATCH] Revert clamping scroll simulation changes (#89885) --- dev/tools/generate_android_spline_data.dart | 61 ----- .../lib/src/widgets/scroll_simulation.dart | 208 ++++-------------- .../widgets/list_wheel_scroll_view_test.dart | 2 +- .../test/widgets/scroll_simulation_test.dart | 17 -- .../test/widgets/scrollable_fling_test.dart | 7 +- .../widgets/scrollable_semantics_test.dart | 6 +- .../flutter/test/widgets/scrollable_test.dart | 12 +- .../slivers_appbar_floating_pinned_test.dart | 2 +- 8 files changed, 58 insertions(+), 257 deletions(-) delete mode 100644 dev/tools/generate_android_spline_data.dart diff --git a/dev/tools/generate_android_spline_data.dart b/dev/tools/generate_android_spline_data.dart deleted file mode 100644 index 12e79d58660..00000000000 --- a/dev/tools/generate_android_spline_data.dart +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -const int _nbSamples = 100; -final List _splinePosition = List.filled(_nbSamples + 1, 0.0); -final List _splineTime = List.filled(_nbSamples + 1, 0.0); -const double _startTension = 0.5; -const double _endTension = 1.0; -const double _inflexion = 0.35; - -// Generate the spline data used in ClampingScrollSimulation. -// -// This logic is a translation of the 2-dimensional logic found in -// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/Scroller.java. -// -// The output of this program should be copied over to [_splinePosition] in -// flutter/packages/flutter/lib/src/widgets/scroll_simulation.dart. -void main() { - const double p1 = _startTension * _inflexion; - const double p2 = 1.0 - _endTension * (1.0 - _inflexion); - double xMin = 0.0; - double yMin = 0.0; - for (int i = 0; i < _nbSamples; i++) { - final double alpha = i / _nbSamples; - double xMax = 1.0; - double x, tx, coef; - while (true) { - x = xMin + (xMax - xMin) / 2.0; - coef = 3.0 * x * (1.0 - x); - tx = coef * ((1.0 - x) * p1 + x * p2) + x * x * x; - if ((tx - alpha).abs() < 1e-5) { - break; - } - if (tx > alpha) { - xMax = x; - } else { - xMin = x; - } - } - _splinePosition[i] = coef * ((1.0 - x) * _startTension + x) + x * x * x; - double yMax = 1.0; - double y, dy; - while (true) { - y = yMin + (yMax - yMin) / 2.0; - coef = 3.0 * y * (1.0 - y); - dy = coef * ((1.0 - y) * _startTension + y) + y * y * y; - if ((dy - alpha).abs() < 1e-5) { - break; - } - if (dy > alpha) { - yMax = y; - } else { - yMin = y; - } - } - _splineTime[i] = coef * ((1.0 - y) * p1 + y * p2) + y * y * y; - } - _splinePosition[_nbSamples] = _splineTime[_nbSamples] = 1.0; - print(_splinePosition); -} diff --git a/packages/flutter/lib/src/widgets/scroll_simulation.dart b/packages/flutter/lib/src/widgets/scroll_simulation.dart index 70b9047c51e..3e9a1c2befd 100644 --- a/packages/flutter/lib/src/widgets/scroll_simulation.dart +++ b/packages/flutter/lib/src/widgets/scroll_simulation.dart @@ -129,8 +129,6 @@ class BouncingScrollSimulation extends Simulation { } } -const double _inflexion = 0.35; - /// An implementation of scroll physics that matches Android. /// /// See also: @@ -149,9 +147,10 @@ class ClampingScrollSimulation extends Simulation { required this.velocity, this.friction = 0.015, Tolerance tolerance = Tolerance.defaultTolerance, - }) : super(tolerance: tolerance) { - _duration = _splineFlingDuration(velocity); - _distance = _splineFlingDistance(velocity); + }) : assert(_flingVelocityPenetration(0.0) == _initialVelocityPenetration), + super(tolerance: tolerance) { + _duration = _flingDuration(velocity); + _distance = (velocity * _duration / _initialVelocityPenetration).abs(); } /// The position of the particle at the beginning of the simulation. @@ -166,7 +165,7 @@ class ClampingScrollSimulation extends Simulation { /// The more friction the particle experiences, the sooner it stops. final double friction; - late int _duration; + late double _duration; late double _distance; // See DECELERATION_RATE. @@ -174,184 +173,59 @@ class ClampingScrollSimulation extends Simulation { // See computeDeceleration(). static double _decelerationForFriction(double friction) { - return 9.80665 * - 39.37 * - friction * - 1.0 * // Flutter operates on logical pixels so the DPI should be 1.0. - 160.0; + return friction * 61774.04968; } - // See getSplineDeceleration(). - double _splineDeceleration(double velocity) { - return math.log(_inflexion * velocity.abs() / (friction * _decelerationForFriction(0.84))); + // See getSplineFlingDuration(). Returns a value in seconds. + double _flingDuration(double velocity) { + // See mPhysicalCoeff + final double scaledFriction = friction * _decelerationForFriction(0.84); + + // See getSplineDeceleration(). + final double deceleration = math.log(0.35 * velocity.abs() / scaledFriction); + + return math.exp(deceleration / (_kDecelerationRate - 1.0)); } - // See getSplineFlingDuration(). - int _splineFlingDuration(double velocity) { - final double deceleration = _splineDeceleration(velocity); - return (1000 * math.exp(deceleration / (_kDecelerationRate - 1.0))).round(); + // Based on a cubic curve fit to the Scroller.computeScrollOffset() values + // produced for an initial velocity of 4000. The value of Scroller.getDuration() + // and Scroller.getFinalY() were 686ms and 961 pixels respectively. + // + // Algebra courtesy of Wolfram Alpha. + // + // f(x) = scrollOffset, x is time in milliseconds + // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x - 3.15307 + // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x, so f(0) is 0 + // f(686ms) = 961 pixels + // Scale to f(0 <= t <= 1.0), x = t * 686 + // f(t) = 1165.03 t^3 - 3143.62 t^2 + 2945.87 t + // Scale f(t) so that 0.0 <= f(t) <= 1.0 + // f(t) = (1165.03 t^3 - 3143.62 t^2 + 2945.87 t) / 961.0 + // = 1.2 t^3 - 3.27 t^2 + 3.065 t + static const double _initialVelocityPenetration = 3.065; + static double _flingDistancePenetration(double t) { + return (1.2 * t * t * t) - (3.27 * t * t) + (_initialVelocityPenetration * t); } - // See getSplineFlingDistance(). - double _splineFlingDistance(double velocity) { - final double l = _splineDeceleration(velocity); - final double decelMinusOne = _kDecelerationRate - 1.0; - return friction * - _decelerationForFriction(0.84) * - math.exp(_kDecelerationRate / decelMinusOne * l); + // The derivative of the _flingDistancePenetration() function. + static double _flingVelocityPenetration(double t) { + return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration; } @override double x(double time) { - if (time == 0) { - return position; - } - final _NBSample sample = _NBSample(time, _duration); - return position + (sample.distanceCoef * _distance) * velocity.sign; + final double t = (time / _duration).clamp(0.0, 1.0); + return position + _distance * _flingDistancePenetration(t) * velocity.sign; } @override double dx(double time) { - if (time == 0) { - return velocity; - } - final _NBSample sample = _NBSample(time, _duration); - return sample.velocityCoef * _distance / _duration * velocity.sign * 1000.0; + final double t = (time / _duration).clamp(0.0, 1.0); + return _distance * _flingVelocityPenetration(t) * velocity.sign / _duration; } @override bool isDone(double time) { - return time * 1000.0 >= _duration; + return time >= _duration; } } - -class _NBSample { - _NBSample(double time, int duration) { - // See computeScrollOffset(). - final double t = time * 1000.0 / duration; - final int index = (_nbSamples * t).clamp(0, _nbSamples).round(); - _distanceCoef = 1.0; - _velocityCoef = 0.0; - if (index < _nbSamples) { - final double tInf = index / _nbSamples; - final double tSup = (index + 1) / _nbSamples; - final double dInf = _splinePosition[index]; - final double dSup = _splinePosition[index + 1]; - _velocityCoef = (dSup - dInf) / (tSup - tInf); - _distanceCoef = dInf + (t - tInf) * _velocityCoef; - } - } - - late double _velocityCoef; - double get velocityCoef => _velocityCoef; - - late double _distanceCoef; - double get distanceCoef => _distanceCoef; - - static const int _nbSamples = 100; - - // Generated from dev/tools/generate_android_spline_data.dart. - static final List _splinePosition = [ - 0.000022888183591973643, - 0.028561000304762274, - 0.05705195792956655, - 0.08538917797618413, - 0.11349556286812107, - 0.14129881694635613, - 0.16877157254923383, - 0.19581093511175632, - 0.22239649722992452, - 0.24843841866631658, - 0.2740024733220569, - 0.298967680744136, - 0.32333234658228116, - 0.34709556909569184, - 0.3702249257894571, - 0.39272483400399893, - 0.41456988647721615, - 0.43582889025419114, - 0.4564192786416, - 0.476410299013587, - 0.4957560715637827, - 0.5145493169954743, - 0.5327205670880077, - 0.5502846891191615, - 0.5673274324802855, - 0.583810881323224, - 0.5997478744397482, - 0.615194045299478, - 0.6301165005270208, - 0.6445484042257972, - 0.6585198219185201, - 0.6720397744233084, - 0.6850997688076114, - 0.6977281404741683, - 0.7099506591298411, - 0.7217749311525871, - 0.7331784038850426, - 0.7442308394229518, - 0.7549087205105974, - 0.7652471277371271, - 0.7752251637549381, - 0.7848768260203478, - 0.7942056937103814, - 0.8032299679689082, - 0.8119428702388629, - 0.8203713516576219, - 0.8285187880808974, - 0.8363794492831295, - 0.8439768562813565, - 0.851322799855549, - 0.8584111051351724, - 0.8652534074722162, - 0.8718525580962131, - 0.8782333271742155, - 0.8843892099362031, - 0.8903155590440985, - 0.8960465359221951, - 0.9015574505919048, - 0.9068736766459904, - 0.9119951682409297, - 0.9169321898723632, - 0.9216747065581234, - 0.9262420604674766, - 0.9306331858366086, - 0.9348476990715433, - 0.9389007110754832, - 0.9427903495057521, - 0.9465220679845756, - 0.9500943036519721, - 0.9535176728088761, - 0.9567898524767604, - 0.959924306623116, - 0.9629127700159108, - 0.9657622101750765, - 0.9684818726275105, - 0.9710676079044347, - 0.9735231939498, - 0.9758514437576309, - 0.9780599066560445, - 0.9801485715370128, - 0.9821149805689633, - 0.9839677526782791, - 0.9857085499421516, - 0.9873347811966005, - 0.9888547171706613, - 0.9902689443512227, - 0.9915771042095881, - 0.9927840651641069, - 0.9938913963715834, - 0.9948987305580712, - 0.9958114963810524, - 0.9966274782266875, - 0.997352148697352, - 0.9979848677523623, - 0.9985285021374979, - 0.9989844084453229, - 0.9993537595844986, - 0.999638729860106, - 0.9998403888004533, - 0.9999602810470701, - 1.0, - ]; -} diff --git a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart index b2bf144373c..ce0f94770aa 100644 --- a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart +++ b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart @@ -1236,7 +1236,7 @@ void main() { await tester.fling( find.byType(ListWheelScrollView), const Offset(0.0, -50.0), - 100.0, + 800.0, ); // At this moment, the ballistics is started but 50px is still inside the diff --git a/packages/flutter/test/widgets/scroll_simulation_test.dart b/packages/flutter/test/widgets/scroll_simulation_test.dart index 99738be6e84..69958f91e31 100644 --- a/packages/flutter/test/widgets/scroll_simulation_test.dart +++ b/packages/flutter/test/widgets/scroll_simulation_test.dart @@ -23,21 +23,4 @@ void main() { checkInitialConditions(75.0, 614.2093); checkInitialConditions(5469.0, 182.114534); }); - - test('ClampingScrollSimulation velocity eventually reaches zero', () { - void checkFinalConditions(double position, double velocity) { - final ClampingScrollSimulation simulation = ClampingScrollSimulation(position: position, velocity: velocity); - expect(simulation.dx(10.0), equals(0.0)); - } - - checkFinalConditions(51.0, 2000.0); - checkFinalConditions(584.0, 2617.294734); - checkFinalConditions(345.0, 1982.785934); - checkFinalConditions(0.0, 1831.366634); - checkFinalConditions(-156.2, 1541.57665); - checkFinalConditions(4.0, 1139.250439); - checkFinalConditions(4534.0, 1073.553798); - checkFinalConditions(75.0, 614.2093); - checkFinalConditions(5469.0, 182.114534); - }); } diff --git a/packages/flutter/test/widgets/scrollable_fling_test.dart b/packages/flutter/test/widgets/scrollable_fling_test.dart index be08e95a343..1d34b11fe0b 100644 --- a/packages/flutter/test/widgets/scrollable_fling_test.dart +++ b/packages/flutter/test/widgets/scrollable_fling_test.dart @@ -44,6 +44,11 @@ void main() { expect(getCurrentOffset(), dragOffset); await tester.pump(const Duration(seconds: 5)); final double androidResult = getCurrentOffset(); + // Regression test for https://github.com/flutter/flutter/issues/83632 + // Before changing these values, ensure the fling results in a distance that + // makes sense. See issue for more context. + expect(androidResult, greaterThan(394.0)); + expect(androidResult, lessThan(395.0)); await pumpTest(tester, TargetPlatform.linux); await tester.fling(find.byType(ListView), const Offset(0.0, -dragOffset), 1000.0); @@ -147,6 +152,6 @@ void main() { expect(log, equals(['tap 21'])); await tester.tap(find.byType(Scrollable)); await tester.pump(const Duration(milliseconds: 50)); - expect(log, equals(['tap 21', 'tap 49'])); + expect(log, equals(['tap 21', 'tap 48'])); }); } diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart index f79e52f52e9..75d5c440ae4 100644 --- a/packages/flutter/test/widgets/scrollable_semantics_test.dart +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -233,7 +233,7 @@ void main() { expect(semantics, includesNodeWith( scrollExtentMin: 0.0, - scrollPosition: 394.3, + scrollPosition: 380.2, scrollExtentMax: 520.0, actions: [ SemanticsAction.scrollUp, @@ -282,7 +282,7 @@ void main() { expect(semantics, includesNodeWith( scrollExtentMin: 0.0, - scrollPosition: 394.3, + scrollPosition: 380.2, scrollExtentMax: double.infinity, actions: [ SemanticsAction.scrollUp, @@ -294,7 +294,7 @@ void main() { expect(semantics, includesNodeWith( scrollExtentMin: 0.0, - scrollPosition: 788.6, + scrollPosition: 760.4, scrollExtentMax: double.infinity, actions: [ SemanticsAction.scrollUp, diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart index 62bccf4c1e7..b0d6b4864e5 100644 --- a/packages/flutter/test/widgets/scrollable_test.dart +++ b/packages/flutter/test/widgets/scrollable_test.dart @@ -918,8 +918,8 @@ void main() { expect(find.byKey(const ValueKey('Box 0')), findsNothing); expect(find.byKey(const ValueKey('Box 52')), findsOneWidget); - expect(expensiveWidgets, 40); - expect(cheapWidgets, 21); + expect(expensiveWidgets, 38); + expect(cheapWidgets, 20); }); testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async { @@ -961,9 +961,9 @@ void main() { expect(find.byKey(const ValueKey('Box 0')), findsNothing); expect(find.byKey(const ValueKey('Cheap box 52')), findsOneWidget); - expect(expensiveWidgets, 17); - expect(cheapWidgets, 44); - expect(physics.count, 44 + 17); + expect(expensiveWidgets, 18); + expect(cheapWidgets, 40); + expect(physics.count, 40 + 18); }); testWidgets('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async { @@ -1004,7 +1004,7 @@ void main() { expect(find.byKey(const ValueKey('Cheap box 52')), findsOneWidget); expect(expensiveWidgets, 0); - expect(cheapWidgets, 61); + expect(cheapWidgets, 58); }); testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart index de366e5207c..23adceea691 100644 --- a/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart +++ b/packages/flutter/test/widgets/slivers_appbar_floating_pinned_test.dart @@ -151,7 +151,7 @@ void main() { ); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true)); - await tester.fling(find.text('Tile 2'), const Offset(0, -600), 1950); + await tester.fling(find.text('Tile 2'), const Offset(0, -600), 2000); await tester.pumpAndSettle(); expectedSemantics = TestSemantics.root(