scroll_behavior.dart 7.62 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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 'package:newton/newton.dart';

const double _kSecondsPerMillisecond = 1000.0;
10
const double _kScrollDrag = 0.025;
11

Hans Muller's avatar
Hans Muller committed
12
/// An interface for controlling the behavior of scrollable widgets.
13
abstract class ScrollBehavior {
Florian Loitsch's avatar
Florian Loitsch committed
14 15 16
  /// Returns a simulation that propels the scrollOffset.
  ///
  /// This function is called when a drag gesture ends.
17
  Simulation createFlingScrollSimulation(double position, double velocity) => null;
18

19
  /// Returns an animation that ends at the snap offset.
Florian Loitsch's avatar
Florian Loitsch committed
20 21
  ///
  /// This function is called when a drag gesture ends and toSnapOffset is specified.
Hans Muller's avatar
Hans Muller committed
22
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) => null;
23

Florian Loitsch's avatar
Florian Loitsch committed
24
  /// Returns the scroll offset to use when the user attempts to scroll
Hans Muller's avatar
Hans Muller committed
25
  /// from the given offset by the given delta.
26
  double applyCurve(double scrollOffset, double scrollDelta);
27 28 29

  /// Whether this scroll behavior currently permits scrolling
  bool get isScrollable => true;
30 31
}

Florian Loitsch's avatar
Florian Loitsch committed
32
/// A scroll behavior for a scrollable widget with linear extent.
33 34 35
abstract class ExtentScrollBehavior extends ScrollBehavior {
  ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
    : _contentExtent = contentExtent, _containerExtent = containerExtent;
36

Hans Muller's avatar
Hans Muller committed
37
  /// The linear extent of the content inside the scrollable widget.
38
  double get contentExtent => _contentExtent;
39
  double _contentExtent;
40

Hans Muller's avatar
Hans Muller committed
41
  /// The linear extent of the exterior of the scrollable widget.
42
  double get containerExtent => _containerExtent;
43
  double _containerExtent;
44

Florian Loitsch's avatar
Florian Loitsch committed
45 46 47
  /// Updates either content or container extent (or both)
  ///
  /// Returns the new scroll offset of the widget after the change in extent.
48
  ///
Florian Loitsch's avatar
Florian Loitsch committed
49
  /// The [scrollOffset] parameter is the scroll offset of the widget before the
50
  /// change in extent.
51
  double updateExtents({
52 53
    double contentExtent,
    double containerExtent,
54 55
    double scrollOffset: 0.0
  }) {
56 57 58 59
    if (contentExtent != null)
      _contentExtent = contentExtent;
    if (containerExtent != null)
      _containerExtent = containerExtent;
60
    return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
61 62
  }

Hans Muller's avatar
Hans Muller committed
63
  /// The minimum value the scroll offset can obtain.
64
  double get minScrollOffset;
65

Hans Muller's avatar
Hans Muller committed
66
  /// The maximum value the scroll offset can obtain.
67 68 69
  double get maxScrollOffset;
}

Florian Loitsch's avatar
Florian Loitsch committed
70
/// A scroll behavior that prevents the user from exceeding scroll bounds.
71
class BoundedBehavior extends ExtentScrollBehavior {
72 73 74 75 76 77
  BoundedBehavior({
    double contentExtent: 0.0,
    double containerExtent: 0.0,
    double minScrollOffset: 0.0
  }) : _minScrollOffset = minScrollOffset,
       super(contentExtent: contentExtent, containerExtent: containerExtent);
78

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
  double _minScrollOffset;

  double updateExtents({
    double contentExtent,
    double containerExtent,
    double minScrollOffset,
    double scrollOffset: 0.0
  }) {
    if (minScrollOffset != null)
      _minScrollOffset = minScrollOffset;
    return super.updateExtents(
      contentExtent: contentExtent,
      containerExtent: containerExtent,
      scrollOffset: scrollOffset
    );
  }

  double get minScrollOffset => _minScrollOffset;
97
  double get maxScrollOffset => math.max(minScrollOffset, minScrollOffset + _contentExtent - _containerExtent);
98 99

