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'; |
6 | library; |
7 | |
8 | import 'dart:math' as math; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | import '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. |
18 | class 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. |
159 | class 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 | |