scroll_behavior.dart 11 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/foundation.dart';
8
import 'package:flutter/physics.dart';
9
import 'package:meta/meta.dart';
10

11
import 'scroll_simulation.dart';
12

13
export 'package:flutter/foundation.dart' show TargetPlatform;
14 15 16 17

Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
  return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
}
18

19 20 21
// 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
22
// be called FooScrollBehavior. See https://github.com/flutter/flutter/issues/5281
23

Hans Muller's avatar
Hans Muller committed
24
/// An interface for controlling the behavior of scrollable widgets.
Hixie's avatar
Hixie committed
25 26 27 28
///
/// 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> {
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  ///
  /// The [platform] must not be null.
  const ScrollBehavior({
    @required this.platform
  });

  /// The platform for which physics constants should be approximated.
  ///
  /// This is what makes flings go further on iOS than Android.
  ///
  /// Must not be null.
  final TargetPlatform platform;

Florian Loitsch's avatar
Florian Loitsch committed
44 45 46
  /// Returns a simulation that propels the scrollOffset.
  ///
  /// This function is called when a drag gesture ends.
Hixie's avatar
Hixie committed
47
  ///
48
  /// Returns `null` if the behavior is to do nothing.
49
  Simulation createScrollSimulation(T position, U velocity) => null;
50

51
  /// Returns an animation that ends at the snap offset.
Florian Loitsch's avatar
Florian Loitsch committed
52
  ///
Hixie's avatar
Hixie committed
53 54 55
  /// This function is called when a drag gesture ends and a
  /// [SnapOffsetCallback] is specified for the scrollable.
  ///
56
  /// Returns `null` if the behavior is to do nothing.
Hixie's avatar
Hixie committed
57
  Simulation createSnapScrollSimulation(T startOffset, T endOffset, U startVelocity, U endVelocity) => null;
58

Florian Loitsch's avatar
Florian Loitsch committed
59
  /// Returns the scroll offset to use when the user attempts to scroll
Hans Muller's avatar
Hans Muller committed
60
  /// from the given offset by the given delta.
Hixie's avatar
Hixie committed
61
  T applyCurve(T scrollOffset, T scrollDelta) => scrollOffset;
62

63
  /// Whether this scroll behavior currently permits scrolling.
64
  bool get isScrollable => true;
65

66
  @override
67 68 69 70 71
  String toString() {
    List<String> description = <String>[];
    debugFillDescription(description);
    return '$runtimeType(${description.join("; ")})';
  }
72 73 74 75 76 77

  /// 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
78 79 80
  void debugFillDescription(List<String> description) {
    description.add(isScrollable ? 'scrollable' : 'not scrollable');
  }
81 82
}

Hixie's avatar
Hixie committed
83 84 85
/// 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> {
86
  /// Creates a scroll behavior for a scrollable widget with linear extent.
87 88
  /// We start with an INFINITE contentExtent so that we don't accidentally
  /// clamp a scrollOffset until we receive an accurate value in updateExtents.
89 90 91 92 93 94 95 96 97
  ///
  /// The extents and the [platform] must not be null.
  ExtentScrollBehavior({
    double contentExtent: double.INFINITY,
    double containerExtent: 0.0,
    @required TargetPlatform platform
  }) : _contentExtent = contentExtent,
       _containerExtent = containerExtent,
       super(platform: platform);
98

Hans Muller's avatar
Hans Muller committed
99
  /// The linear extent of the content inside the scrollable widget.
100
  double get contentExtent => _contentExtent;
101
  double _contentExtent;
102

Hans Muller's avatar
Hans Muller committed
103
  /// The linear extent of the exterior of the scrollable widget.
104
  double get containerExtent => _containerExtent;
105
  double _containerExtent;
106

Florian Loitsch's avatar
Florian Loitsch committed
107 108 109
  /// Updates either content or container extent (or both)
  ///
  /// Returns the new scroll offset of the widget after the change in extent.
110
  ///
Florian Loitsch's avatar
Florian Loitsch committed
111
  /// The [scrollOffset] parameter is the scroll offset of the widget before the
112
  /// change in extent.
113
  double updateExtents({
114 115
    double contentExtent,
    double containerExtent,
116 117
    double scrollOffset: 0.0
  }) {
Hixie's avatar
Hixie committed
118
    assert(minScrollOffset <= maxScrollOffset);
119 120 121 122
    if (contentExtent != null)
      _contentExtent = contentExtent;
    if (containerExtent != null)
      _containerExtent = containerExtent;
123
    return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
124 125
  }

Hans Muller's avatar
Hans Muller committed
126
  /// The minimum value the scroll offset can obtain.
127
  double get minScrollOffset;
128

Hans Muller's avatar
Hans Muller committed
129
  /// The maximum value the scroll offset can obtain.
130
  double get maxScrollOffset;
131

132
  @override
133 134 135
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('content: ${contentExtent.toStringAsFixed(1)}');
136
    description.add('container: ${containerExtent.toStringAsFixed(1)}');
137 138
    description.add('range: ${minScrollOffset?.toStringAsFixed(1)} .. ${maxScrollOffset?.toStringAsFixed(1)}');
  }
139 140
}

