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