// 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 Simulation, ScrollSpringSimulation, Tolerance; // Examples can assume: // class FooScrollPhysics extends ScrollPhysics { // const FooScrollPhysics({ ScrollPhysics? parent }): super(parent: parent); // @override // FooScrollPhysics applyTo(ScrollPhysics? ancestor) { // return FooScrollPhysics(parent: buildParent(ancestor)); // } // } // class BarScrollPhysics extends ScrollPhysics { // const BarScrollPhysics({ ScrollPhysics? parent }): super(parent: 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(); /// // ... /// 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 /// FooScrollPhysics applyTo(ScrollPhysics ancestor) { /// return FooScrollPhysics(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 = FooScrollPhysics().applyTo(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 /// FooScrollPhysics applyTo(ScrollPhysics ancestor) { /// return FooScrollPhysics(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, 'ScrollPhsyics'); return '${objectRuntimeType(this, 'ScrollPhysics')} -> $parent'; } } /// Scroll physics that attempt to keep the scroll position in range when the /// contents change dimensions suddenly. /// /// If the scroll position is already out of range, this attempts to maintain /// the amount of overscroll or underscroll already present. /// /// 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({ ScrollPhysics? parent }) : super(parent: 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 only enforce // the new boundary if the previous boundary was not 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. if (oldPosition.pixels < oldPosition.minScrollExtent) { final double oldDelta = oldPosition.minScrollExtent - oldPosition.pixels; return newPosition.minScrollExtent - oldDelta; } if (oldPosition.pixels > 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 = result.clamp(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 /// 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({ ScrollPhysics? parent }) : super(parent: 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 [ThemeData.accentColor]. class ClampingScrollPhysics extends ScrollPhysics { /// Creates scroll physics that prevent the scroll offset from exceeding the /// bounds of the content. const ClampingScrollPhysics({ ScrollPhysics? parent }) : super(parent: 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({ ScrollPhysics? parent }) : super(parent: 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({ ScrollPhysics? parent }) : super(parent: parent); @override NeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { return NeverScrollableScrollPhysics(parent: buildParent(ancestor)); } @override bool shouldAcceptUserOffset(ScrollMetrics position) => false; @override bool get allowImplicitScrolling => false; }