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 | import 'dart:math' as math; |
6 | |
7 | import 'package:flutter/foundation.dart'; |
8 | import '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. |
15 | class 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. |
156 | class 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 | |