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 |