Florian Loitsch's avatar
Florian Loitsch committed
141
/// A scroll behavior that prevents the user from exceeding scroll bounds.
142
class BoundedBehavior extends ExtentScrollBehavior {
143
  /// Creates a scroll behavior that does not overscroll.
144
  BoundedBehavior({
145
    double contentExtent: double.INFINITY,
146
    double containerExtent: 0.0,
147 148
    double minScrollOffset: 0.0,
    @required TargetPlatform platform
149
  }) : _minScrollOffset = minScrollOffset,
150 151 152 153 154
       super(
         contentExtent: contentExtent,
         containerExtent: containerExtent,
         platform: platform
       );
155

156 157
  double _minScrollOffset;

158
  @override
159 160 161 162 163 164
  double updateExtents({
    double contentExtent,
    double containerExtent,
    double minScrollOffset,
    double scrollOffset: 0.0
  }) {
Hixie's avatar
Hixie committed
165
    if (minScrollOffset != null) {
166
      _minScrollOffset = minScrollOffset;
Hixie's avatar
Hixie committed
167 168
      assert(minScrollOffset <= maxScrollOffset);
    }
169 170 171 172 173 174 175
    return super.updateExtents(
      contentExtent: contentExtent,
      containerExtent: containerExtent,
      scrollOffset: scrollOffset
    );
  }

176
  @override
177
  double get minScrollOffset => _minScrollOffset;
178 179

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

182
  @override
183
  double applyCurve(double scrollOffset, double scrollDelta) {
184
    return (scrollOffset + scrollDelta).clamp(minScrollOffset, maxScrollOffset);
185 186 187
  }
}

188
/// A scroll behavior that does not prevent the user from exceeding scroll bounds.
189
class UnboundedBehavior extends ExtentScrollBehavior {
190
  /// Creates a scroll behavior with no scrolling limits.
191 192 193 194 195 196 197 198 199
  UnboundedBehavior({
    double contentExtent: double.INFINITY,
    double containerExtent: 0.0,
    @required TargetPlatform platform
  }) : super(
    contentExtent: contentExtent,
    containerExtent: containerExtent,
    platform: platform
  );
200

201
  @override
202
  Simulation createScrollSimulation(double position, double velocity) {
203 204 205 206 207 208
    return new ScrollSimulation(
      position: position,
      velocity: velocity,
      leadingExtent: double.NEGATIVE_INFINITY,
      trailingExtent: double.INFINITY,
      platform: platform,
209 210 211
    );
  }

212
  @override
Hans Muller's avatar
Hans Muller committed
213 214
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
    return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
215 216
  }

217
  @override
218
  double get minScrollOffset => double.NEGATIVE_INFINITY;
219 220

  @override
221 222
  double get maxScrollOffset => double.INFINITY;

223
  @override
224 225 226 227 228
  double applyCurve(double scrollOffset, double scrollDelta) {
    return scrollOffset + scrollDelta;
  }
}

Hans Muller's avatar
Hans Muller committed
229
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
230
class OverscrollBehavior extends BoundedBehavior {
231
  /// Creates a scroll behavior that resists, but does not prevent, scrolling beyond its limits.
232 233 234 235 236 237 238 239 240 241 242
  OverscrollBehavior({
    double contentExtent: double.INFINITY,
    double containerExtent: 0.0,
    double minScrollOffset: 0.0,
    @required TargetPlatform platform
  }) : super(
    contentExtent: contentExtent,
    containerExtent: containerExtent,
    minScrollOffset: minScrollOffset,
    platform: platform
  );
243

244
  @override
245
  Simulation createScrollSimulation(double position, double velocity) {
246 247 248 249 250 251 252
    return new ScrollSimulation(
      position: position,
      velocity: velocity,
      leadingExtent: minScrollOffset,
      trailingExtent: maxScrollOffset,
      platform: platform,
    );
253 254
  }

255
  @override
Hans Muller's avatar
Hans Muller committed
256 257
  Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
    return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
258 259
  }

260
  @override
261 262 263 264 265 266 267 268
  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.
269 270
    if (newScrollOffset < minScrollOffset) {
      newScrollOffset -= (newScrollOffset - math.min(minScrollOffset, scrollOffset)) / 2.0;
271 272 273 274 275 276
    } else if (newScrollOffset > maxScrollOffset) {
      newScrollOffset -= (newScrollOffset - math.max(maxScrollOffset, scrollOffset)) / 2.0;
    }
    return newScrollOffset;
  }
}
277

Hans Muller's avatar
Hans Muller committed
278
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
279
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
280
  /// Creates a scroll behavior that allows overscrolling only when some amount of scrolling is already possible.
281 282 283 284 285 286 287 288 289 290 291
  OverscrollWhenScrollableBehavior({
    double contentExtent: double.INFINITY,
    double containerExtent: 0.0,
    double minScrollOffset: 0.0,
    @required TargetPlatform platform
  }) : super(
    contentExtent: contentExtent,
    containerExtent: containerExtent,
    minScrollOffset: minScrollOffset,
    platform: platform
  );
292

293
  @override
294
  bool get isScrollable => contentExtent > containerExtent;
295

296
  @override
297
  Simulation createScrollSimulation(double position, double velocity) {
298
    if ((isScrollable && velocity.abs() > 0) || position < minScrollOffset || position > maxScrollOffset) {
299 300 301 302 303
      // 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;
304
      return super.createScrollSimulation(position, velocity);
305
    }
306 307 308
    return null;
  }

309
  @override
310 311 312 313 314 315
  double applyCurve(double scrollOffset, double scrollDelta) {
    if (isScrollable)
      return super.applyCurve(scrollOffset, scrollDelta);
    return minScrollOffset;
  }
}