scroll_controller.dart 9.93 KB
Newer Older
1 2 3 4 5 6 7
// 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:async';

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

10 11
import 'scroll_context.dart';
import 'scroll_physics.dart';
12
import 'scroll_position.dart';
13
import 'scroll_position_with_single_context.dart';
14

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
/// Controls a scrollable widget.
///
/// Scroll controllers are typically stored as member variables in [State]
/// objects and are reused in each [State.build]. A single scroll controller can
/// be used to control multiple scrollable widgets, but some operations, such
/// as reading the scroll [offset], require the controller to be used with a
/// single scrollable widget.
///
/// A scroll controller creates a [ScrollPosition] to manage the state specific
/// to an individual [Scrollable] widget. To use a custom [ScrollPosition],
/// subclass [ScrollController] and override [createScrollPosition].
///
/// Typically used with [ListView], [GridView], [CustomScrollView].
///
/// See also:
///
///  * [ListView], [GridView], [CustomScrollView], which can be controlled by a
///    [ScrollController].
///  * [Scrollable], which is the lower-level widget that creates and associates
///    [ScrollPosition] objects with [ScrollController] objects.
///  * [PageController], which is an analogous object for controlling a
///    [PageView].
///  * [ScrollPosition], which manages the scroll offset for an individual
///    scrolling widget.
39
class ScrollController extends ChangeNotifier {
40 41 42
  /// Creates a controller for a scrollable widget.
  ///
  /// The [initialScrollOffset] must not be null.
43
  ScrollController({
44
    this.initialScrollOffset: 0.0,
45
    this.debugLabel,
46 47 48
  }) {
    assert(initialScrollOffset != null);
  }
49 50 51

  /// The initial value to use for [offset].
  ///
52 53
  /// New [ScrollPosition] objects that are created and attached to this
  /// controller will have their offset initialized to this value.
54 55
  ///
  /// Defaults to 0.0.
56 57
  final double initialScrollOffset;

58 59
  /// A label that is used in the [toString] output. Intended to aid with
  /// identifying scroll controller instances in debug output.
60 61
  final String debugLabel;

62 63 64 65 66 67
  /// The currently attached positions.
  ///
  /// This should not be mutated directly. [ScrollPosition] objects can be added
  /// and removed using [attach] and [detach].
  @protected
  Iterable<ScrollPosition> get positions => _positions;
68 69
  final List<ScrollPosition> _positions = <ScrollPosition>[];

70 71 72 73 74 75 76 77
  /// Whether any [ScrollPosition] objects have attached themselves to the
  /// [ScrollController] using the [attach] method.
  ///
  /// If this is false, then members that interact with the [ScrollPosition],
  /// such as [position], [offset], [animateTo], and [jumpTo], must not be
  /// called.
  bool get hasClients => _positions.isNotEmpty;

78 79 80 81
  /// Returns the attached [ScrollPosition], from which the actual scroll offset
  /// of the [ScrollView] can be obtained.
  ///
  /// Calling this is only valid when only a single position is attached.
82
  ScrollPosition get position {
83 84
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
85
    return _positions.single;
86 87
  }

88 89 90
  /// The current scroll offset of the scrollable widget.
  ///
  /// Requires the controller to be controlling exactly one scrollable widget.
91 92
  double get offset => position.pixels;

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 118 119 120
  /// Animates the position from its current value to the given value.
  ///
  /// Any active animation is canceled. If the user is currently scrolling, that
  /// action is canceled.
  ///
  /// The returned [Future] will complete when the animation ends, whether it
  /// completed successfully or whether it was interrupted prematurely.
  ///
  /// An animation will be interrupted whenever the user attempts to scroll
  /// manually, or whenever another activity is started, or whenever the
  /// animation reaches the edge of the viewport and attempts to overscroll. (If
  /// the [ScrollPosition] does not overscroll but instead allows scrolling
  /// beyond the extents, then going beyond the extents will not interrupt the
  /// animation.)
  ///
  /// The animation is indifferent to changes to the viewport or content
  /// dimensions.
  ///
  /// Once the animation has completed, the scroll position will attempt to
  /// begin a ballistic activity in case its value is not stable (for example,
  /// if it is scrolled beyond the extents and in that situation the scroll
  /// position would normally bounce back).
  ///
  /// The duration must not be zero. To jump to a particular value without an
  /// animation, use [jumpTo].
  Future<Null> animateTo(double offset, {
    @required Duration duration,
    @required Curve curve,
121 122
  }) {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
123
    final List<Future<Null>> animations = new List<Future<Null>>(_positions.length);
124
    for (int i = 0; i < _positions.length; i += 1)
125 126 127
      animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
    return Future.wait<Null>(animations).then((List<Null> _) => null);
  }
128 129 130 131 132 133 134 135 136 137 138 139 140

