scroll_simulation.dart 11 KB
Newer Older
1
// Copyright 2016 The Chromium 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
import 'dart:math' as math;

7 8 9
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
/// An implementation of scroll physics that matches iOS.
///
/// See also:
///
///  * [ClampingScrollSimulation], which implements Android scroll physics.
class BouncingScrollSimulation extends SimulationGroup {
  /// 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({
    @required double position,
    @required double velocity,
    @required double leadingExtent,
    @required double trailingExtent,
Adam Barth's avatar
Adam Barth committed
36
    @required SpringDescription spring,
37 38
  }) : _leadingExtent = leadingExtent,
       _trailingExtent = trailingExtent,
Adam Barth's avatar
Adam Barth committed
39
       _spring = spring {
40 41 42 43 44 45 46 47
    assert(position != null);
    assert(velocity != null);
    assert(_leadingExtent != null);
    assert(_trailingExtent != null);
    assert(_leadingExtent <= _trailingExtent);
    assert(_spring != null);
    _chooseSimulation(position, velocity, 0.0);
  }
48

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
  final double _leadingExtent;
  final double _trailingExtent;
  final SpringDescription _spring;

  bool _isSpringing = false;
  Simulation _currentSimulation;
  double _offset = 0.0;

  // This simulation can only step forward.
  @override
  bool step(double time) => _chooseSimulation(
    _currentSimulation.x(time - _offset),
    _currentSimulation.dx(time - _offset),
    time,
  );

  @override
  Simulation get currentSimulation => _currentSimulation;

  @override
  double get currentIntervalOffset => _offset;

  bool _chooseSimulation(double position, double velocity, double intervalOffset) {
    if (!_isSpringing) {
      if (position > _trailingExtent) {
        _isSpringing = true;
        _offset = intervalOffset;
        _currentSimulation = new ScrollSpringSimulation(_spring, position, _trailingExtent, velocity);
        return true;
      } else if (position < _leadingExtent) {
        _isSpringing = true;
        _offset = intervalOffset;
        _currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity);
        return true;
      } else if (_currentSimulation == null) {
        _currentSimulation = new FrictionSimulation(0.135, position, velocity * 0.91);
        return true;
      }
    }
    return false;
  }

  @override
  String toString() {
    return '$runtimeType(leadingExtent: $_leadingExtent, trailingExtent: $_trailingExtent)';
  }
}

/// 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.
  //
  // TODO(ianh): The incoming `velocity` is used to determine the starting speed
  // and duration, but does not represent the exact velocity of the simulation
  // at t=0 as it should. This causes crazy scrolling irregularities when the
  // scroll dimensions change during a fling.
  ClampingScrollSimulation({
    @required this.position,
    @required this.velocity,
118
    this.friction: 0.015,
119 120
    Tolerance tolerance: Tolerance.defaultTolerance,
  }) : super(tolerance: tolerance) {
121 122 123 124 125 126 127 128 129 130 131 132 133
    _scaledFriction = friction * _decelerationForFriction(0.84); // See mPhysicalCoeff
    _duration = _flingDuration(velocity);
    _distance = _flingDistance(velocity);
  }

  final double position;
  final double velocity;
  final double friction;

  double _scaledFriction;
  double _duration;
  double _distance;

134
  // See DECELERATION_RATE.
135 136 137 138 139 140
  static final double _decelerationRate = math.log(0.78) / math.log(0.9);

  // See computeDeceleration().
  double _decelerationForFriction(double friction) {
    return friction * 61774.04968;
  }
141

142
  // See getSplineDeceleration().
143 144 145
  double _flingDeceleration(double velocity) {
    return math.log(0.35 * velocity.abs() / _scaledFriction);
  }
146

147 148 149 150 151
  // See getSplineFlingDuration(). Returns a value in seconds.
  double _flingDuration(double velocity) {
    return math.exp(_flingDeceleration(velocity) / (_decelerationRate - 1.0));
  }

152
  // See getSplineFlingDistance().
153 154 155 156 157
  double _flingDistance(double velocity) {
    final double rate = _decelerationRate / (_decelerationRate - 1.0) * _flingDeceleration(velocity);
    return _scaledFriction * math.exp(rate);
  }

158 159 160 161
  // 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.
  //
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
  // Algebra courtesy of Wolfram Alpha.
  //
  // f(x) = scrollOffset, x is time in millseconds
  // 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
  double _flingDistancePenetration(double t) {
    return (1.2 * t * t * t) - (3.27 * t * t) + (3.065 * t);
  }

177
  // The derivative of the _flingDistancePenetration() function.
