blob: 05da9cba00c78007da0dfed1a4425b886cd6dca8 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'binding.dart' show WidgetsBinding;
import 'framework.dart';
import 'overscroll_indicator.dart';
import 'scroll_metrics.dart';
import 'scroll_simulation.dart';
export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance;
/// The rate at which scroll momentum will be decelerated.
enum ScrollDecelerationRate {
/// Standard deceleration, aligned with mobile software expectations.
normal,
/// Increased deceleration, aligned with desktop software expectations.
///
/// Appropriate for use with input devices more precise than touch screens,
/// such as trackpads or mouse wheels.
fast
}
// Examples can assume:
// class FooScrollPhysics extends ScrollPhysics {
// const FooScrollPhysics({ super.parent });
// @override
// FooScrollPhysics applyTo(ScrollPhysics? ancestor) {
// return FooScrollPhysics(parent: buildParent(ancestor));
// }
// }
// class BarScrollPhysics extends ScrollPhysics {
// const BarScrollPhysics({ super.parent });
// }
/// Determines the physics of a [Scrollable] widget.
///
/// For example, determines how the [Scrollable] will behave when the user
/// reaches the maximum scroll extent or when the user stops scrolling.
///
/// When starting a physics [Simulation], the current scroll position and
/// velocity are used as the initial conditions for the particle in the
/// simulation. The movement of the particle in the simulation is then used to
/// determine the scroll position for the widget.
///
/// Instead of creating your own subclasses, [parent] can be used to combine
/// [ScrollPhysics] objects of different types to get the desired scroll physics.
/// For example:
///
/// ```dart
/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
/// ```
///
/// You can also use `applyTo`, which is useful when you already have
/// an instance of [ScrollPhysics]:
///
/// ```dart
/// ScrollPhysics physics = const BouncingScrollPhysics();
/// // ...
/// final ScrollPhysics mergedPhysics = physics.applyTo(const AlwaysScrollableScrollPhysics());
/// ```
@immutable
class ScrollPhysics {
/// Creates an object with the default scroll physics.
const ScrollPhysics({ this.parent });
/// If non-null, determines the default behavior for each method.
///
/// If a subclass of [ScrollPhysics] does not override a method, that subclass
/// will inherit an implementation from this base class that defers to
/// [parent]. This mechanism lets you assemble novel combinations of
/// [ScrollPhysics] subclasses at runtime. For example:
///
/// ```dart
/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
/// ```
///
/// will result in a [ScrollPhysics] that has the combined behavior
/// of [BouncingScrollPhysics] and [AlwaysScrollableScrollPhysics]:
/// behaviors that are not specified in [BouncingScrollPhysics]
/// (e.g. [shouldAcceptUserOffset]) will defer to [AlwaysScrollableScrollPhysics].
final ScrollPhysics? parent;
/// If [parent] is null then return ancestor, otherwise recursively build a
/// ScrollPhysics that has [ancestor] as its parent.
///
/// This method is typically used to define [applyTo] methods like:
///
/// ```dart
/// class MyScrollPhysics extends ScrollPhysics {
/// const MyScrollPhysics({ super.parent });
///
/// @override
/// MyScrollPhysics applyTo(ScrollPhysics? ancestor) {
/// return MyScrollPhysics(parent: buildParent(ancestor));
/// }
///
/// // ...
/// }
/// ```
@protected
ScrollPhysics? buildParent(ScrollPhysics? ancestor) => parent?.applyTo(ancestor) ?? ancestor;
/// Combines this [ScrollPhysics] instance with the given physics.
///
/// The returned object uses this instance's physics when it has an
/// opinion, and defers to the given `ancestor` object's physics
/// when it does not.
///
/// If [parent] is null then this returns a [ScrollPhysics] with the
/// same [runtimeType], but where the [parent] has been replaced
/// with the [ancestor].
///
/// If this scroll physics object already has a parent, then this
/// method is applied recursively and ancestor will appear at the
/// end of the existing chain of parents.
///
/// Calling this method with a null argument will copy the current
/// object. This is inefficient.
///
/// {@tool snippet}
///
/// In the following example, the [applyTo] method is used to combine the
/// scroll physics of two [ScrollPhysics] objects. The resulting [ScrollPhysics]
/// `x` has the same behavior as `y`.
///
/// ```dart
/// final FooScrollPhysics x = const FooScrollPhysics().applyTo(const BarScrollPhysics());
/// const FooScrollPhysics y = FooScrollPhysics(parent: BarScrollPhysics());
/// ```
/// {@end-tool}
///
/// ## Implementing [applyTo]
///
/// When creating a custom [ScrollPhysics] subclass, this method
/// must be implemented. If the physics class has no constructor
/// arguments, then implementing this method is merely a matter of
/// calling the constructor with a [parent] constructed using
/// [buildParent], as follows:
///
/// ```dart
/// class MyScrollPhysics extends ScrollPhysics {
/// const MyScrollPhysics({ super.parent });
///
/// @override
/// MyScrollPhysics applyTo(ScrollPhysics? ancestor) {
/// return MyScrollPhysics(parent: buildParent(ancestor));
/// }
///
/// // ...
/// }
/// ```
///
/// If the physics class has constructor arguments, they must be passed to
/// the constructor here as well, so as to create a clone.
///
/// See also:
///
/// * [buildParent], a utility method that's often used to define [applyTo]
/// methods for [ScrollPhysics] subclasses.
ScrollPhysics applyTo(ScrollPhysics? ancestor) {
return ScrollPhysics(parent: buildParent(ancestor));
}
/// Used by [DragScrollActivity] and other user-driven activities to convert
/// an offset in logical pixels as provided by the [DragUpdateDetails] into a
/// delta to apply (subtract from the current position) using
/// [ScrollActivityDelegate.setPixels].
///
/// This is used by some [ScrollPosition] subclasses to apply friction during
/// overscroll situations.
///
/// This method must not adjust parts of the offset that are entirely within
/// the bounds described by the given `position`.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null) {
return offset;
}
return parent!.applyPhysicsToUserOffset(position, offset);
}
/// Whether the scrollable should let the user adjust the scroll offset, for
/// example by dragging.
///
/// By default, the user can manipulate the scroll offset if, and only if,
/// there is actually content outside the viewport to reveal.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
bool shouldAcceptUserOffset(ScrollMetrics position) {
if (parent == null) {
return position.pixels != 0.0 || position.minScrollExtent != position.maxScrollExtent;
}
return parent!.shouldAcceptUserOffset(position);
}
/// Provides a heuristic to determine if expensive frame-bound tasks should be
/// deferred.
///
/// The velocity parameter must not be null, but may be positive, negative, or
/// zero.
///
/// The metrics parameter must not be null.
///
/// The context parameter must not be null. It normally refers to the
/// [BuildContext] of the widget making the call, such as an [Image] widget
/// in a [ListView].
///
/// This can be used to determine whether decoding or fetching complex data
/// for the currently visible part of the viewport should be delayed
/// to avoid doing work that will not have a chance to appear before a new
/// frame is rendered.
///
/// For example, a list of images could use this logic to delay decoding
/// images until scrolling is slow enough to actually render the decoded
/// image to the screen.
///
/// The default implementation is a heuristic that compares the current
/// scroll velocity in local logical pixels to the longest side of the window
/// in physical pixels. Implementers can change this heuristic by overriding
/// this method and providing their custom physics to the scrollable widget.
/// For example, an application that changes the local coordinate system with
/// a large perspective transform could provide a more or less aggressive
/// heuristic depending on whether the transform was increasing or decreasing
/// the overall scale between the global screen and local scrollable
/// coordinate systems.
///
/// The default implementation is stateless, and simply provides a point-in-
/// time decision about how fast the scrollable is scrolling. It would always
/// return true for a scrollable that is animating back and forth at high
/// velocity in a loop. It is assumed that callers will handle such
/// a case, or that a custom stateful implementation would be written that
/// tracks the sign of the velocity on successive calls.
///
/// Returning true from this method indicates that the current scroll velocity
/// is great enough that expensive operations impacting the UI should be
/// deferred.
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
assert(velocity != null);
assert(metrics != null);
assert(context != null);
if (parent == null) {
final double maxPhysicalPixels = WidgetsBinding.instance.window.physicalSize.longestSide;
return velocity.abs() > maxPhysicalPixels;
}
return parent!.recommendDeferredLoading(velocity, metrics, context);
}
/// Determines the overscroll by applying the boundary conditions.
///
/// Called by [ScrollPosition.applyBoundaryConditions], which is called by
/// [ScrollPosition.setPixels] just before the [ScrollPosition.pixels] value
/// is updated, to determine how much of the offset is to be clamped off and
/// sent to [ScrollPosition.didOverscrollBy].
///
/// The `value` argument is guaranteed to not equal the [ScrollMetrics.pixels]
/// of the `position` argument when this is called.
///
/// It is possible for this method to be called when the `position` describes
/// an already-out-of-bounds position. In that case, the boundary conditions
/// should usually only prevent a further increase in the extent to which the
/// position is out of bounds, allowing a decrease to be applied successfully,
/// so that (for instance) an animation can smoothly snap an out of bounds
/// position to the bounds. See [BallisticScrollActivity].
///
/// This method must not clamp parts of the offset that are entirely within
/// the bounds described by the given `position`.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
///
/// ## Examples
///
/// [BouncingScrollPhysics] returns zero. In other words, it allows scrolling
/// past the boundary unhindered.
///
/// [ClampingScrollPhysics] returns the amount by which the value is beyond
/// the position or the boundary, whichever is furthest from the content. In
/// other words, it disallows scrolling past the boundary, but allows
/// scrolling back from being overscrolled, if for some reason the position
/// ends up overscrolled.
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (parent == null) {
return 0.0;
}
return parent!.applyBoundaryConditions(position, value);
}
/// Describes what the scroll position should be given new viewport dimensions.
///
/// This is called by [ScrollPosition.correctForNewDimensions].
///
/// The arguments consist of the scroll metrics as they stood in the previous
/// frame and the scroll metrics as they now stand after the last layout,
/// including the position and minimum and maximum scroll extents; a flag
/// indicating if the current [ScrollActivity] considers that the user is
/// actively scrolling (see [ScrollActivity.isScrolling]); and the current
/// velocity of the scroll position, if it is being driven by the scroll
/// activity (this is 0.0 during a user gesture) (see
/// [ScrollActivity.velocity]).
///
/// The scroll metrics will be identical except for the
/// [ScrollMetrics.minScrollExtent] and [ScrollMetrics.maxScrollExtent]. They
/// are referred to as the `oldPosition` and `newPosition` (even though they
/// both technically have the same "position", in the form of
/// [ScrollMetrics.pixels]) because they are generated from the
/// [ScrollPosition] before and after updating the scroll extents.
///
/// If the returned value does not exactly match the scroll offset given by
/// the `newPosition` argument (see [ScrollMetrics.pixels]), then the
/// [ScrollPosition] will call [ScrollPosition.correctPixels] to update the
/// new scroll position to the returned value, and layout will be re-run. This
/// is expensive. The new value is subject to further manipulation by
/// [applyBoundaryConditions].
///
/// If the returned value _does_ match the `newPosition.pixels` scroll offset
/// exactly, then [ScrollPosition.applyNewDimensions] will be called next. In
/// that case, [applyBoundaryConditions] is not applied to the return value.
///
/// The given [ScrollMetrics] are only valid during this method call. Do not
/// keep references to them to use later, as the values may update, may not
/// update, or may update to reflect an entirely unrelated scrollable.
///
/// The default implementation returns the [ScrollMetrics.pixels] of the
/// `newPosition`, which indicates that the current scroll offset is
/// acceptable.
///
/// See also:
///
/// * [RangeMaintainingScrollPhysics], which is enabled by default, and
/// which prevents unexpected changes to the content dimensions from
/// causing the scroll position to get any further out of bounds.
double adjustPositionForNewDimensions({
required ScrollMetrics oldPosition,
required ScrollMetrics newPosition,
required bool isScrolling,
required double velocity,
}) {
if (parent == null) {
return newPosition.pixels;
}
return parent!.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
}
/// Returns a simulation for ballistic scrolling starting from the given
/// position with the given velocity.
///
/// This is used by [ScrollPositionWithSingleContext] in the
/// [ScrollPositionWithSingleContext.goBallistic] method. If the result
/// is non-null, [ScrollPositionWithSingleContext] will begin a
/// [BallisticScrollActivity] with the returned value. Otherwise, it will
/// begin an idle activity instead.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
if (parent == null) {
return null;
}
return parent!.createBallisticSimulation(position, velocity);
}
static final SpringDescription _kDefaultSpring = SpringDescription.withDampingRatio(
mass: 0.5,
stiffness: 100.0,
ratio: 1.1,
);
/// The spring to use for ballistic simulations.
SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
/// The default accuracy to which scrolling is computed.
static final Tolerance _kDefaultTolerance = Tolerance(
// TODO(ianh): Handle the case of the device pixel ratio changing.
// TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
velocity: 1.0 / (0.050 * WidgetsBinding.instance.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / WidgetsBinding.instance.window.devicePixelRatio, // logical pixels
);
/// The tolerance to use for ballistic simulations.
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
/// The minimum distance an input pointer drag must have moved to
/// to be considered a scroll fling gesture.
///
/// This value is typically compared with the distance traveled along the
/// scrolling axis.
///
/// See also:
///
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
/// of a press-drag-release gesture.
double get minFlingDistance => parent?.minFlingDistance ?? kTouchSlop;
/// The minimum velocity for an input pointer drag to be considered a
/// scroll fling.
///
/// This value is typically compared with the magnitude of fling gesture's
/// velocity along the scrolling axis.
///
/// See also:
///
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
/// of a press-drag-release gesture.
double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
/// Scroll fling velocity magnitudes will be clamped to this value.
double get maxFlingVelocity => parent?.maxFlingVelocity ?? kMaxFlingVelocity;
/// Returns the velocity carried on repeated flings.
///
/// The function is applied to the existing scroll velocity when another
/// scroll drag is applied in the same direction.
///
/// By default, physics for platforms other than iOS doesn't carry momentum.
double carriedMomentum(double existingVelocity) {
if (parent == null) {
return 0.0;
}
return parent!.carriedMomentum(existingVelocity);
}
/// The minimum amount of pixel distance drags must move by to start motion
/// the first time or after each time the drag motion stopped.
///
/// If null, no minimum threshold is enforced.
double? get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
/// Whether a viewport is allowed to change its scroll position implicitly in
/// response to a call to [RenderObject.showOnScreen].
///
/// [RenderObject.showOnScreen] is for example used to bring a text field
/// fully on screen after it has received focus. This property controls
/// whether the viewport associated with this object is allowed to change the
/// scroll position to fulfill such a request.
bool get allowImplicitScrolling => true;
@override
String toString() {
if (parent == null) {
return objectRuntimeType(this, 'ScrollPhysics');
}
return '${objectRuntimeType(this, 'ScrollPhysics')} -> $parent';
}
}
/// Scroll physics that attempt to keep the scroll position in range when the
/// contents change dimensions suddenly.
///
/// This attempts to maintain the amount of overscroll or underscroll already present,
/// if the scroll position is already out of range _and_ the extents
/// have decreased, meaning that some content was removed. The reason for this
/// condition is that when new content is added, keeping the same overscroll
/// would mean that instead of showing it to the user, all of it is
/// being skipped by jumping right to the max extent.
///
/// If the scroll activity is animating the scroll position, sudden changes to
/// the scroll dimensions are allowed to happen (so as to prevent animations
/// from jumping back and forth between in-range and out-of-range values).
///
/// These physics should be combined with other scroll physics, e.g.
/// [BouncingScrollPhysics] or [ClampingScrollPhysics], to obtain a complete
/// description of typical scroll physics. See [applyTo].
///
/// ## Implementation details
///
/// Specifically, these physics perform two adjustments.
///
/// The first is to maintain overscroll when the position is out of range.
///
/// The second is to enforce the boundary when the position is in range.
///
/// If the current velocity is non-zero, neither adjustment is made. The
/// assumption is that there is an ongoing animation and therefore
/// further changing the scroll position would disrupt the experience.
///
/// If the extents haven't changed, then the overscroll adjustment is
/// not made. The assumption is that if the position is overscrolled,
/// it is intentional, otherwise the position could not have reached
/// that position. (Consider [ClampingScrollPhysics] vs
/// [BouncingScrollPhysics] for example.)
///
/// If the position itself changed since the last animation frame,
/// then the overscroll is not maintained. The assumption is similar
/// to the previous case: the position would not have been placed out
/// of range unless it was intentional.
///
/// In addition, if the position changed and the boundaries were and
/// still are finite, then the boundary isn't enforced either, for
/// the same reason. However, if any of the boundaries were or are
/// now infinite, the boundary _is_ enforced, on the assumption that
/// infinite boundaries indicate a lazy-loading scroll view, which
/// cannot enforce boundaries while the full list has not loaded.
///
/// If the range was out of range, then the boundary is not enforced
/// even if the range is not maintained. If the range is maintained,
/// then the distance between the old position and the old boundary is
/// applied to the new boundary to obtain the new position.
///
/// If the range was in range, and the boundary is to be enforced,
/// then the new position is obtained by deferring to the other physics,
/// if any, and then clamped to the new range.
class RangeMaintainingScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that maintain the scroll position in range.
const RangeMaintainingScrollPhysics({ super.parent });
@override
RangeMaintainingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return RangeMaintainingScrollPhysics(parent: buildParent(ancestor));
}
@override
double adjustPositionForNewDimensions({
required ScrollMetrics oldPosition,
required ScrollMetrics newPosition,
required bool isScrolling,
required double velocity,
}) {
bool maintainOverscroll = true;
bool enforceBoundary = true;
if (velocity != 0.0) {
// Don't try to adjust an animating position, the jumping around
// would be distracting.
maintainOverscroll = false;
enforceBoundary = false;
}
if ((oldPosition.minScrollExtent == newPosition.minScrollExtent) &&
(oldPosition.maxScrollExtent == newPosition.maxScrollExtent)) {
// If the extents haven't changed then ignore overscroll.
maintainOverscroll = false;
}
if (oldPosition.pixels != newPosition.pixels) {
// If the position has been changed already, then it might have
// been adjusted to expect new overscroll, so don't try to
// maintain the relative overscroll.
maintainOverscroll = false;
if (oldPosition.minScrollExtent.isFinite && oldPosition.maxScrollExtent.isFinite &&
newPosition.minScrollExtent.isFinite && newPosition.maxScrollExtent.isFinite) {
// In addition, if the position changed then we don't enforce the new
// boundary if both the new and previous boundaries are entirely finite.
// A common case where the position changes while one
// of the extents is infinite is a lazily-loaded list. (If the
// boundaries were finite, and the position changed, then we
// assume it was intentional.)
enforceBoundary = false;
}
}
if ((oldPosition.pixels < oldPosition.minScrollExtent) ||
(oldPosition.pixels > oldPosition.maxScrollExtent)) {
// If the old position was out of range, then we should
// not try to keep the new position in range.
enforceBoundary = false;
}
if (maintainOverscroll) {
// Force the new position to be no more out of range than it was before, if:
// * it was overscrolled, and
// * the extents have decreased, meaning that some content was removed. The
// reason for this condition is that when new content is added, keeping
// the same overscroll would mean that instead of showing it to the user,
// all of it is being skipped by jumping right to the max extent.
if (oldPosition.pixels < oldPosition.minScrollExtent &&
newPosition.minScrollExtent > oldPosition.minScrollExtent) {
final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels;
return newPosition.minScrollExtent - oldDelta;
}
if (oldPosition.pixels > oldPosition.maxScrollExtent &&
newPosition.maxScrollExtent < oldPosition.maxScrollExtent) {
final double oldDelta = oldPosition.pixels - oldPosition.maxScrollExtent;
return newPosition.maxScrollExtent + oldDelta;
}
}
// If we're not forcing the overscroll, defer to other physics.
double result = super.adjustPositionForNewDimensions(oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity);
if (enforceBoundary) {
// ...but if they put us out of range then reinforce the boundary.
result = clampDouble(result, newPosition.minScrollExtent, newPosition.maxScrollExtent);
}
return result;
}
}
/// Scroll physics for environments that allow the scroll offset to go beyond
/// the bounds of the content, but then bounce the content back to the edge of
/// those bounds.
///
/// This is the behavior typically seen on iOS.
///
/// [BouncingScrollPhysics] by itself will not create an overscroll effect if
/// the contents of the scroll view do not extend beyond the size of the
/// viewport. To create the overscroll and bounce effect regardless of the
/// length of your scroll view, combine with [AlwaysScrollableScrollPhysics].
///
/// {@tool snippet}
/// ```dart
/// const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [ScrollConfiguration], which uses this to provide the default
/// scroll behavior on iOS.
/// * [ClampingScrollPhysics], which is the analogous physics for Android's
/// clamping behavior.
/// * [ScrollPhysics], for more examples of combining [ScrollPhysics] objects
/// of different types to get the desired scroll physics.
class BouncingScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that bounce back from the edge.
const BouncingScrollPhysics({
this.decelerationRate = ScrollDecelerationRate.normal,
super.parent,
});
/// Used to determine parameters for friction simulations.
final ScrollDecelerationRate decelerationRate;
@override
BouncingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return BouncingScrollPhysics(parent: buildParent(ancestor));
}
/// The multiple applied to overscroll to make it appear that scrolling past
/// the edge of the scrollable contents is harder than scrolling the list.
/// This is done by reducing the ratio of the scroll effect output vs the
/// scroll gesture input.
///
/// This factor starts at 0.52 and progressively becomes harder to overscroll
/// as more of the area past the edge is dragged in (represented by an increasing
/// `overscrollFraction` which starts at 0 when there is no overscroll).
double frictionFactor(double overscrollFraction) {
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
return 0.07 * math.pow(1 - overscrollFraction, 2);
case ScrollDecelerationRate.normal:
return 0.52 * math.pow(1 - overscrollFraction, 2);
}
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
assert(offset != 0.0);
assert(position.minScrollExtent <= position.maxScrollExtent);
if (!position.outOfRange) {
return offset;
}
final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0);
final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0);
final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd);
final bool easing = (overscrollPastStart > 0.0 && offset < 0.0)
|| (overscrollPastEnd > 0.0 && offset > 0.0);
final double friction = easing
// Apply less resistance when easing the overscroll vs tensioning.
? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension)
: frictionFactor(overscrollPast / position.viewportDimension);
final double direction = offset.sign;
return direction * _applyFriction(overscrollPast, offset.abs(), friction);
}
static double _applyFriction(double extentOutside, double absDelta, double gamma) {
assert(absDelta > 0);
double total = 0.0;
if (extentOutside > 0) {
final double deltaToLimit = extentOutside / gamma;
if (absDelta < deltaToLimit) {
return absDelta * gamma;
}
total += extentOutside;
absDelta -= deltaToLimit;
}
return total + absDelta;
}
@override
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
double constantDeceleration;
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
constantDeceleration = 1400;
break;
case ScrollDecelerationRate.normal:
constantDeceleration = 0;
break;
}
return BouncingScrollSimulation(
spring: spring,
position: position.pixels,
velocity: velocity,
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
tolerance: tolerance,
constantDeceleration: constantDeceleration
);
}
return null;
}
// The ballistic simulation here decelerates more slowly than the one for
// ClampingScrollPhysics so we require a more deliberate input gesture
// to trigger a fling.
@override
double get minFlingVelocity => kMinFlingVelocity * 2.0;
// Methodology:
// 1- Use https://github.com/flutter/platform_tests/tree/master/scroll_overlay to test with
// Flutter and platform scroll views superimposed.
// 3- If the scrollables stopped overlapping at any moment, adjust the desired
// output value of this function at that input speed.
// 4- Feed new input/output set into a power curve fitter. Change function
// and repeat from 2.
// 5- Repeat from 2 with medium and slow flings.
/// Momentum build-up function that mimics iOS's scroll speed increase with repeated flings.
///
/// The velocity of the last fling is not an important factor. Existing speed
/// and (related) time since last fling are factors for the velocity transfer
/// calculations.
@override
double carriedMomentum(double existingVelocity) {
return existingVelocity.sign *
math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0);
}
// Eyeballed from observation to counter the effect of an unintended scroll
// from the natural motion of lifting the finger after a scroll.
@override
double get dragStartDistanceMotionThreshold => 3.5;
@override
double get maxFlingVelocity {
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
return kMaxFlingVelocity * 8.0;
case ScrollDecelerationRate.normal:
return super.maxFlingVelocity;
}
}
@override
SpringDescription get spring {
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
return SpringDescription.withDampingRatio(
mass: 0.3,
stiffness: 75.0,
ratio: 1.3,
);
case ScrollDecelerationRate.normal:
return super.spring;
}
}
}
/// Scroll physics for environments that prevent the scroll offset from reaching
/// beyond the bounds of the content.
///
/// This is the behavior typically seen on Android.
///
/// See also:
///
/// * [ScrollConfiguration], which uses this to provide the default
/// scroll behavior on Android.
/// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing
/// behavior.
/// * [GlowingOverscrollIndicator], which is used by [ScrollConfiguration] to
/// provide the glowing effect that is usually found with this clamping effect
/// on Android. When using a [MaterialApp], the [GlowingOverscrollIndicator]'s
/// glow color is specified to use the overall theme's
/// [ColorScheme.secondary] color.
class ClampingScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that prevent the scroll offset from exceeding the
/// bounds of the content.
const ClampingScrollPhysics({ super.parent });
@override
ClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return ClampingScrollPhysics(parent: buildParent(ancestor));
}
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$runtimeType.applyBoundaryConditions() was called redundantly.'),
ErrorDescription(
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.',
),
DiagnosticsProperty<ScrollPhysics>('The physics object in question was', this, style: DiagnosticsTreeStyle.errorProperty),
DiagnosticsProperty<ScrollMetrics>('The position object in question was', position, style: DiagnosticsTreeStyle.errorProperty),
]);
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent) {
// Underscroll.
return value - position.pixels;
}
if (position.maxScrollExtent <= position.pixels && position.pixels < value) {
// Overscroll.
return value - position.pixels;
}
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) {
// Hit top edge.
return value - position.minScrollExtent;
}
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) {
// Hit bottom edge.
return value - position.maxScrollExtent;
}
return 0.0;
}
@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (position.outOfRange) {
double? end;
if (position.pixels > position.maxScrollExtent) {
end = position.maxScrollExtent;
}
if (position.pixels < position.minScrollExtent) {
end = position.minScrollExtent;
}
assert(end != null);
return ScrollSpringSimulation(
spring,
position.pixels,
end!,
math.min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) {
return null;
}
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
return null;
}
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
return null;
}
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
}
/// Scroll physics that always lets the user scroll.
///
/// This overrides the default behavior which is to disable scrolling
/// when there is no content to scroll. It does not override the
/// handling of overscrolling.
///
/// On Android, overscrolls will be clamped by default and result in an
/// overscroll glow. On iOS, overscrolls will load a spring that will return the
/// scroll view to its normal range when released.
///
/// See also:
///
/// * [ScrollPhysics], which can be used instead of this class when the default
/// behavior is desired instead.
/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
/// found on iOS.
/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
/// found on Android.
class AlwaysScrollableScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that always lets the user scroll.
const AlwaysScrollableScrollPhysics({ super.parent });
@override
AlwaysScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
return AlwaysScrollableScrollPhysics(parent: buildParent(ancestor));
}
@override
bool shouldAcceptUserOffset(ScrollMetrics position) => true;
}
/// Scroll physics that does not allow the user to scroll.
///
/// See also:
///
/// * [ScrollPhysics], which can be used instead of this class when the default
/// behavior is desired instead.
/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
/// found on iOS.
/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
/// found on Android.
class NeverScrollableScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that does not let the user scroll.
const NeverScrollableScrollPhysics({ super.parent });
@override
NeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NeverScrollableScrollPhysics(parent: buildParent(ancestor));
}
@override
bool shouldAcceptUserOffset(ScrollMetrics position) => false;
@override
bool get allowImplicitScrolling => false;
}