  /// Jumps the scroll position from its current value to the given value,
  /// without animation, and without checking if the new value is in range.
  ///
  /// Any active animation is canceled. If the user is currently scrolling, that
  /// action is canceled.
  ///
  /// If this method changes the scroll position, a sequence of start/update/end
  /// scroll notifications will be dispatched. No overscroll notifications can
  /// be generated by this method.
  ///
  /// Immediately after the jump, a ballistic activity is started, in case the
  /// value was out of range.
141 142
  void jumpTo(double value) {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
143 144
    for (ScrollPosition position in new List<ScrollPosition>.from(_positions))
      position.jumpTo(value);
145
  }
146 147 148 149 150 151 152 153

  /// Register the given position with this controller.
  ///
  /// After this function returns, the [animateTo] and [jumpTo] methods on this
  /// controller will manipulate the given position.
  void attach(ScrollPosition position) {
    assert(!_positions.contains(position));
    _positions.add(position);
154
    position.addListener(notifyListeners);
155 156 157 158 159 160 161 162
  }

  /// Unregister the given position with this controller.
  ///
  /// After this function returns, the [animateTo] and [jumpTo] methods on this
  /// controller will not manipulate the given position.
  void detach(ScrollPosition position) {
    assert(_positions.contains(position));
163
    position.removeListener(notifyListeners);
164 165
    _positions.remove(position);
  }
166

167 168 169 170 171 172 173
  @override
  void dispose() {
    for (ScrollPosition position in _positions)
      position.removeListener(notifyListeners);
    super.dispose();
  }

174 175 176 177 178 179 180 181 182
  /// Creates a [ScrollPosition] for use by a [Scrollable] widget.
  ///
  /// Subclasses can override this function to customize the [ScrollPosition]
  /// used by the scrollable widgets they control. For example, [PageController]
  /// overrides this function to return a page-oriented scroll position
  /// subclass that keeps the same page visible when the scrollable widget
  /// resizes.
  ///
  /// By default, returns a [ScrollPositionWithSingleContext].
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
  ///
  /// The arguments are generally passed to the [ScrollPosition] being created:
  ///
  ///  * `physics`: An instance of [ScrollPhysics] that determines how the
  ///    [ScrollPosition] should react to user interactions, how it should
  ///    simulate scrolling when released or flung, etc. The value will not be
  ///    null. It typically comes from the [ScrollView] or other widget that
  ///    creates the [Scrollable], or, if none was provided, from the ambient
  ///    [ScrollConfiguration].
  ///  * `context`: A [ScrollContext] used for communicating with the object
  ///    that is to own the [ScrollPosition] (typically, this is the
  ///    [Scrollable] itself).
  ///  * `oldPosition`: If this is not the first time a [ScrollPosition] has
  ///    been created for this [Scrollable], this will be the previous instance.
  ///    This is used when the environment has changed and the [Scrollable]
  ///    needs to recreate the [ScrollPosition] object. It is null the first
  ///    time the [ScrollPosition] is created.
 ScrollPosition createScrollPosition(
201 202 203 204 205
    ScrollPhysics physics,
    ScrollContext context,
    ScrollPosition oldPosition,
  ) {
    return new ScrollPositionWithSingleContext(
206
      physics: physics,
207
      context: context,
208
      initialPixels: initialScrollOffset,
209
      oldPosition: oldPosition,
210
      debugLabel: debugLabel,
211 212
    );
  }
213 214 215

  @override
  String toString() {
216 217 218 219 220
    final List<String> description = <String>[];
    debugFillDescription(description);
    return '$runtimeType#$hashCode(${description.join(", ")})';
  }

221 222 223 224 225 226 227 228 229
  /// Add additional information to the given description for use by [toString].
  ///
  /// This method makes it easier for subclasses to coordinate to provide a
  /// high-quality [toString] implementation. The [toString] implementation on
  /// the [ScrollController] base class calls [debugFillDescription] to collect
  /// useful information from subclasses to incorporate into its return value.
  ///
  /// If you override this, make sure to start your method with a call to
  /// `super.debugFillDescription(description)`.
230 231
  @mustCallSuper
  void debugFillDescription(List<String> description) {
232 233
    if (debugLabel != null)
      description.add(debugLabel);
234
    if (initialScrollOffset != 0.0)
235
      description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
236
    if (_positions.isEmpty) {
237
      description.add('no clients');
238
    } else if (_positions.length == 1) {
239 240
      // Don't actually list the client itself, since its toString may refer to us.
      description.add('one client, offset ${offset.toStringAsFixed(1)}');
241
    } else {
242
      description.add('${_positions.length} clients');
243 244
    }
  }
245
}