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