Revert clamping scroll simulation changes (#89885)
diff --git a/dev/tools/generate_android_spline_data.dart b/dev/tools/generate_android_spline_data.dart
deleted file mode 100644
index 12e79d5..0000000
--- 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<double> _splinePosition = List<double>.filled(_nbSamples + 1, 0.0);
-final List<double> _splineTime = List<double>.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 70b9047..3e9a1c2 100644
--- a/packages/flutter/lib/src/widgets/scroll_simulation.dart
+++ b/packages/flutter/lib/src/widgets/scroll_simulation.dart
@@ -129,8 +129,6 @@
}
}
-const double _inflexion = 0.35;
-
/// An implementation of scroll physics that matches Android.
///
/// See also:
@@ -149,9 +147,10 @@
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 @@
/// 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 @@
// 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<double> _splinePosition = <double>[
- 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 b2bf144..ce0f947 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 @@
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 99738be..69958f9 100644
--- a/packages/flutter/test/widgets/scroll_simulation_test.dart
+++ b/packages/flutter/test/widgets/scroll_simulation_test.dart
@@ -23,21 +23,4 @@
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 be08e95..1d34b11 100644
--- a/packages/flutter/test/widgets/scrollable_fling_test.dart
+++ b/packages/flutter/test/widgets/scrollable_fling_test.dart
@@ -44,6 +44,11 @@
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 @@
expect(log, equals(<String>['tap 21']));
await tester.tap(find.byType(Scrollable));
await tester.pump(const Duration(milliseconds: 50));
- expect(log, equals(<String>['tap 21', 'tap 49']));
+ expect(log, equals(<String>['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 f79e52f..75d5c44 100644
--- a/packages/flutter/test/widgets/scrollable_semantics_test.dart
+++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart
@@ -233,7 +233,7 @@
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
- scrollPosition: 394.3,
+ scrollPosition: 380.2,
scrollExtentMax: 520.0,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
@@ -282,7 +282,7 @@
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
- scrollPosition: 394.3,
+ scrollPosition: 380.2,
scrollExtentMax: double.infinity,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
@@ -294,7 +294,7 @@
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
- scrollPosition: 788.6,
+ scrollPosition: 760.4,
scrollExtentMax: double.infinity,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart
index 62bccf4..b0d6b48 100644
--- a/packages/flutter/test/widgets/scrollable_test.dart
+++ b/packages/flutter/test/widgets/scrollable_test.dart
@@ -918,8 +918,8 @@
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
expect(find.byKey(const ValueKey<String>('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 @@
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
expect(find.byKey(const ValueKey<String>('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 @@
expect(find.byKey(const ValueKey<String>('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 de366e5..23adcee 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 @@
);
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(