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';
14library;
15
16import 'dart:math' as math;
17
18import 'package:flutter/foundation.dart';
19import 'package:flutter/gestures.dart';
20import 'package:flutter/painting.dart' show AxisDirection;
21import 'package:flutter/physics.dart';
22
23import 'binding.dart' show WidgetsBinding;
24import 'framework.dart';
25import 'overscroll_indicator.dart';
26import 'scroll_metrics.dart';
27import 'scroll_simulation.dart';
28import 'view.dart';
29
30export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance;
31
32/// The rate at which scroll momentum will be decelerated.
33enum 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
87class 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.
558class 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.
662class 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.
828class 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.
930class 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.
953class 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