Unverified Commit 3f629153 authored by Gary Roumanis's avatar Gary Roumanis Committed by GitHub

Update ClampingScrollSimulation to better match Android (#77497)

parent 9f420ffb
// 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);
}
...@@ -129,6 +129,8 @@ class BouncingScrollSimulation extends Simulation { ...@@ -129,6 +129,8 @@ class BouncingScrollSimulation extends Simulation {
} }
} }
const double _inflexion = 0.35;
/// An implementation of scroll physics that matches Android. /// An implementation of scroll physics that matches Android.
/// ///
/// See also: /// See also:
...@@ -147,10 +149,9 @@ class ClampingScrollSimulation extends Simulation { ...@@ -147,10 +149,9 @@ class ClampingScrollSimulation extends Simulation {
required this.velocity, required this.velocity,
this.friction = 0.015, this.friction = 0.015,
Tolerance tolerance = Tolerance.defaultTolerance, Tolerance tolerance = Tolerance.defaultTolerance,
}) : assert(_flingVelocityPenetration(0.0) == _initialVelocityPenetration), }) : super(tolerance: tolerance) {
super(tolerance: tolerance) { _duration = _splineFlingDuration(velocity);
_duration = _flingDuration(velocity); _distance = _splineFlingDistance(velocity);
_distance = (velocity * _duration / _initialVelocityPenetration).abs();
} }
/// The position of the particle at the beginning of the simulation. /// The position of the particle at the beginning of the simulation.
...@@ -165,7 +166,7 @@ class ClampingScrollSimulation extends Simulation { ...@@ -165,7 +166,7 @@ class ClampingScrollSimulation extends Simulation {
/// The more friction the particle experiences, the sooner it stops. /// The more friction the particle experiences, the sooner it stops.
final double friction; final double friction;
late double _duration; late int _duration;
late double _distance; late double _distance;
// See DECELERATION_RATE. // See DECELERATION_RATE.
...@@ -173,59 +174,186 @@ class ClampingScrollSimulation extends Simulation { ...@@ -173,59 +174,186 @@ class ClampingScrollSimulation extends Simulation {
// See computeDeceleration(). // See computeDeceleration().
static double _decelerationForFriction(double friction) { static double _decelerationForFriction(double friction) {
return friction * 61774.04968; return 9.80665 *
39.37 *
friction *
1.0 * // Flutter operates on logical pixels so the DPI should be 1.0.
160.0;
} }
// See getSplineFlingDuration(). Returns a value in seconds. // See getSplineDeceleration().
double _flingDuration(double velocity) { double _splineDeceleration(double velocity) {
// See mPhysicalCoeff return math.log(_inflexion *
final double scaledFriction = friction * _decelerationForFriction(0.84); velocity.abs() /
(friction * _decelerationForFriction(0.84)));
// See getSplineDeceleration().
final double deceleration = math.log(0.35 * velocity.abs() / scaledFriction);
return math.exp(deceleration / (_kDecelerationRate - 1.0));
} }
// Based on a cubic curve fit to the Scroller.computeScrollOffset() values // See getSplineFlingDuration().
// produced for an initial velocity of 4000. The value of Scroller.getDuration() int _splineFlingDuration(double velocity) {
// and Scroller.getFinalY() were 686ms and 961 pixels respectively. final double deceleration = _splineDeceleration(velocity);
// return (1000 * math.exp(deceleration / (_kDecelerationRate - 1.0))).round();
// 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);
} }
// The derivative of the _flingDistancePenetration() function. // See getSplineFlingDistance().
static double _flingVelocityPenetration(double t) { double _splineFlingDistance(double velocity) {
return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration; final double l = _splineDeceleration(velocity);
final double decelMinusOne = _kDecelerationRate - 1.0;
return friction *
_decelerationForFriction(0.84) *
math.exp(_kDecelerationRate / decelMinusOne * l);
} }
@override @override
double x(double time) { double x(double time) {
final double t = (time / _duration).clamp(0.0, 1.0); if (time == 0) {
return position + _distance * _flingDistancePenetration(t) * velocity.sign; return position;
}
final _NBSample sample = _NBSample(time, _duration);
return position + (sample.distanceCoef * _distance) * velocity.sign;
} }
@override @override
double dx(double time) { double dx(double time) {
final double t = (time / _duration).clamp(0.0, 1.0); if (time == 0) {
return _distance * _flingVelocityPenetration(t) * velocity.sign / _duration; return velocity;
}
final _NBSample sample = _NBSample(time, _duration);
return sample.velocityCoef * _distance / _duration * velocity.sign * 1000.0;
} }
@override @override
bool isDone(double time) { bool isDone(double time) {
return time >= _duration; return time * 1000.0 >= _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
];
} }
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../rendering/rendering_tester.dart'; import '../rendering/rendering_tester.dart';
...@@ -1195,7 +1195,7 @@ void main() { ...@@ -1195,7 +1195,7 @@ void main() {
await tester.fling( await tester.fling(
find.byType(ListWheelScrollView), find.byType(ListWheelScrollView),
const Offset(0.0, -50.0), const Offset(0.0, -50.0),
800.0, 100.0,
); );
// At this moment, the ballistics is started but 50px is still inside the // At this moment, the ballistics is started but 50px is still inside the
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
test('ClampingScrollSimulation has a stable initial conditions', () { test('ClampingScrollSimulation has a stable initial conditions', () {
...@@ -23,4 +23,21 @@ void main() { ...@@ -23,4 +23,21 @@ void main() {
checkInitialConditions(75.0, 614.2093); checkInitialConditions(75.0, 614.2093);
checkInitialConditions(5469.0, 182.114534); 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);
});
} }
...@@ -147,6 +147,6 @@ void main() { ...@@ -147,6 +147,6 @@ void main() {
expect(log, equals(<String>['tap 21'])); expect(log, equals(<String>['tap 21']));
await tester.tap(find.byType(Scrollable)); await tester.tap(find.byType(Scrollable));
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
expect(log, equals(<String>['tap 21', 'tap 48'])); expect(log, equals(<String>['tap 21', 'tap 49']));
}); });
} }
...@@ -4,10 +4,10 @@ ...@@ -4,10 +4,10 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
...@@ -232,7 +232,7 @@ void main() { ...@@ -232,7 +232,7 @@ void main() {
expect(semantics, includesNodeWith( expect(semantics, includesNodeWith(
scrollExtentMin: 0.0, scrollExtentMin: 0.0,
scrollPosition: 380.2, scrollPosition: 394.3,
scrollExtentMax: 520.0, scrollExtentMax: 520.0,
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.scrollUp, SemanticsAction.scrollUp,
...@@ -281,7 +281,7 @@ void main() { ...@@ -281,7 +281,7 @@ void main() {
expect(semantics, includesNodeWith( expect(semantics, includesNodeWith(
scrollExtentMin: 0.0, scrollExtentMin: 0.0,
scrollPosition: 380.2, scrollPosition: 394.3,
scrollExtentMax: double.infinity, scrollExtentMax: double.infinity,
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.scrollUp, SemanticsAction.scrollUp,
...@@ -293,7 +293,7 @@ void main() { ...@@ -293,7 +293,7 @@ void main() {
expect(semantics, includesNodeWith( expect(semantics, includesNodeWith(
scrollExtentMin: 0.0, scrollExtentMin: 0.0,
scrollPosition: 760.4, scrollPosition: 788.6,
scrollExtentMax: double.infinity, scrollExtentMax: double.infinity,
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.scrollUp, SemanticsAction.scrollUp,
......
...@@ -9,7 +9,6 @@ import 'package:flutter/material.dart'; ...@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
Future<void> pumpTest( Future<void> pumpTest(
...@@ -890,8 +889,8 @@ void main() { ...@@ -890,8 +889,8 @@ void main() {
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing); expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
expect(find.byKey(const ValueKey<String>('Box 52')), findsOneWidget); expect(find.byKey(const ValueKey<String>('Box 52')), findsOneWidget);
expect(expensiveWidgets, 38); expect(expensiveWidgets, 40);
expect(cheapWidgets, 20); expect(cheapWidgets, 21);
}); });
testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async { testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async {
...@@ -933,9 +932,9 @@ void main() { ...@@ -933,9 +932,9 @@ void main() {
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing); expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget); expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
expect(expensiveWidgets, 18); expect(expensiveWidgets, 17);
expect(cheapWidgets, 40); expect(cheapWidgets, 44);
expect(physics.count, 40 + 18); expect(physics.count, 44 + 17);
}); });
testWidgets('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async { testWidgets('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async {
...@@ -976,7 +975,7 @@ void main() { ...@@ -976,7 +975,7 @@ void main() {
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget); expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
expect(expensiveWidgets, 0); expect(expensiveWidgets, 0);
expect(cheapWidgets, 58); expect(cheapWidgets, 61);
}); });
testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async { testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async {
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
...@@ -151,7 +151,7 @@ void main() { ...@@ -151,7 +151,7 @@ void main() {
); );
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true));
await tester.fling(find.text('Tile 2'), const Offset(0, -600), 2000); await tester.fling(find.text('Tile 2'), const Offset(0, -600), 1950);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expectedSemantics = TestSemantics.root( expectedSemantics = TestSemantics.root(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment