scroll_simulation.dart 8.37 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:math' as math;

9 10 11
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';

12 13 14 15 16
/// An implementation of scroll physics that matches iOS.
///
/// See also:
///
///  * [ClampingScrollSimulation], which implements Android scroll physics.
17
class BouncingScrollSimulation extends Simulation {
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
  /// Creates a simulation group for scrolling on iOS, with the given
  /// parameters.
  ///
  /// The position and velocity arguments must use the same units as will be
  /// expected from the [x] and [dx] methods respectively (typically logical
  /// pixels and logical pixels per second respectively).
  ///
  /// The leading and trailing extents must use the unit of length, the same
  /// unit as used for the position argument and as expected from the [x]
  /// method (typically logical pixels).
  ///
  /// The units used with the provided [SpringDescription] must similarly be
  /// consistent with the other arguments. A default set of constants is used
  /// for the `spring` description if it is omitted; these defaults assume
  /// that the unit of length is the logical pixel.
  BouncingScrollSimulation({
34 35 36 37 38
    @required double position,
    @required double velocity,
    @required this.leadingExtent,
    @required this.trailingExtent,
    @required this.spring,
39
    Tolerance tolerance = Tolerance.defaultTolerance,
40 41 42 43 44 45 46
  }) : assert(position != null),
       assert(velocity != null),
       assert(leadingExtent != null),
       assert(trailingExtent != null),
       assert(leadingExtent <= trailingExtent),
       assert(spring != null),
       super(tolerance: tolerance) {
47 48
    if (position < leadingExtent) {
      _springSimulation = _underscrollSimulation(position, velocity);
49
      _springTime = double.negativeInfinity;
50 51
    } else if (position > trailingExtent) {
      _springSimulation = _overscrollSimulation(position, velocity);
52
      _springTime = double.negativeInfinity;
53
    } else {
54 55
      // Taken from UIScrollView.decelerationRate (.normal = 0.998)
      // 0.998^1000 = ~0.135
56
      _frictionSimulation = FrictionSimulation(0.135, position, velocity);
57 58 59
      final double finalX = _frictionSimulation.finalX;
      if (velocity > 0.0 && finalX > trailingExtent) {
        _springTime = _frictionSimulation.timeAtX(trailingExtent);
60 61 62 63
        _springSimulation = _overscrollSimulation(
          trailingExtent,
          math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
        );
64 65 66
        assert(_springTime.isFinite);
      } else if (velocity < 0.0 && finalX < leadingExtent) {
        _springTime = _frictionSimulation.timeAtX(leadingExtent);
67 68 69 70
        _springSimulation = _underscrollSimulation(
          leadingExtent,
          math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
        );
71 72
        assert(_springTime.isFinite);
      } else {
73
        _springTime = double.infinity;
74 75 76
      }
    }
    assert(_springTime != null);
77
  }
78

79
  /// The maximum velocity that can be transferred from the inertia of a ballistic
80 81 82
  /// scroll into overscroll.
  static const double maxSpringTransferVelocity = 5000.0;

83 84 85 86 87 88 89
  /// When [x] falls below this value the simulation switches from an internal friction
  /// model to a spring model which causes [x] to "spring" back to [leadingExtent].
  final double leadingExtent;

  /// When [x] exceeds this value the simulation switches from an internal friction
  /// model to a spring model which causes [x] to "spring" back to [trailingExtent].
  final double trailingExtent;
90

91 92 93
  /// The spring used used to return [x] to either [leadingExtent] or [trailingExtent].
  final SpringDescription spring;

94 95 96
  FrictionSimulation _frictionSimulation;
  Simulation _springSimulation;
  double _springTime;
97 98 99
  double _timeOffset = 0.0;

  Simulation _underscrollSimulation(double x, double dx) {
100
    return ScrollSpringSimulation(spring, x, leadingExtent, dx);
101 102 103
  }

  Simulation _overscrollSimulation(double x, double dx) {
104
    return ScrollSpringSimulation(spring, x, trailingExtent, dx);
105 106 107 108
  }

  Simulation _simulation(double time) {
    Simulation simulation;
109
    if (time > _springTime) {
110 111 112 113 114 115 116 117
      _timeOffset = _springTime.isFinite ? _springTime : 0.0;
      simulation = _springSimulation;
    } else {
      _timeOffset = 0.0;
      simulation = _frictionSimulation;
    }
    return simulation..tolerance = tolerance;
  }
118 119

  @override
120
  double x(double time) => _simulation(time).x(time - _timeOffset);
121 122

  @override
123
  double dx(double time) => _simulation(time).dx(time - _timeOffset);
124 125

  @override
126
  bool isDone(double time) => _simulation(time).isDone(time - _timeOffset);
127 128 129

  @override
  String toString() {
130
    return '${objectRuntimeType(this, 'BouncingScrollSimulation')}(leadingExtent: $leadingExtent, trailingExtent: $trailingExtent)';
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
  }
}

/// An implementation of scroll physics that matches Android.
///
/// See also:
///
///  * [BouncingScrollSimulation], which implements iOS scroll physics.
//
// This class is based on Scroller.java from Android:
//   https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
//
// The "See..." comments below refer to Scroller methods and values. Some
// simplifications have been made.
class ClampingScrollSimulation extends Simulation {
  /// Creates a scroll physics simulation that matches Android scrolling.
  ClampingScrollSimulation({
148 149
    @required this.position,
    @required this.velocity,
150 151
    this.friction = 0.015,
    Tolerance tolerance = Tolerance.defaultTolerance,
152
  }) : assert(_flingVelocityPenetration(0.0) == _initialVelocityPenetration),
153
       super(tolerance: tolerance) {
154
    _duration = _flingDuration(velocity);
155
    _distance = (velocity * _duration / _initialVelocityPenetration).abs();
156 157
  }

158
  /// The position of the particle at the beginning of the simulation.
159
  final double position;
160 161 162

  /// The velocity at which the particle is traveling at the beginning of the
  /// simulation.
163
  final double velocity;
164 165 166 167

  /// The amount of friction the particle experiences as it travels.
  ///
  /// The more friction the particle experiences, the sooner it stops.
168 169
  final double friction;

170 171
  double _duration;
  double _distance;
172

173
  // See DECELERATION_RATE.
174
  static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);
175 176

  // See computeDeceleration().
177
  static double _decelerationForFriction(double friction) {
178 179
    return friction * 61774.04968;
  }
180

181 182
  // See getSplineFlingDuration(). Returns a value in seconds.
  double _flingDuration(double velocity) {
183 184 185 186 187
    // See mPhysicalCoeff
    final double scaledFriction = friction * _decelerationForFriction(0.84);

    // See getSplineDeceleration().
    final double deceleration = math.log(0.35 * velocity.abs() / scaledFriction);
188

189
    return math.exp(deceleration / (_kDecelerationRate - 1.0));
190 191
  }

192 193 194 195
  // Based on a cubic curve fit to the Scroller.computeScrollOffset() values
  // produced for an initial velocity of 4000. The value of Scroller.getDuration()
  // and Scroller.getFinalY() were 686ms and 961 pixels respectively.
  //
196 197
  // Algebra courtesy of Wolfram Alpha.
  //
Josh Soref's avatar
Josh Soref committed
198
  // f(x) = scrollOffset, x is time in milliseconds
199 200 201 202 203 204 205 206
  // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x - 3.15307
  // f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x, so f(0) is 0
  // f(686ms) = 961 pixels
  // Scale to f(0 <= t <= 1.0), x = t * 686
  // f(t) = 1165.03 t^3 - 3143.62 t^2 + 2945.87 t
  // Scale f(t) so that 0.0 <= f(t) <= 1.0
  // f(t) = (1165.03 t^3 - 3143.62 t^2 + 2945.87 t) / 961.0
  //      = 1.2 t^3 - 3.27 t^2 + 3.065 t
207
  static const double _initialVelocityPenetration = 3.065;
208
  static double _flingDistancePenetration(double t) {
209
    return (1.2 * t * t * t) - (3.27 * t * t) + (_initialVelocityPenetration * t);
210 211
  }

212
  // The derivative of the _flingDistancePenetration() function.
213
  static double _flingVelocityPenetration(double t) {
214
    return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration;
215 216 217 218
  }

  @override
  double x(double time) {
219
    final double t = (time / _duration).clamp(0.0, 1.0) as double;
220 221 222 223 224
    return position + _distance * _flingDistancePenetration(t) * velocity.sign;
  }

  @override
  double dx(double time) {
225
    final double t = (time / _duration).clamp(0.0, 1.0) as double;
226
    return _distance * _flingVelocityPenetration(t) * velocity.sign / _duration;
227 228 229 230 231 232
  }

  @override
  bool isDone(double time) {
    return time >= _duration;
  }
233
}