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 'dart:ui'; |
6 | /// @docImport 'package:flutter/material.dart'; |
7 | /// |
8 | /// @docImport 'scroll_activity.dart'; |
9 | /// @docImport 'scroll_configuration.dart'; |
10 | /// @docImport 'scroll_position.dart'; |
11 | /// @docImport 'scroll_position_with_single_context.dart'; |
12 | /// @docImport 'scroll_view.dart'; |
13 | /// @docImport 'scrollable.dart'; |
14 | library; |
15 | |
16 | import 'dart:math' as math; |
17 | |
18 | import 'package:flutter/foundation.dart'; |
19 | import 'package:flutter/gestures.dart'; |
20 | import 'package:flutter/painting.dart' show AxisDirection; |
21 | import 'package:flutter/physics.dart'; |
22 | |
23 | import 'binding.dart' show WidgetsBinding; |
24 | import 'framework.dart'; |
25 | import 'overscroll_indicator.dart'; |
26 | import 'scroll_metrics.dart'; |
27 | import 'scroll_simulation.dart'; |
28 | import 'view.dart'; |
29 | |
30 | export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance; |
31 | |
32 | /// The rate at which scroll momentum will be decelerated. |
33 | enum ScrollDecelerationRate { |
34 | /// Standard deceleration, aligned with mobile software expectations. |
35 | normal, |
36 | /// Increased deceleration, aligned with desktop software expectations. |
37 | /// |
38 | /// Appropriate for use with input devices more precise than touch screens, |
39 | /// such as trackpads or mouse wheels. |
40 | fast |
41 | } |
42 | |
43 | // Examples can assume: |
44 | // class FooScrollPhysics extends ScrollPhysics { |
45 | // const FooScrollPhysics({ super.parent }); |
46 | // @override |
47 | // FooScrollPhysics applyTo(ScrollPhysics? ancestor) { |
48 | // return FooScrollPhysics(parent: buildParent(ancestor)); |
49 | // } |
50 | // } |
51 | // class BarScrollPhysics extends ScrollPhysics { |
52 | // const BarScrollPhysics({ super.parent }); |
53 | // } |
54 | |
55 | /// Determines the physics of a [Scrollable] widget. |
56 | /// |
57 | /// For example, determines how the [Scrollable] will behave when the user |
58 | /// reaches the maximum scroll extent or when the user stops scrolling. |
59 | /// |
60 | /// When starting a physics [Simulation], the current scroll position and |
61 | /// velocity are used as the initial conditions for the particle in the |
62 | /// simulation. The movement of the particle in the simulation is then used to |
63 | /// determine the scroll position for the widget. |
64 | /// |
65 | /// Instead of creating your own subclasses, [parent] can be used to combine |
66 | /// [ScrollPhysics] objects of different types to get the desired scroll physics. |
67 | /// For example: |
68 | /// |
69 | /// ```dart |
70 | /// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()) |
71 | /// ``` |
72 | /// |
73 | /// You can also use `applyTo`, which is useful when you already have |
74 | /// an instance of [ScrollPhysics]: |
75 | /// |
76 | /// ```dart |
77 | /// ScrollPhysics physics = const BouncingScrollPhysics(); |
78 | /// // ... |
79 | /// final ScrollPhysics mergedPhysics = physics.applyTo(const AlwaysScrollableScrollPhysics()); |
80 | /// ``` |
81 | /// |
82 | /// When implementing a subclass, you must override [applyTo] so that it returns |
83 | /// an appropriate instance of your subclass. Otherwise, classes like |
84 | /// [Scrollable] that inform a [ScrollPosition] will combine them with |
85 | /// the default [ScrollPhysics] object instead of your custom subclass. |
86 | @immutable |
87 | class ScrollPhysics { |
88 | /// Creates an object with the default scroll physics. |
89 | const ScrollPhysics({ this.parent }); |
90 | |
91 | /// If non-null, determines the default behavior for each method. |
92 | /// |
93 | /// If a subclass of [ScrollPhysics] does not override a method, that subclass |
94 | /// will inherit an implementation from this base class that defers to |
95 | /// [parent]. This mechanism lets you assemble novel combinations of |
96 | /// [ScrollPhysics] subclasses at runtime. For example: |
97 | /// |
98 | /// ```dart |
99 | /// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()) |
100 | /// ``` |
101 | /// |
102 | /// will result in a [ScrollPhysics] that has the combined behavior |
103 | /// of [BouncingScrollPhysics] and [AlwaysScrollableScrollPhysics]: |
104 | /// behaviors that are not specified in [BouncingScrollPhysics] |
105 | /// (e.g. [shouldAcceptUserOffset]) will defer to [AlwaysScrollableScrollPhysics]. |
106 | final ScrollPhysics? parent; |
107 | |
108 | /// If [parent] is null then return ancestor, otherwise recursively build a |
109 | /// ScrollPhysics that has [ancestor] as its parent. |
110 | /// |
111 | /// This method is typically used to define [applyTo] methods like: |
112 | /// |
113 | /// ```dart |
114 | /// class MyScrollPhysics extends ScrollPhysics { |
115 | /// const MyScrollPhysics({ super.parent }); |
116 | /// |
117 | /// @override |
118 | /// MyScrollPhysics applyTo(ScrollPhysics? ancestor) { |
119 | /// return MyScrollPhysics(parent: buildParent(ancestor)); |
120 | /// } |
121 | /// |
122 | /// // ... |
123 | /// } |
124 | /// ``` |
125 | @protected |
126 | ScrollPhysics? buildParent(ScrollPhysics? ancestor) => parent?.applyTo(ancestor) ?? ancestor; |
127 | |
128 | /// Combines this [ScrollPhysics] instance with the given physics. |
129 | /// |
130 | /// The returned object uses this instance's physics when it has an |
131 | /// opinion, and defers to the given `ancestor` object's physics |
132 | /// when it does not. |
133 | /// |
134 | /// If [parent] is null then this returns a [ScrollPhysics] with the |
135 | /// same [runtimeType], but where the [parent] has been replaced |
136 | /// with the [ancestor]. |
137 | /// |
138 | /// If this scroll physics object already has a parent, then this |
139 | /// method is applied recursively and ancestor will appear at the |
140 | /// end of the existing chain of parents. |
141 | /// |
142 | /// Calling this method with a null argument will copy the current |
143 | /// object. This is inefficient. |
144 | /// |
145 | /// {@tool snippet} |
146 | /// |
147 | /// In the following example, the [applyTo] method is used to combine the |
148 | /// scroll physics of two [ScrollPhysics] objects. The resulting [ScrollPhysics] |
149 | /// `x` has the same behavior as `y`. |
150 | /// |
151 | /// ```dart |
152 | /// final FooScrollPhysics x = const FooScrollPhysics().applyTo(const BarScrollPhysics()); |
153 | /// const FooScrollPhysics y = FooScrollPhysics(parent: BarScrollPhysics()); |
154 | /// ``` |
155 | /// {@end-tool} |
156 | /// |
157 | /// ## Implementing [applyTo] |
158 | /// |
159 | /// When creating a custom [ScrollPhysics] subclass, this method |
160 | /// must be implemented. If the physics class has no constructor |
161 | /// arguments, then implementing this method is merely a matter of |
162 | /// calling the constructor with a [parent] constructed using |
163 | /// [buildParent], as follows: |
164 | /// |
165 | /// ```dart |
166 | /// class MyScrollPhysics extends ScrollPhysics { |
167 | /// const MyScrollPhysics({ super.parent }); |
168 | /// |
169 | /// @override |
170 | /// MyScrollPhysics applyTo(ScrollPhysics? ancestor) { |
171 | /// return MyScrollPhysics(parent: buildParent(ancestor)); |
172 | /// } |
173 | /// |
174 | /// // ... |
175 | /// } |
176 | /// ``` |
177 | /// |
178 | /// If the physics class has constructor arguments, they must be passed to |
179 | /// the constructor here as well, so as to create a clone. |
180 | /// |
181 | /// See also: |
182 | /// |
183 | /// * [buildParent], a utility method that's often used to define [applyTo] |
184 | /// methods for [ScrollPhysics] subclasses. |
185 | ScrollPhysics applyTo(ScrollPhysics? ancestor) { |
186 | return ScrollPhysics(parent: buildParent(ancestor)); |
187 | } |
188 | |
189 | /// Used by [DragScrollActivity] and other user-driven activities to convert |
190 | /// an offset in logical pixels as provided by the [DragUpdateDetails] into a |
191 | /// delta to apply (subtract from the current position) using |
192 | /// [ScrollActivityDelegate.setPixels]. |
193 | /// |
194 | /// This is used by some [ScrollPosition] subclasses to apply friction during |
195 | /// overscroll situations. |
196 | /// |
197 | /// This method must not adjust parts of the offset that are entirely within |
198 | /// the bounds described by the given `position`. |
199 | /// |
200 | /// The given `position` is only valid during this method call. Do not keep a |
201 | /// reference to it to use later, as the values may update, may not update, or |
202 | /// may update to reflect an entirely unrelated scrollable. |
203 | double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { |
204 | return parent?.applyPhysicsToUserOffset(position, offset) ?? offset; |
205 | } |
206 | |
207 | /// Whether the scrollable should let the user adjust the scroll offset, for |
208 | /// example by dragging. If [allowUserScrolling] is false, the scrollable |
209 | /// will never allow user input to change the scroll position. |
210 | /// |
211 | /// By default, the user can manipulate the scroll offset if, and only if, |
212 | /// there is actually content outside the viewport to reveal. |
213 | /// |
214 | /// The given `position` is only valid during this method call. Do not keep a |
215 | /// reference to it to use later, as the values may update, may not update, or |
216 | /// may update to reflect an entirely unrelated scrollable. |
217 | bool shouldAcceptUserOffset(ScrollMetrics position) { |
218 | if (!allowUserScrolling) { |
219 | return false; |
220 | } |
221 | |
222 | if (parent == null) { |
223 | return position.pixels != 0.0 || position.minScrollExtent != position.maxScrollExtent; |
224 | } |
225 | return parent!.shouldAcceptUserOffset(position); |
226 | } |
227 | |
228 | /// Provides a heuristic to determine if expensive frame-bound tasks should be |
229 | /// deferred. |
230 | /// |
231 | /// The `velocity` parameter may be positive, negative, or zero. |
232 | /// |
233 | /// The `context` parameter normally refers to the [BuildContext] of the widget |
234 | /// making the call, such as an [Image] widget in a [ListView]. |
235 | /// |
236 | /// This can be used to determine whether decoding or fetching complex data |
237 | /// for the currently visible part of the viewport should be delayed |
238 | /// to avoid doing work that will not have a chance to appear before a new |
239 | /// frame is rendered. |
240 | /// |
241 | /// For example, a list of images could use this logic to delay decoding |
242 | /// images until scrolling is slow enough to actually render the decoded |
243 | /// image to the screen. |
244 | /// |
245 | /// The default implementation is a heuristic that compares the current |
246 | /// scroll velocity in local logical pixels to the longest side of the window |
247 | /// in physical pixels. Implementers can change this heuristic by overriding |
248 | /// this method and providing their custom physics to the scrollable widget. |
249 | /// For example, an application that changes the local coordinate system with |
250 | /// a large perspective transform could provide a more or less aggressive |
251 | /// heuristic depending on whether the transform was increasing or decreasing |
252 | /// the overall scale between the global screen and local scrollable |
253 | /// coordinate systems. |
254 | /// |
255 | /// The default implementation is stateless, and provides a point-in-time |
256 | /// decision about how fast the scrollable is scrolling. It would always |
257 | /// return true for a scrollable that is animating back and forth at high |
258 | /// velocity in a loop. It is assumed that callers will handle such |
259 | /// a case, or that a custom stateful implementation would be written that |
260 | /// tracks the sign of the velocity on successive calls. |
261 | /// |
262 | /// Returning true from this method indicates that the current scroll velocity |
263 | /// is great enough that expensive operations impacting the UI should be |
264 | /// deferred. |
265 | bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) { |
266 | if (parent == null) { |
267 | final double maxPhysicalPixels = View.of(context).physicalSize.longestSide; |
268 | return velocity.abs() > maxPhysicalPixels; |
269 | } |
270 | return parent!.recommendDeferredLoading(velocity, metrics, context); |
271 | } |
272 | |
273 | /// Determines the overscroll by applying the boundary conditions. |
274 | /// |
275 | /// Called by [ScrollPosition.applyBoundaryConditions], which is called by |
276 | /// [ScrollPosition.setPixels] just before the [ScrollPosition.pixels] value |
277 | /// is updated, to determine how much of the offset is to be clamped off and |
278 | /// sent to [ScrollPosition.didOverscrollBy]. |
279 | /// |
280 | /// The `value` argument is guaranteed to not equal the [ScrollMetrics.pixels] |
281 | /// of the `position` argument when this is called. |
282 | /// |
283 | /// It is possible for this method to be called when the `position` describes |
284 | /// an already-out-of-bounds position. In that case, the boundary conditions |
285 | /// should usually only prevent a further increase in the extent to which the |
286 | /// position is out of bounds, allowing a decrease to be applied successfully, |
287 | /// so that (for instance) an animation can smoothly snap an out of bounds |
288 | /// position to the bounds. See [BallisticScrollActivity]. |
289 | /// |
290 | /// This method must not clamp parts of the offset that are entirely within |
291 | /// the bounds described by the given `position`. |
292 | /// |
293 | /// The given `position` is only valid during this method call. Do not keep a |
294 | /// reference to it to use later, as the values may update, may not update, or |
295 | /// may update to reflect an entirely unrelated scrollable. |
296 | /// |
297 | /// ## Examples |
298 | /// |
299 | /// [BouncingScrollPhysics] returns zero. In other words, it allows scrolling |
300 | /// past the boundary unhindered. |
301 | /// |
302 | /// [ClampingScrollPhysics] returns the amount by which the value is beyond |
303 | /// the position or the boundary, whichever is furthest from the content. In |
304 | /// other words, it disallows scrolling past the boundary, but allows |
305 | /// scrolling back from being overscrolled, if for some reason the position |
306 | /// ends up overscrolled. |
307 | double applyBoundaryConditions(ScrollMetrics position, double value) { |
308 | return parent?.applyBoundaryConditions(position, value) ?? 0.0; |
309 | } |
310 | |
311 | /// Describes what the scroll position should be given new viewport dimensions. |
312 | /// |
313 | /// This is called by [ScrollPosition.correctForNewDimensions]. |
314 | /// |
315 | /// The arguments consist of the scroll metrics as they stood in the previous |
316 | /// frame and the scroll metrics as they now stand after the last layout, |
317 | /// including the position and minimum and maximum scroll extents; a flag |
318 | /// indicating if the current [ScrollActivity] considers that the user is |
319 | /// actively scrolling (see [ScrollActivity.isScrolling]); and the current |
320 | /// velocity of the scroll position, if it is being driven by the scroll |
321 | /// activity (this is 0.0 during a user gesture) (see |
322 | /// [ScrollActivity.velocity]). |
323 | /// |
324 | /// The scroll metrics will be identical except for the |
325 | /// [ScrollMetrics.minScrollExtent] and [ScrollMetrics.maxScrollExtent]. They |
326 | /// are referred to as the `oldPosition` and `newPosition` (even though they |
327 | /// both technically have the same "position", in the form of |
328 | /// [ScrollMetrics.pixels]) because they are generated from the |
329 | /// [ScrollPosition] before and after updating the scroll extents. |
330 | /// |
331 | /// If the returned value does not exactly match the scroll offset given by |
332 | /// the `newPosition` argument (see [ScrollMetrics.pixels]), then the |
333 | /// [ScrollPosition] will call [ScrollPosition.correctPixels] to update the |
334 | /// new scroll position to the returned value, and layout will be re-run. This |
335 | /// is expensive. The new value is subject to further manipulation by |
336 | /// [applyBoundaryConditions]. |
337 | /// |
338 | /// If the returned value _does_ match the `newPosition.pixels` scroll offset |
339 | /// exactly, then [ScrollPosition.applyNewDimensions] will be called next. In |
340 | /// that case, [applyBoundaryConditions] is not applied to the return value. |
341 | /// |
342 | /// The given [ScrollMetrics] are only valid during this method call. Do not |
343 | /// keep references to them to use later, as the values may update, may not |
344 | /// update, or may update to reflect an entirely unrelated scrollable. |
345 | /// |
346 | /// The default implementation returns the [ScrollMetrics.pixels] of the |
347 | /// `newPosition`, which indicates that the current scroll offset is |
348 | /// acceptable. |
349 | /// |
350 | /// See also: |
351 | /// |
352 | /// * [RangeMaintainingScrollPhysics], which is enabled by default, and |
353 | /// which prevents unexpected changes to the content dimensions from |
354 | /// causing the scroll position to get any further out of bounds. |
355 | double adjustPositionForNewDimensions({ |
356 | required ScrollMetrics oldPosition, |
357 | required ScrollMetrics newPosition, |
358 | required bool isScrolling, |
359 | required double velocity, |
360 | }) { |
361 | if (parent == null) { |
362 | return newPosition.pixels; |
363 | } |
364 | return parent!.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity); |
365 | } |
366 | |
367 | /// Returns a simulation for ballistic scrolling starting from the given |
368 | /// position with the given velocity. |
369 | /// |
370 | /// This is used by [ScrollPositionWithSingleContext] in the |
371 | /// [ScrollPositionWithSingleContext.goBallistic] method. If the result |
372 | /// is non-null, [ScrollPositionWithSingleContext] will begin a |
373 | /// [BallisticScrollActivity] with the returned value. Otherwise, it will |
374 | /// begin an idle activity instead. |
375 | /// |
376 | /// The given `position` is only valid during this method call. Do not keep a |
377 | /// reference to it to use later, as the values may update, may not update, or |
378 | /// may update to reflect an entirely unrelated scrollable. |
379 | /// |
380 | /// This method can potentially be called in every frame, even in the middle |
381 | /// of what the user perceives as a single ballistic scroll. For example, in |
382 | /// a [ListView] when previously off-screen items come into view and are laid |
383 | /// out, this method may be called with a new [ScrollMetrics.maxScrollExtent]. |
384 | /// The method implementation should ensure that when the same ballistic |
385 | /// scroll motion is still intended, these calls have no side effects on the |
386 | /// physics beyond continuing that motion. |
387 | /// |
388 | /// Generally this is ensured by having the [Simulation] conform to a physical |
389 | /// metaphor of a particle in ballistic flight, where the forces on the |
390 | /// particle depend only on its position, velocity, and environment, and not |
391 | /// on the current time or any internal state. This means that the |
392 | /// time-derivative of [Simulation.dx] should be possible to write |
393 | /// mathematically as a function purely of the values of [Simulation.x], |
394 | /// [Simulation.dx], and the parameters used to construct the [Simulation], |
395 | /// independent of the time. |
396 | // TODO(gnprice): Some scroll physics in the framework violate that invariant; fix them. |
397 | // An audit found three cases violating the invariant: |
398 | // https://github.com/flutter/flutter/issues/120338 |
399 | // https://github.com/flutter/flutter/issues/120340 |
400 | // https://github.com/flutter/flutter/issues/109675 |
401 | Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { |
402 | return parent?.createBallisticSimulation(position, velocity); |
403 | } |
404 | |
405 | static final SpringDescription _kDefaultSpring = SpringDescription.withDampingRatio( |
406 | mass: 0.5, |
407 | stiffness: 100.0, |
408 | ratio: 1.1, |
409 | ); |
410 | |
411 | /// The spring to use for ballistic simulations. |
412 | SpringDescription get spring => parent?.spring ?? _kDefaultSpring; |
413 | |
414 | /// Deprecated. Call [toleranceFor] instead. |
415 | @Deprecated( |
416 | 'Call toleranceFor instead. ' |
417 | 'This feature was deprecated after v3.7.0-13.0.pre.' , |
418 | ) |
419 | Tolerance get tolerance { |
420 | return toleranceFor(FixedScrollMetrics( |
421 | minScrollExtent: null, |
422 | maxScrollExtent: null, |
423 | pixels: null, |
424 | viewportDimension: null, |
425 | axisDirection: AxisDirection.down, |
426 | devicePixelRatio: WidgetsBinding.instance.window.devicePixelRatio, |
427 | )); |
428 | } |
429 | |
430 | /// The tolerance to use for ballistic simulations. |
431 | Tolerance toleranceFor(ScrollMetrics metrics) { |
432 | return parent?.toleranceFor(metrics) ?? Tolerance( |
433 | velocity: 1.0 / (0.050 * metrics.devicePixelRatio), // logical pixels per second |
434 | distance: 1.0 / metrics.devicePixelRatio, // logical pixels |
435 | ); |
436 | } |
437 | |
438 | /// The minimum distance an input pointer drag must have moved to be |
439 | /// considered a scroll fling gesture. |
440 | /// |
441 | /// This value is typically compared with the distance traveled along the |
442 | /// scrolling axis. |
443 | /// |
444 | /// See also: |
445 | /// |
446 | /// * [VelocityTracker.getVelocityEstimate], which computes the velocity |
447 | /// of a press-drag-release gesture. |
448 | double get minFlingDistance => parent?.minFlingDistance ?? kTouchSlop; |
449 | |
450 | /// The minimum velocity for an input pointer drag to be considered a |
451 | /// scroll fling. |
452 | /// |
453 | /// This value is typically compared with the magnitude of fling gesture's |
454 | /// velocity along the scrolling axis. |
455 | /// |
456 | /// See also: |
457 | /// |
458 | /// * [VelocityTracker.getVelocityEstimate], which computes the velocity |
459 | /// of a press-drag-release gesture. |
460 | double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity; |
461 | |
462 | /// Scroll fling velocity magnitudes will be clamped to this value. |
463 | double get maxFlingVelocity => parent?.maxFlingVelocity ?? kMaxFlingVelocity; |
464 | |
465 | /// Returns the velocity carried on repeated flings. |
466 | /// |
467 | /// The function is applied to the existing scroll velocity when another |
468 | /// scroll drag is applied in the same direction. |
469 | /// |
470 | /// By default, physics for platforms other than iOS doesn't carry momentum. |
471 | double carriedMomentum(double existingVelocity) { |
472 | return parent?.carriedMomentum(existingVelocity) ?? 0.0; |
473 | } |
474 | |
475 | /// The minimum amount of pixel distance drags must move by to start motion |
476 | /// the first time or after each time the drag motion stopped. |
477 | /// |
478 | /// If null, no minimum threshold is enforced. |
479 | double? get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold; |
480 | |
481 | /// Whether a viewport is allowed to change its scroll position implicitly in |
482 | /// response to a call to [RenderObject.showOnScreen]. |
483 | /// |
484 | /// [RenderObject.showOnScreen] is for example used to bring a text field |
485 | /// fully on screen after it has received focus. This property controls |
486 | /// whether the viewport associated with this object is allowed to change the |
487 | /// scroll position to fulfill such a request. |
488 | bool get allowImplicitScrolling => true; |
489 | |
490 | /// Whether a viewport is allowed to change the scroll position as the result of user input. |
491 | bool get allowUserScrolling => true; |
492 | |
493 | @override |
494 | String toString() { |
495 | if (parent == null) { |
496 | return objectRuntimeType(this, 'ScrollPhysics' ); |
497 | } |
498 | return ' ${objectRuntimeType(this, 'ScrollPhysics' )} -> $parent' ; |
499 | } |
500 | } |
501 | |
502 | /// Scroll physics that attempt to keep the scroll position in range when the |
503 | /// contents change dimensions suddenly. |
504 | /// |
505 | /// This attempts to maintain the amount of overscroll or underscroll already present, |
506 | /// if the scroll position is already out of range _and_ the extents |
507 | /// have decreased, meaning that some content was removed. The reason for this |
508 | /// condition is that when new content is added, keeping the same overscroll |
509 | /// would mean that instead of showing it to the user, all of it is |
510 | /// being skipped by jumping right to the max extent. |
511 | /// |
512 | /// If the scroll activity is animating the scroll position, sudden changes to |
513 | /// the scroll dimensions are allowed to happen (so as to prevent animations |
514 | /// from jumping back and forth between in-range and out-of-range values). |
515 | /// |
516 | /// These physics should be combined with other scroll physics, e.g. |
517 | /// [BouncingScrollPhysics] or [ClampingScrollPhysics], to obtain a complete |
518 | /// description of typical scroll physics. See [applyTo]. |
519 | /// |
520 | /// ## Implementation details |
521 | /// |
522 | /// Specifically, these physics perform two adjustments. |
523 | /// |
524 | /// The first is to maintain overscroll when the position is out of range. |
525 | /// |
526 | /// The second is to enforce the boundary when the position is in range. |
527 | /// |
528 | /// If the current velocity is non-zero, neither adjustment is made. The |
529 | /// assumption is that there is an ongoing animation and therefore |
530 | /// further changing the scroll position would disrupt the experience. |
531 | /// |
532 | /// If the extents haven't changed, then the overscroll adjustment is |
533 | /// not made. The assumption is that if the position is overscrolled, |
534 | /// it is intentional, otherwise the position could not have reached |
535 | /// that position. (Consider [ClampingScrollPhysics] vs |
536 | /// [BouncingScrollPhysics] for example.) |
537 | /// |
538 | /// If the position itself changed since the last animation frame, |
539 | /// then the overscroll is not maintained. The assumption is similar |
540 | /// to the previous case: the position would not have been placed out |
541 | /// of range unless it was intentional. |
542 | /// |
543 | /// In addition, if the position changed and the boundaries were and |
544 | /// still are finite, then the boundary isn't enforced either, for |
545 | /// the same reason. However, if any of the boundaries were or are |
546 | /// now infinite, the boundary _is_ enforced, on the assumption that |
547 | /// infinite boundaries indicate a lazy-loading scroll view, which |
548 | /// cannot enforce boundaries while the full list has not loaded. |
549 | /// |
550 | /// If the range was out of range, then the boundary is not enforced |
551 | /// even if the range is not maintained. If the range is maintained, |
552 | /// then the distance between the old position and the old boundary is |
553 | /// applied to the new boundary to obtain the new position. |
554 | /// |
555 | /// If the range was in range, and the boundary is to be enforced, |
556 | /// then the new position is obtained by deferring to the other physics, |
557 | /// if any, and then clamped to the new range. |
558 | class RangeMaintainingScrollPhysics extends ScrollPhysics { |
559 | /// Creates scroll physics that maintain the scroll position in range. |
560 | const RangeMaintainingScrollPhysics({ super.parent }); |
561 | |
562 | @override |
563 | RangeMaintainingScrollPhysics applyTo(ScrollPhysics? ancestor) { |
564 | return RangeMaintainingScrollPhysics(parent: buildParent(ancestor)); |
565 | } |
566 | |
567 | @override |
568 | double adjustPositionForNewDimensions({ |
569 | required ScrollMetrics oldPosition, |
570 | required ScrollMetrics newPosition, |
571 | required bool isScrolling, |
572 | required double velocity, |
573 | }) { |
574 | bool maintainOverscroll = true; |
575 | bool enforceBoundary = true; |
576 | if (velocity != 0.0) { |
577 | // Don't try to adjust an animating position, the jumping around |
578 | // would be distracting. |
579 | maintainOverscroll = false; |
580 | enforceBoundary = false; |
581 | } |
582 | if ((oldPosition.minScrollExtent == newPosition.minScrollExtent) && |
583 | (oldPosition.maxScrollExtent == newPosition.maxScrollExtent)) { |
584 | // If the extents haven't changed then ignore overscroll. |
585 | maintainOverscroll = false; |
586 | } |
587 | if (oldPosition.pixels != newPosition.pixels) { |
588 | // If the position has been changed already, then it might have |
589 | // been adjusted to expect new overscroll, so don't try to |
590 | // maintain the relative overscroll. |
591 | maintainOverscroll = false; |
592 | if (oldPosition.minScrollExtent.isFinite && oldPosition.maxScrollExtent.isFinite && |
593 | newPosition.minScrollExtent.isFinite && newPosition.maxScrollExtent.isFinite) { |
594 | // In addition, if the position changed then we don't enforce the new |
595 | // boundary if both the new and previous boundaries are entirely finite. |
596 | // A common case where the position changes while one |
597 | // of the extents is infinite is a lazily-loaded list. (If the |
598 | // boundaries were finite, and the position changed, then we |
599 | // assume it was intentional.) |
600 | enforceBoundary = false; |
601 | } |
602 | } |
603 | if ((oldPosition.pixels < oldPosition.minScrollExtent) || |
604 | (oldPosition.pixels > oldPosition.maxScrollExtent)) { |
605 | // If the old position was out of range, then we should |
606 | // not try to keep the new position in range. |
607 | enforceBoundary = false; |
608 | } |
609 | if (maintainOverscroll) { |
610 | // Force the new position to be no more out of range than it was before, if: |
611 | // * it was overscrolled, and |
612 | // * the extents have decreased, meaning that some content was removed. The |
613 | // reason for this condition is that when new content is added, keeping |
614 | // the same overscroll would mean that instead of showing it to the user, |
615 | // all of it is being skipped by jumping right to the max extent. |
616 | if (oldPosition.pixels < oldPosition.minScrollExtent && |
617 | newPosition.minScrollExtent > oldPosition.minScrollExtent) { |
618 | final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels; |
619 | return newPosition.minScrollExtent - oldDelta; |
620 | } |
621 | if (oldPosition.pixels > oldPosition.maxScrollExtent && |
622 | newPosition.maxScrollExtent < oldPosition.maxScrollExtent) { |
623 | final double oldDelta = oldPosition.pixels - oldPosition.maxScrollExtent; |
624 | return newPosition.maxScrollExtent + oldDelta; |
625 | } |
626 | } |
627 | // If we're not forcing the overscroll, defer to other physics. |
628 | double result = super.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity); |
629 | if (enforceBoundary) { |
630 | // ...but if they put us out of range then reinforce the boundary. |
631 | result = clampDouble(result, newPosition.minScrollExtent, newPosition.maxScrollExtent); |
632 | } |
633 | return result; |
634 | } |
635 | } |
636 | |
637 | /// Scroll physics for environments that allow the scroll offset to go beyond |
638 | /// the bounds of the content, but then bounce the content back to the edge of |
639 | /// those bounds. |
640 | /// |
641 | /// This is the behavior typically seen on iOS. |
642 | /// |
643 | /// [BouncingScrollPhysics] by itself will not create an overscroll effect if |
644 | /// the contents of the scroll view do not extend beyond the size of the |
645 | /// viewport. To create the overscroll and bounce effect regardless of the |
646 | /// length of your scroll view, combine with [AlwaysScrollableScrollPhysics]. |
647 | /// |
648 | /// {@tool snippet} |
649 | /// ```dart |
650 | /// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()) |
651 | /// ``` |
652 | /// {@end-tool} |
653 | /// |
654 | /// See also: |
655 | /// |
656 | /// * [ScrollConfiguration], which uses this to provide the default |
657 | /// scroll behavior on iOS. |
658 | /// * [ClampingScrollPhysics], which is the analogous physics for Android's |
659 | /// clamping behavior. |
660 | /// * [ScrollPhysics], for more examples of combining [ScrollPhysics] objects |
661 | /// of different types to get the desired scroll physics. |
662 | class BouncingScrollPhysics extends ScrollPhysics { |
663 | /// Creates scroll physics that bounce back from the edge. |
664 | const BouncingScrollPhysics({ |
665 | this.decelerationRate = ScrollDecelerationRate.normal, |
666 | super.parent, |
667 | }); |
668 | |
669 | /// Used to determine parameters for friction simulations. |
670 | final ScrollDecelerationRate decelerationRate; |
671 | |
672 | @override |
673 | BouncingScrollPhysics applyTo(ScrollPhysics? ancestor) { |
674 | return BouncingScrollPhysics( |
675 | parent: buildParent(ancestor), |
676 | decelerationRate: decelerationRate |
677 | ); |
678 | } |
679 | |
680 | /// The multiple applied to overscroll to make it appear that scrolling past |
681 | /// the edge of the scrollable contents is harder than scrolling the list. |
682 | /// This is done by reducing the ratio of the scroll effect output vs the |
683 | /// scroll gesture input. |
684 | /// |
685 | /// This factor starts at 0.52 and progressively becomes harder to overscroll |
686 | /// as more of the area past the edge is dragged in (represented by an increasing |
687 | /// `overscrollFraction` which starts at 0 when there is no overscroll). |
688 | double frictionFactor(double overscrollFraction) { |
689 | return math.pow(1 - overscrollFraction, 2) * switch (decelerationRate) { |
690 | ScrollDecelerationRate.fast => 0.26, |
691 | ScrollDecelerationRate.normal => 0.52, |
692 | }; |
693 | } |
694 | |
695 | @override |
696 | double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { |
697 | assert(offset != 0.0); |
698 | assert(position.minScrollExtent <= position.maxScrollExtent); |
699 | |
700 | if (!position.outOfRange) { |
701 | return offset; |
702 | } |
703 | |
704 | final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0); |
705 | final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0); |
706 | final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd); |
707 | final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) |
708 | || (overscrollPastEnd > 0.0 && offset > 0.0); |
709 | |
710 | final double friction = easing |
711 | // Apply less resistance when easing the overscroll vs tensioning. |
712 | ? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension) |
713 | : frictionFactor(overscrollPast / position.viewportDimension); |
714 | final double direction = offset.sign; |
715 | |
716 | if (easing && decelerationRate == ScrollDecelerationRate.fast) { |
717 | return direction * offset.abs(); |
718 | } |
719 | return direction * _applyFriction(overscrollPast, offset.abs(), friction); |
720 | } |
721 | |
722 | static double _applyFriction(double extentOutside, double absDelta, double gamma) { |
723 | assert(absDelta > 0); |
724 | double total = 0.0; |
725 | if (extentOutside > 0) { |
726 | final double deltaToLimit = extentOutside / gamma; |
727 | if (absDelta < deltaToLimit) { |
728 | return absDelta * gamma; |
729 | } |
730 | total += extentOutside; |
731 | absDelta -= deltaToLimit; |
732 | } |
733 | return total + absDelta; |
734 | } |
735 | |
736 | @override |
737 | double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0; |
738 | |
739 | @override |
740 | Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { |
741 | final Tolerance tolerance = toleranceFor(position); |
742 | if (velocity.abs() >= tolerance.velocity || position.outOfRange) { |
743 | return BouncingScrollSimulation( |
744 | spring: spring, |
745 | position: position.pixels, |
746 | velocity: velocity, |
747 | leadingExtent: position.minScrollExtent, |
748 | trailingExtent: position.maxScrollExtent, |
749 | tolerance: tolerance, |
750 | constantDeceleration: switch (decelerationRate) { |
751 | ScrollDecelerationRate.fast => 1400, |
752 | ScrollDecelerationRate.normal => 0, |
753 | }, |
754 | ); |
755 | } |
756 | return null; |
757 | } |
758 | |
759 | // The ballistic simulation here decelerates more slowly than the one for |
760 | // ClampingScrollPhysics so we require a more deliberate input gesture |
761 | // to trigger a fling. |
762 | @override |
763 | double get minFlingVelocity => kMinFlingVelocity * 2.0; |
764 | |
765 | // Methodology: |
766 | // 1- Use https://github.com/flutter/platform_tests/tree/master/scroll_overlay to test with |
767 | // Flutter and platform scroll views superimposed. |
768 | // 3- If the scrollables stopped overlapping at any moment, adjust the desired |
769 | // output value of this function at that input speed. |
770 | // 4- Feed new input/output set into a power curve fitter. Change function |
771 | // and repeat from 2. |
772 | // 5- Repeat from 2 with medium and slow flings. |
773 | /// Momentum build-up function that mimics iOS's scroll speed increase with repeated flings. |
774 | /// |
775 | /// The velocity of the last fling is not an important factor. Existing speed |
776 | /// and (related) time since last fling are factors for the velocity transfer |
777 | /// calculations. |
778 | @override |
779 | double carriedMomentum(double existingVelocity) { |
780 | return existingVelocity.sign * |
781 | math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0); |
782 | } |
783 | |
784 | // Eyeballed from observation to counter the effect of an unintended scroll |
785 | // from the natural motion of lifting the finger after a scroll. |
786 | @override |
787 | double get dragStartDistanceMotionThreshold => 3.5; |
788 | |
789 | @override |
790 | double get maxFlingVelocity { |
791 | return switch (decelerationRate) { |
792 | ScrollDecelerationRate.fast => kMaxFlingVelocity * 8.0, |
793 | ScrollDecelerationRate.normal => super.maxFlingVelocity, |
794 | }; |
795 | } |
796 | |
797 | @override |
798 | SpringDescription get spring { |
799 | switch (decelerationRate) { |
800 | case ScrollDecelerationRate.fast: |
801 | return SpringDescription.withDampingRatio( |
802 | mass: 0.3, |
803 | stiffness: 75.0, |
804 | ratio: 1.3, |
805 | ); |
806 | case ScrollDecelerationRate.normal: |
807 | return super.spring; |
808 | } |
809 | } |
810 | } |
811 | |
812 | /// Scroll physics for environments that prevent the scroll offset from reaching |
813 | /// beyond the bounds of the content. |
814 | /// |
815 | /// This is the behavior typically seen on Android. |
816 | /// |
817 | /// See also: |
818 | /// |
819 | /// * [ScrollConfiguration], which uses this to provide the default |
820 | /// scroll behavior on Android. |
821 | /// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing |
822 | /// behavior. |
823 | /// * [GlowingOverscrollIndicator], which is used by [ScrollConfiguration] to |
824 | /// provide the glowing effect that is usually found with this clamping effect |
825 | /// on Android. When using a [MaterialApp], the [GlowingOverscrollIndicator]'s |
826 | /// glow color is specified to use the overall theme's |
827 | /// [ColorScheme.secondary] color. |
828 | class ClampingScrollPhysics extends ScrollPhysics { |
829 | /// Creates scroll physics that prevent the scroll offset from exceeding the |
830 | /// bounds of the content. |
831 | const ClampingScrollPhysics({ super.parent }); |
832 | |
833 | @override |
834 | ClampingScrollPhysics applyTo(ScrollPhysics? ancestor) { |
835 | return ClampingScrollPhysics(parent: buildParent(ancestor)); |
836 | } |
837 | |
838 | @override |
839 | double applyBoundaryConditions(ScrollMetrics position, double value) { |
840 | assert(() { |
841 | if (value == position.pixels) { |
842 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
843 | ErrorSummary(' $runtimeType.applyBoundaryConditions() was called redundantly.' ), |
844 | ErrorDescription( |
845 | 'The proposed new position, $value, is exactly equal to the current position of the ' |
846 | 'given ${position.runtimeType}, ${position.pixels}.\n' |
847 | 'The applyBoundaryConditions method should only be called when the value is ' |
848 | 'going to actually change the pixels, otherwise it is redundant.' , |
849 | ), |
850 | DiagnosticsProperty<ScrollPhysics>('The physics object in question was' , this, style: DiagnosticsTreeStyle.errorProperty), |
851 | DiagnosticsProperty<ScrollMetrics>('The position object in question was' , position, style: DiagnosticsTreeStyle.errorProperty), |
852 | ]); |
853 | } |
854 | return true; |
855 | }()); |
856 | if (value < position.pixels && position.pixels <= position.minScrollExtent) { |
857 | // Underscroll. |
858 | return value - position.pixels; |
859 | } |
860 | if (position.maxScrollExtent <= position.pixels && position.pixels < value) { |
861 | // Overscroll. |
862 | return value - position.pixels; |
863 | } |
864 | if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) { |
865 | // Hit top edge. |
866 | return value - position.minScrollExtent; |
867 | } |
868 | if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) { |
869 | // Hit bottom edge. |
870 | return value - position.maxScrollExtent; |
871 | } |
872 | return 0.0; |
873 | } |
874 | |
875 | @override |
876 | Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { |
877 | final Tolerance tolerance = toleranceFor(position); |
878 | if (position.outOfRange) { |
879 | double? end; |
880 | if (position.pixels > position.maxScrollExtent) { |
881 | end = position.maxScrollExtent; |
882 | } |
883 | if (position.pixels < position.minScrollExtent) { |
884 | end = position.minScrollExtent; |
885 | } |
886 | assert(end != null); |
887 | return ScrollSpringSimulation( |
888 | spring, |
889 | position.pixels, |
890 | end!, |
891 | math.min(0.0, velocity), |
892 | tolerance: tolerance, |
893 | ); |
894 | } |
895 | if (velocity.abs() < tolerance.velocity) { |
896 | return null; |
897 | } |
898 | if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) { |
899 | return null; |
900 | } |
901 | if (velocity < 0.0 && position.pixels <= position.minScrollExtent) { |
902 | return null; |
903 | } |
904 | return ClampingScrollSimulation( |
905 | position: position.pixels, |
906 | velocity: velocity, |
907 | tolerance: tolerance, |
908 | ); |
909 | } |
910 | } |
911 | |
912 | /// Scroll physics that always lets the user scroll. |
913 | /// |
914 | /// This overrides the default behavior which is to disable scrolling |
915 | /// when there is no content to scroll. It does not override the |
916 | /// handling of overscrolling. |
917 | /// |
918 | /// On Android, overscrolls will be clamped by default and result in an |
919 | /// overscroll glow. On iOS, overscrolls will load a spring that will return the |
920 | /// scroll view to its normal range when released. |
921 | /// |
922 | /// See also: |
923 | /// |
924 | /// * [ScrollPhysics], which can be used instead of this class when the default |
925 | /// behavior is desired instead. |
926 | /// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior |
927 | /// found on iOS. |
928 | /// * [ClampingScrollPhysics], which provides the clamping overscroll behavior |
929 | /// found on Android. |
930 | class AlwaysScrollableScrollPhysics extends ScrollPhysics { |
931 | /// Creates scroll physics that always lets the user scroll. |
932 | const AlwaysScrollableScrollPhysics({ super.parent }); |
933 | |
934 | @override |
935 | AlwaysScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { |
936 | return AlwaysScrollableScrollPhysics(parent: buildParent(ancestor)); |
937 | } |
938 | |
939 | @override |
940 | bool shouldAcceptUserOffset(ScrollMetrics position) => true; |
941 | } |
942 | |
943 | /// Scroll physics that does not allow the user to scroll. |
944 | /// |
945 | /// See also: |
946 | /// |
947 | /// * [ScrollPhysics], which can be used instead of this class when the default |
948 | /// behavior is desired instead. |
949 | /// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior |
950 | /// found on iOS. |
951 | /// * [ClampingScrollPhysics], which provides the clamping overscroll behavior |
952 | /// found on Android. |
953 | class NeverScrollableScrollPhysics extends ScrollPhysics { |
954 | /// Creates scroll physics that does not let the user scroll. |
955 | const NeverScrollableScrollPhysics({ super.parent }); |
956 | |
957 | @override |
958 | NeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { |
959 | return NeverScrollableScrollPhysics(parent: buildParent(ancestor)); |
960 | } |
961 | |
962 | @override |
963 | bool get allowUserScrolling => false; |
964 | |
965 | @override |
966 | bool get allowImplicitScrolling => false; |
967 | } |
968 | |