| // Copyright 2015 The Chromium 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 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/physics.dart'; |
| |
| import 'overscroll_indicator.dart'; |
| import 'scroll_metrics.dart'; |
| import 'scroll_simulation.dart'; |
| |
| export 'package:flutter/physics.dart' show Simulation, ScrollSpringSimulation, Tolerance; |
| |
| /// 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. |
| @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. |
| 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 new FooScrollPhysics(parent: buildParent(ancestor)); |
| /// } |
| /// ``` |
| @protected |
| ScrollPhysics buildParent(ScrollPhysics ancestor) => parent?.applyTo(ancestor) ?? ancestor; |
| |
| /// If [parent] is null then return a [ScrollPhysics] with the same |
| /// [runtimeType] 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. |
| /// |
| /// The returned object will combine some of the behaviors from this |
| /// [ScrollPhysics] instance and some of the behaviors from [ancestor]. |
| /// |
| /// See also: |
| /// |
| /// * [buildParent], a utility method that's often used to define [applyTo] |
| /// methods for ScrollPhysics subclasses. |
| ScrollPhysics applyTo(ScrollPhysics ancestor) { |
| return new 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); |
| } |
| |
| /// 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); |
| } |
| |
| /// 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 = new 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 = new 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 * ui.window.devicePixelRatio), // logical pixels per second |
| distance: 1.0 / ui.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; |
| |
| @override |
| String toString() { |
| if (parent == null) |
| return runtimeType.toString(); |
| return '$runtimeType -> $parent'; |
| } |
| } |
| |
| /// 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. |
| /// |
| /// 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. |
| 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 new 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 new BouncingScrollSimulation( |
| spring: spring, |
| position: position.pixels, |
| velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end. |
| 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/scroll_overlay to test with Flutter and |
| // platform scroll views superimposed. |
| // 2- Record incoming speed and make rapid flings in the test app. |
| // 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 new ClampingScrollPhysics(parent: buildParent(ancestor)); |
| } |
| |
| @override |
| double applyBoundaryConditions(ScrollMetrics position, double value) { |
| assert(() { |
| if (value == position.pixels) { |
| throw new FlutterError( |
| '$runtimeType.applyBoundaryConditions() was called redundantly.\n' |
| '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.\n' |
| 'The physics object in question was:\n' |
| ' $this\n' |
| 'The position object in question was:\n' |
| ' $position\n' |
| ); |
| } |
| 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 new ScrollSpringSimulation( |
| spring, |
| position.pixels, |
| position.maxScrollExtent, |
| 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 new ClampingScrollSimulation( |
| position: position.pixels, |
| velocity: velocity, |
| tolerance: tolerance, |
| ); |
| } |
| } |
| |
| /// Scroll physics that always lets the user scroll. |
| /// |
| /// 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 new 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 new NeverScrollableScrollPhysics(parent: buildParent(ancestor)); |
| } |
| |
| @override |
| bool shouldAcceptUserOffset(ScrollMetrics position) => false; |
| } |