178 179 180 181 182 183 184 185 186 187 188 189 190
  double _flingVelocityPenetration(double t) {
    return (3.63693 * t * t) - (6.5424 * t) + 3.06542;
  }

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

  @override
  double dx(double time) {
    final double t = (time / _duration).clamp(0.0, 1.0);
191
    return _distance * _flingVelocityPenetration(t) * velocity.sign;
192 193 194 195 196 197
  }

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

200
////////////////////////////////////////////////////////////////////////////////
201
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
202
////////////////////////////////////////////////////////////////////////////////
203 204 205 206

final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
final double _kDrag = 0.025;

207 208 209 210 211
class _CupertinoSimulation extends FrictionSimulation {
  static const double drag = 0.135;
  _CupertinoSimulation({ double position, double velocity })
    : super(drag, position, velocity * 0.91);
}
212

213 214 215 216 217 218 219 220 221 222 223 224
class _MountainViewSimulation extends ClampingScrollSimulation {
  _MountainViewSimulation({
    double position,
    double velocity,
    double friction: 0.015,
  }) : super(
    position: position,
    velocity: velocity,
    friction: friction,
  );
}

Ian Hickson's avatar
Ian Hickson committed
225 226
/// Composite simulation for scrollable interfaces.
///
227
/// Simulates kinetic scrolling behavior between a leading and trailing
Ian Hickson's avatar
Ian Hickson committed
228 229
/// boundary. Friction is applied within the extents and a spring action is
/// applied at the boundaries. This simulation can only step forward.
230
class ScrollSimulation extends SimulationGroup {
Ian Hickson's avatar
Ian Hickson committed
231 232 233 234 235 236 237 238 239 240 241 242 243
  /// Creates a [ScrollSimulation] 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.
  ///
  /// 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.
  ///
  /// The units used with the provided [SpringDescription] must similarly be
  /// consistent with the other arguments.
  ///
  /// The final argument is the coefficient of friction, which is unitless.
244
  ScrollSimulation({
245 246 247 248
    @required double position,
    @required double velocity,
    @required double leadingExtent,
    @required double trailingExtent,
249 250 251 252 253 254
    SpringDescription spring,
    double drag,
    TargetPlatform platform,
  }) : _leadingExtent = leadingExtent,
       _trailingExtent = trailingExtent,
       _spring = spring ?? _kScrollSpring,
255
       _drag = drag ?? _kDrag,
256
       _platform = platform {
257 258
    assert(position != null);
    assert(velocity != null);
259 260 261
    assert(_leadingExtent != null);
    assert(_trailingExtent != null);
    assert(_spring != null);
262 263 264
    _chooseSimulation(position, velocity, 0.0);
  }

265 266
  final double _leadingExtent;
  final double _trailingExtent;
267
  final SpringDescription _spring;
268
  final double _drag;
269
  final TargetPlatform _platform;
270 271 272

  bool _isSpringing = false;
  Simulation _currentSimulation;
273
  double _offset = 0.0;
274 275

  @override
276
  bool step(double time) => _chooseSimulation(
277 278
      _currentSimulation.x(time - _offset),
      _currentSimulation.dx(time - _offset), time);
279 280 281 282

  @override
  Simulation get currentSimulation => _currentSimulation;

283 284 285
  @override
  double get currentIntervalOffset => _offset;

286 287
  bool _chooseSimulation(double position, double velocity, double intervalOffset) {
    if (_spring == null && (position > _trailingExtent || position < _leadingExtent))
288 289
      return false;

Ian Hickson's avatar
Ian Hickson committed
290
    // This simulation can only step forward.
291 292 293
    if (!_isSpringing) {
      if (position > _trailingExtent) {
        _isSpringing = true;
294
        _offset = intervalOffset;
295
        _currentSimulation = new ScrollSpringSimulation(_spring, position, _trailingExtent, velocity);
296
        return true;
297 298
      } else if (position < _leadingExtent) {
        _isSpringing = true;
299
        _offset = intervalOffset;
300
        _currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity);
301
        return true;
302
      }
303 304 305
    }

    if (_currentSimulation == null) {
306 307 308 309 310
      switch (_platform) {
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          _currentSimulation = new _MountainViewSimulation(
            position: position,
311
            velocity: velocity,
312 313 314 315 316 317 318 319 320 321 322 323
          );
          break;
        case TargetPlatform.iOS:
          _currentSimulation = new _CupertinoSimulation(
            position: position,
            velocity: velocity,
          );
          break;
      }
      // No platform specified
      _currentSimulation ??= new FrictionSimulation(_drag, position, velocity);

324
      return true;
325
    }
326 327

    return false;
328
  }
329

330
  @override
331 332 333
  String toString() {
    return 'ScrollSimulation(leadingExtent: $_leadingExtent, trailingExtent: $_trailingExtent)';
  }
334
}