| // 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; |
| } |