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