scroll_behavior.dart 9.88 KB
Newer Older
1 2 3 4 5 6
// 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;

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

10
const double _kScrollDrag = 0.025;
11

12 13 14 15 16
// TODO(hansmuller): Simplify these classes. We're no longer using the ScrollBehavior<T, U>
// base class directly. Only LazyBlock uses BoundedBehavior's updateExtents minScrollOffset
// parameter; simpler to move that into ExtentScrollBehavior.  All of the classes should
// be called FooScrollBehavior.

Hans Muller's avatar
Hans Muller committed
17
/// An interface for controlling the behavior of scrollable widgets.
Hixie's avatar
Hixie committed
18 19 20 21
///
/// The type argument T is the type that describes the scroll offset.
/// The type argument U is the type that describes the scroll velocity.
abstract class ScrollBehavior<T, U> {
Florian Loitsch's avatar
Florian Loitsch committed
22 23 24
  /// Returns a simulation that propels the scrollOffset.
  ///
  /// This function is called when a drag gesture ends.
Hixie's avatar
Hixie committed
25
  ///
26
  /// Returns `null` if the behavior is to do nothing.
27
  Simulation createScrollSimulation(T position, U velocity) => null;
28

29
  /// Returns an animation that ends at the snap offset.
Florian Loitsch's avatar
Florian Loitsch committed
30
  ///
Hixie's avatar
Hixie committed
31 32 33
  /// This function is called when a drag gesture ends and a
  /// [SnapOffsetCallback] is specified for the scrollable.
  ///
34
  /// Returns `null` if the behavior is to do nothing.
Hixie's avatar
Hixie committed
35
  Simulation createSnapScrollSimulation(T startOffset, T endOffset, U startVelocity, U endVelocity) => null;
36

Florian Loitsch's avatar
Florian Loitsch committed
37
  /// Returns the scroll offset to use when the user attempts to scroll
Hans Muller's avatar
Hans Muller committed
38
  /// from the given offset by the given delta.
Hixie's avatar
Hixie committed
39
  T applyCurve(T scrollOffset, T scrollDelta) => scrollOffset;
40 41 42

  /// Whether this scroll behavior currently permits scrolling
  bool get isScrollable => true;
43

44
  @override
45 46 47 48 49
  String toString() {
    List<String> description = <String>[];
    debugFillDescription(description);
    return '$runtimeType(${description.join("; ")})';
  }
50 51 52 53 54 55

  /// Accumulates a list of strings describing the current node's fields, one
  /// field per string. Subclasses should override this to have their
  /// information included in [toString].
  @protected
  @mustCallSuper
56 57 58
  void debugFillDescription(List<String> description) {
    description.add(isScrollable ? 'scrollable' : 'not scrollable');
  }
59 60
}

Hixie's avatar
Hixie committed
61 62 63
/// A scroll behavior for a scrollable widget with linear extent (i.e.
/// that only scrolls along one axis).
abstract class ExtentScrollBehavior extends ScrollBehavior<double, double> {
64
  /// Creates a scroll behavior for a scrollable widget with linear extent.
65 66
  ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
    : _contentExtent = contentExtent, _containerExtent = containerExtent;
67

Hans Muller's avatar
Hans Muller committed
68
  /// The linear extent of the content inside the scrollable widget.
69
  double get contentExtent => _contentExtent;
70
  double _contentExtent;
71

Hans Muller's avatar
Hans Muller committed
72
  /// The linear extent of the exterior of the scrollable widget.
73
  double get containerExtent => _containerExtent;
74
  double _containerExtent;
75

Florian Loitsch's avatar
Florian Loitsch committed
76 77 78
  /// Updates either content or container extent (or both)
  ///
  /// Returns the new scroll offset of the widget after the change in extent.
79
  ///
Florian Loitsch's avatar
Florian Loitsch committed
80
  /// The [scrollOffset] parameter is the scroll offset of the widget before the
81
  /// change in extent.
82
  double updateExtents({
83 84
    double contentExtent,
    double containerExtent,
85 86
    double scrollOffset: 0.0
  }) {
Hixie's avatar
Hixie committed
87
    assert(minScrollOffset <= maxScrollOffset);
88 89 90 91
    if (contentExtent != null)
      _contentExtent = contentExtent;
    if (containerExtent != null)
      _containerExtent = containerExtent;
92
    return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
93 94
  }

Hans Muller's avatar
Hans Muller committed
95
  /// The minimum value the scroll offset can obtain.
96
  double get minScrollOffset;
97

Hans Muller's avatar
Hans Muller committed
98
  /// The maximum value the scroll offset can obtain.
99
  double get maxScrollOffset;
100

101
  @override
102 103 104
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('content: ${contentExtent.toStringAsFixed(1)}');
105
    description.add('container: ${containerExtent.toStringAsFixed(1)}');
106 107
    description.add('range: ${minScrollOffset?.toStringAsFixed(1)} .. ${maxScrollOffset?.toStringAsFixed(1)}');
  }
108 109
}