  double applyCurve(double scrollOffset, double scrollDelta) {
100
    return (scrollOffset + scrollDelta).clamp(minScrollOffset, maxScrollOffset);
101 102 103
  }
}

Hans Muller's avatar
Hans Muller committed
104 105 106 107 108 109 110 111 112 113 114 115
Simulation _createFlingScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
  final double startVelocity = velocity * _kSecondsPerMillisecond;
  final SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
  return new ScrollSimulation(position, startVelocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
}

Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
  final double velocity = startVelocity * _kSecondsPerMillisecond;
  return new FrictionSimulation.through(startOffset, endOffset, velocity, endVelocity);
}

/// A scroll behavior that does not prevent the user from exeeding scroll bounds.
116 117 118 119
class UnboundedBehavior extends ExtentScrollBehavior {
  UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
    : super(contentExtent: contentExtent, containerExtent: containerExtent);

120
  Simulation createFlingScrollSimulation(double position, double velocity) {
121 122 123 124 125 126
    double velocityPerSecond = velocity * 1000.0;
    return new BoundedFrictionSimulation(
      _kScrollDrag, position, velocityPerSecond, double.NEGATIVE_INFINITY, double.INFINITY
    );
  }

Hans Muller's avatar
Hans Muller committed
127 128
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
    return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
129 130
  }

131 132 133 134 135 136 137 138
  double get minScrollOffset => double.NEGATIVE_INFINITY;
  double get maxScrollOffset => double.INFINITY;

  double applyCurve(double scrollOffset, double scrollDelta) {
    return scrollOffset + scrollDelta;
  }
}

Hans Muller's avatar
Hans Muller committed
139
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
140
class OverscrollBehavior extends BoundedBehavior {
141 142
  OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0, double minScrollOffset: 0.0 })
    : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
143

Hans Muller's avatar
Hans Muller committed
144 145
  Simulation createFlingScrollSimulation(double position, double velocity) {
    return _createFlingScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
146 147
  }

Hans Muller's avatar
Hans Muller committed
148 149
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
    return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
150 151 152 153 154 155 156 157 158 159
  }

  double applyCurve(double scrollOffset, double scrollDelta) {
    double newScrollOffset = scrollOffset + scrollDelta;
    // If we're overscrolling, we want move the scroll offset 2x
    // slower than we would otherwise. Therefore, we "rewind" the
    // newScrollOffset by half the amount that we moved it above.
    // Notice that we clamp the "old" value to 0.0 so that we only
    // reduce the portion of scrollDelta that's applied beyond 0.0. We
    // do similar things for overscroll in the other direction.
160 161
    if (newScrollOffset < minScrollOffset) {
      newScrollOffset -= (newScrollOffset - math.min(minScrollOffset, scrollOffset)) / 2.0;
162 163 164 165 166 167
    } else if (newScrollOffset > maxScrollOffset) {
      newScrollOffset -= (newScrollOffset - math.max(maxScrollOffset, scrollOffset)) / 2.0;
    }
    return newScrollOffset;
  }
}
168

Hans Muller's avatar
Hans Muller committed
169
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
170
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
171 172 173
  OverscrollWhenScrollableBehavior({ double contentExtent: 0.0, double containerExtent: 0.0, double minScrollOffset: 0.0 })
    : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);

174
  bool get isScrollable => contentExtent > containerExtent;
175

Hans Muller's avatar
Hans Muller committed
176
  Simulation createFlingScrollSimulation(double position, double velocity) {
177
    if (isScrollable || position < minScrollOffset || position > maxScrollOffset)
Hans Muller's avatar
Hans Muller committed
178
      return super.createFlingScrollSimulation(position, velocity);
179 180 181 182 183 184 185 186 187
    return null;
  }

  double applyCurve(double scrollOffset, double scrollDelta) {
    if (isScrollable)
      return super.applyCurve(scrollOffset, scrollDelta);
    return minScrollOffset;
  }
}