1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/physics.dart';
9
10/// An implementation of scroll physics that matches iOS.
11///
12/// See also:
13///
14/// * [ClampingScrollSimulation], which implements Android scroll physics.
15class BouncingScrollSimulation extends Simulation {
16 /// Creates a simulation group for scrolling on iOS, with the given
17 /// parameters.
18 ///
19 /// The position and velocity arguments must use the same units as will be
20 /// expected from the [x] and [dx] methods respectively (typically logical
21 /// pixels and logical pixels per second respectively).
22 ///
23 /// The leading and trailing extents must use the unit of length, the same
24 /// unit as used for the position argument and as expected from the [x]
25 /// method (typically logical pixels).
26 ///
27 /// The units used with the provided [SpringDescription] must similarly be
28 /// consistent with the other arguments. A default set of constants is used
29 /// for the `spring` description if it is omitted; these defaults assume
30 /// that the unit of length is the logical pixel.
31 BouncingScrollSimulation({
32 required double position,
33 required double velocity,
34 required this.leadingExtent,
35 required this.trailingExtent,
36 required this.spring,
37 double constantDeceleration = 0,
38 super.tolerance,
39 }) : assert(leadingExtent <= trailingExtent) {
40 if (position < leadingExtent) {
41 _springSimulation = _underscrollSimulation(position, velocity);
42 _springTime = double.negativeInfinity;
43 } else if (position > trailingExtent) {
44 _springSimulation = _overscrollSimulation(position, velocity);
45 _springTime = double.negativeInfinity;
46 } else {
47 // Taken from UIScrollView.decelerationRate (.normal = 0.998)
48 // 0.998^1000 = ~0.135
49 _frictionSimulation = FrictionSimulation(0.135, position, velocity, constantDeceleration: constantDeceleration);
50 final double finalX = _frictionSimulation.finalX;
51 if (velocity > 0.0 && finalX > trailingExtent) {
52 _springTime = _frictionSimulation.timeAtX(trailingExtent);
53 _springSimulation = _overscrollSimulation(
54 trailingExtent,
55 math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
56 );
57 assert(_springTime.isFinite);
58 } else if (velocity < 0.0 && finalX < leadingExtent) {
59 _springTime = _frictionSimulation.timeAtX(leadingExtent);
60 _springSimulation = _underscrollSimulation(
61 leadingExtent,
62 math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
63 );
64 assert(_springTime.isFinite);
65 } else {
66 _springTime = double.infinity;
67 }
68 }
69 }
70
71 /// The maximum velocity that can be transferred from the inertia of a ballistic
72 /// scroll into overscroll.
73 static const double maxSpringTransferVelocity = 5000.0;
74
75 /// When [x] falls below this value the simulation switches from an internal friction
76 /// model to a spring model which causes [x] to "spring" back to [leadingExtent].
77 final double leadingExtent;
78
79 /// When [x] exceeds this value the simulation switches from an internal friction
80 /// model to a spring model which causes [x] to "spring" back to [trailingExtent].
81 final double trailingExtent;
82
83 /// The spring used to return [x] to either [leadingExtent] or [trailingExtent].
84 final SpringDescription spring;
85
86 late FrictionSimulation _frictionSimulation;
87 late Simulation _springSimulation;
88 late double _springTime;
89 double _timeOffset = 0.0;
90
91 Simulation _underscrollSimulation(double x, double dx) {
92 return ScrollSpringSimulation(spring, x, leadingExtent, dx);
93 }
94
95 Simulation _overscrollSimulation(double x, double dx) {
96 return ScrollSpringSimulation(spring, x, trailingExtent, dx);
97 }
98
99 Simulation _simulation(double time) {
100 final Simulation simulation;
101 if (time > _springTime) {
102 _timeOffset = _springTime.isFinite ? _springTime : 0.0;
103 simulation = _springSimulation;
104 } else {
105 _timeOffset = 0.0;
106 simulation = _frictionSimulation;
107 }
108 return simulation..tolerance = tolerance;
109 }
110
111 @override
112 double x(double time) => _simulation(time).x(time - _timeOffset);
113
114 @override
115 double dx(double time) => _simulation(time).dx(time - _timeOffset);
116
117 @override
118 bool isDone(double time) => _simulation(time).isDone(time - _timeOffset);
119
120 @override
121 String toString() {
122 return '${objectRuntimeType(this, 'BouncingScrollSimulation')}(leadingExtent: $leadingExtent, trailingExtent: $trailingExtent)';
123 }
124}
125
126/// An implementation of scroll physics that aligns with Android.
127///
128/// For any value of [velocity], this travels the same total distance as the
129/// Android scroll physics.
130///
131/// This scroll physics has been adjusted relative to Android's in order to make
132/// it ballistic, meaning that the deceleration at any moment is a function only
133/// of the current velocity [dx] and does not depend on how long ago the
134/// simulation was started. (This is required by Flutter's scrolling protocol,
135/// where [ScrollActivityDelegate.goBallistic] may restart a scroll activity
136/// using only its current velocity and the scroll position's own state.)
137/// Compared to this scroll physics, Android's moves faster at the very
138/// beginning, then slower, and it ends at the same place but a little later.
139///
140/// Times are measured in seconds, and positions in logical pixels.
141///
142/// See also:
143///
144/// * [BouncingScrollSimulation], which implements iOS scroll physics.
145//
146// This class is based on OverScroller.java from Android:
147// https://android.googlesource.com/platform/frameworks/base/+/android-13.0.0_r24/core/java/android/widget/OverScroller.java#738
148// and in particular class SplineOverScroller (at the end of the file), starting
149// at method "fling". (A very similar algorithm is in Scroller.java in the same
150// directory, but OverScroller is what's used by RecyclerView.)
151//
152// In the Android implementation, times are in milliseconds, positions are in
153// physical pixels, but velocity is in physical pixels per whole second.
154//
155// The "See..." comments below refer to SplineOverScroller methods and values.
156class ClampingScrollSimulation extends Simulation {
157 /// Creates a scroll physics simulation that aligns with Android scrolling.
158 ClampingScrollSimulation({
159 required this.position,
160 required this.velocity,
161 this.friction = 0.015,
162 super.tolerance,
163 }) {
164 _duration = _flingDuration();
165 _distance = _flingDistance();
166 }
167
168 /// The position of the particle at the beginning of the simulation, in
169 /// logical pixels.
170 final double position;
171
172 /// The velocity at which the particle is traveling at the beginning of the
173 /// simulation, in logical pixels per second.
174 final double velocity;
175
176 /// The amount of friction the particle experiences as it travels.
177 ///
178 /// The more friction the particle experiences, the sooner it stops and the
179 /// less far it travels.
180 ///
181 /// The default value causes the particle to travel the same total distance
182 /// as in the Android scroll physics.
183 // See mFlingFriction.
184 final double friction;
185
186 /// The total time the simulation will run, in seconds.
187 late double _duration;
188
189 /// The total, signed, distance the simulation will travel, in logical pixels.
190 late double _distance;
191
192 // See DECELERATION_RATE.
193 static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);
194
195 // See INFLEXION.
196 static const double _kInflexion = 0.35;
197
198 // See mPhysicalCoeff. This has a value of 0.84 times Earth gravity,
199 // expressed in units of logical pixels per second^2.
200 static const double _physicalCoeff =
201 9.80665 // g, in meters per second^2
202 * 39.37 // 1 meter / 1 inch
203 * 160.0 // 1 inch / 1 logical pixel
204 * 0.84; // "look and feel tuning"
205
206 // See getSplineFlingDuration().
207 double _flingDuration() {
208 // See getSplineDeceleration(). That function's value is
209 // math.log(velocity.abs() / referenceVelocity).
210 final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
211
212 // This is the value getSplineFlingDuration() would return, but in seconds.
213 final double androidDuration =
214 math.pow(velocity.abs() / referenceVelocity,
215 1 / (_kDecelerationRate - 1.0)) as double;
216
217 // We finish a bit sooner than Android, in order to travel the
218 // same total distance.
219 return _kDecelerationRate * _kInflexion * androidDuration;
220 }
221
222 // See getSplineFlingDistance(). This returns the same value but with the
223 // sign of [velocity], and in logical pixels.
224 double _flingDistance() {
225 final double distance = velocity * _duration / _kDecelerationRate;
226 assert(() {
227 // This is the more complicated calculation that getSplineFlingDistance()
228 // actually performs, which boils down to the much simpler formula above.
229 final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
230 final double logVelocity = math.log(velocity.abs() / referenceVelocity);
231 final double distanceAgain =
232 friction * _physicalCoeff
233 * math.exp(logVelocity * _kDecelerationRate / (_kDecelerationRate - 1.0));
234 return (distance.abs() - distanceAgain).abs() < tolerance.distance;
235 }());
236 return distance;
237 }
238
239 @override
240 double x(double time) {
241 final double t = clampDouble(time / _duration, 0.0, 1.0);
242 return position + _distance * (1.0 - math.pow(1.0 - t, _kDecelerationRate));
243 }
244
245 @override
246 double dx(double time) {
247 final double t = clampDouble(time / _duration, 0.0, 1.0);
248 return velocity * math.pow(1.0 - t, _kDecelerationRate - 1.0);
249 }
250
251 @override
252 bool isDone(double time) {
253 return time >= _duration;
254 }
255}
256