mirror of
https://github.com/flutter/flutter.git
synced 2026-02-12 13:56:40 +08:00
872 lines
36 KiB
Dart
872 lines
36 KiB
Dart
// 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;
|
|
|
|
// 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({ super.parent });
|
|
|
|
@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) => 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) {
|
|
return BouncingScrollSimulation(
|
|
spring: spring,
|
|
position: position.pixels,
|
|
velocity: velocity,
|
|
leadingExtent: position.minScrollExtent,
|
|
trailingExtent: position.maxScrollExtent,
|
|
tolerance: tolerance,
|
|
);
|
|
}
|
|
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;
|
|
}
|
|
|
|
/// 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;
|
|
}
|