Florian Loitsch's avatar
Florian Loitsch committed
110
/// A scroll behavior that prevents the user from exceeding scroll bounds.
111
class BoundedBehavior extends ExtentScrollBehavior {
112
  /// Creates a scroll behavior that does not overscroll.
113 114 115 116 117 118
  BoundedBehavior({
    double contentExtent: 0.0,
    double containerExtent: 0.0,
    double minScrollOffset: 0.0
  }) : _minScrollOffset = minScrollOffset,
       super(contentExtent: contentExtent, containerExtent: containerExtent);
119

120 121
  double _minScrollOffset;

122
  @override
123 124 125 126 127 128
  double updateExtents({
    double contentExtent,
    double containerExtent,
    double minScrollOffset,
    double scrollOffset: 0.0
  }) {
Hixie's avatar
Hixie committed
129
    if (minScrollOffset != null) {
130
      _minScrollOffset = minScrollOffset;
Hixie's avatar
Hixie committed
131 132
      assert(minScrollOffset <= maxScrollOffset);
    }
133 134 135 136 137 138 139
    return super.updateExtents(
      contentExtent: contentExtent,
      containerExtent: containerExtent,
      scrollOffset: scrollOffset
    );
  }

140
  @override
141
  double get minScrollOffset => _minScrollOffset;
142 143

  @override
144
  double get maxScrollOffset => math.max(minScrollOffset, minScrollOffset + _contentExtent - _containerExtent);
145

146
  @override
147
  double applyCurve(double scrollOffset, double scrollDelta) {
148
    return (scrollOffset + scrollDelta).clamp(minScrollOffset, maxScrollOffset);
149 150 151
  }
}

152
Simulation _createScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
Hans Muller's avatar
Hans Muller committed
153
  final SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
154
  return new ScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
Hans Muller's avatar
Hans Muller committed
155 156 157
}

Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
158
  return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
Hans Muller's avatar
Hans Muller committed
159 160 161
}

/// A scroll behavior that does not prevent the user from exeeding scroll bounds.
162
class UnboundedBehavior extends ExtentScrollBehavior {
163
  /// Creates a scroll behavior with no scrolling limits.
164 165 166
  UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
    : super(contentExtent: contentExtent, containerExtent: containerExtent);

167
  @override
168
  Simulation createScrollSimulation(double position, double velocity) {
169
    return new BoundedFrictionSimulation(
170
      _kScrollDrag, position, velocity, double.NEGATIVE_INFINITY, double.INFINITY
171 172 173
    );
  }

174
  @override
Hans Muller's avatar
Hans Muller committed
175 176
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
    return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
177 178
  }

179
  @override
180
  double get minScrollOffset => double.NEGATIVE_INFINITY;
181 182

  @override
183 184
  double get maxScrollOffset => double.INFINITY;

185
  @override
186 187 188 189 190
  double applyCurve(double scrollOffset, double scrollDelta) {
    return scrollOffset + scrollDelta;
  }
}

Hans Muller's avatar
Hans Muller committed
191
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
192
class OverscrollBehavior extends BoundedBehavior {
193
  /// Creates a scroll behavior that resists, but does not prevent, scrolling beyond its limits.
194 195
  OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0, double minScrollOffset: 0.0 })
    : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
196

197
  @override
198 199
  Simulation createScrollSimulation(double position, double velocity) {
    return _createScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
200 201
  }

202
  @override
Hans Muller's avatar
Hans Muller committed
203 204
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
    return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
205 206
  }

207
  @override
208 209 210 211 212 213 214 215
  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.
216 217
    if (newScrollOffset < minScrollOffset) {
      newScrollOffset -= (newScrollOffset - math.min(minScrollOffset, scrollOffset)) / 2.0;
218 219 220 221 222 223
    } else if (newScrollOffset > maxScrollOffset) {
      newScrollOffset -= (newScrollOffset - math.max(maxScrollOffset, scrollOffset)) / 2.0;
    }
    return newScrollOffset;
  }
}
224

Hans Muller's avatar
Hans Muller committed
225
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
226
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
227
  /// Creates a scroll behavior that allows overscrolling only when some amount of scrolling is already possible.
228 229 230
  OverscrollWhenScrollableBehavior({ double contentExtent: 0.0, double containerExtent: 0.0, double minScrollOffset: 0.0 })
    : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);

231
  @override
232
  bool get isScrollable => contentExtent > containerExtent;
233

234
  @override
235
  Simulation createScrollSimulation(double position, double velocity) {
236
    if ((isScrollable && velocity.abs() > 0) || position < minScrollOffset || position > maxScrollOffset) {
237 238 239 240 241
      // If the triggering gesture starts at or beyond the contentExtent's limits
      // then the simulation only serves to settle the scrollOffset back to its
      // minimum or maximum value.
      if (position < minScrollOffset || position > maxScrollOffset)
        velocity = 0.0;
242
      return super.createScrollSimulation(position, velocity);
243
    }
244 245 246
    return null;
  }

247
  @override
248 249 250 251 252 253
  double applyCurve(double scrollOffset, double scrollDelta) {
    if (isScrollable)
      return super.applyCurve(scrollOffset, scrollDelta);
    return minScrollOffset;
  }
}