viewport_offset.dart 11.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter 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
import 'package:flutter/animation.dart';
6 7 8 9 10 11 12 13
import 'package:flutter/foundation.dart';

/// The direction of a scroll, relative to the positive scroll offset axis given
/// by an [AxisDirection] and a [GrowthDirection].
///
/// This contrasts to [GrowthDirection] in that it has a third value, [idle],
/// for the case where no scroll is occurring.
///
14 15 16
/// This is used by [RenderSliverFloatingPersistentHeader] to only expand when
/// the user is scrolling in the same direction as the detected scroll offset
/// change.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
enum ScrollDirection {
  /// No scrolling is underway.
  idle,

  /// Scrolling is happening in the positive scroll offset direction.
  ///
  /// For example, for the [GrowthDirection.forward] part of a vertical
  /// [AxisDirection.down] list, this means the content is moving up, exposing
  /// lower content.
  forward,

  /// Scrolling is happening in the negative scroll offset direction.
  ///
  /// For example, for the [GrowthDirection.forward] part of a vertical
  /// [AxisDirection.down] list, this means the content is moving down, exposing
  /// earlier content.
  reverse,
}

36 37
/// Returns the opposite of the given [ScrollDirection].
///
38
/// Specifically, returns [ScrollDirection.reverse] for [ScrollDirection.forward]
39 40 41 42 43 44 45 46 47 48 49 50 51
/// (and vice versa) and returns [ScrollDirection.idle] for
/// [ScrollDirection.idle].
ScrollDirection flipScrollDirection(ScrollDirection direction) {
  switch (direction) {
    case ScrollDirection.idle:
      return ScrollDirection.idle;
    case ScrollDirection.forward:
      return ScrollDirection.reverse;
    case ScrollDirection.reverse:
      return ScrollDirection.forward;
  }
}

52 53 54 55 56 57
/// Which part of the content inside the viewport should be visible.
///
/// The [pixels] value determines the scroll offset that the viewport uses to
/// select which part of its content to display. As the user scrolls the
/// viewport, this value changes, which changes the content that is displayed.
///
58
/// This object is a [Listenable] that notifies its listeners when [pixels]
59
/// changes.
60 61 62 63 64 65
///
/// See also:
///
///  * [ScrollPosition], which is a commonly used concrete subclass.
///  * [RenderViewportBase], which is a render object that uses viewport
///    offsets.
66
abstract class ViewportOffset extends ChangeNotifier {
67 68 69
  /// Default constructor.
  ///
  /// Allows subclasses to construct this object directly.
70
  ViewportOffset();
71 72 73 74 75

  /// Creates a viewport offset with the given [pixels] value.
  ///
  /// The [pixels] value does not change unless the viewport issues a
  /// correction.
76
  factory ViewportOffset.fixed(double value) = _FixedViewportOffset;
77 78 79 80 81

  /// Creates a viewport offset with a [pixels] value of 0.0.
  ///
  /// The [pixels] value does not change unless the viewport issues a
  /// correction.
82 83 84 85 86 87 88
  factory ViewportOffset.zero() = _FixedViewportOffset.zero;

  /// The number of pixels to offset the children in the opposite of the axis direction.
  ///
  /// For example, if the axis direction is down, then the pixel value
  /// represents the number of logical pixels to move the children _up_ the
  /// screen. Similarly, if the axis direction is left, then the pixels value
89
  /// represents the number of logical pixels to move the children to _right_.
90 91 92
  ///
  /// This object notifies its listeners when this value changes (except when
  /// the value changes due to [correctBy]).
93 94
  double get pixels;

95 96 97
  /// Whether the [pixels] property is available.
  bool get hasPixels;

98 99
  /// Called when the viewport's extents are established.
  ///
Adam Barth's avatar
Adam Barth committed
100
  /// The argument is the dimension of the [RenderViewport] in the main axis
101 102 103
  /// (e.g. the height, for a vertical viewport).
  ///
  /// This may be called redundantly, with the same value, each frame. This is
Adam Barth's avatar
Adam Barth committed
104
  /// called during layout for the [RenderViewport]. If the viewport is
105 106 107 108 109 110 111 112
  /// configured to shrink-wrap its contents, it may be called several times,
  /// since the layout is repeated each time the scroll offset is corrected.
  ///
  /// If this is called, it is called before [applyContentDimensions]. If this
  /// is called, [applyContentDimensions] will be called soon afterwards in the
  /// same layout phase. If the viewport is not configured to shrink-wrap its
  /// contents, then this will only be called when the viewport recomputes its
  /// size (i.e. when its parent lays out), and not during normal scrolling.
113
  ///
114
  /// If applying the viewport dimensions changes the scroll offset, return
Adam Barth's avatar
Adam Barth committed
115
  /// false. Otherwise, return true. If you return false, the [RenderViewport]
116 117 118 119 120 121
  /// will be laid out again with the new scroll offset. This is expensive. (The
  /// return value is answering the question "did you accept these viewport
  /// dimensions unconditionally?"; if the new dimensions change the
  /// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
  /// be laid out again.)
  bool applyViewportDimension(double viewportDimension);
122 123 124 125

  /// Called when the viewport's content extents are established.
  ///
  /// The arguments are the minimum and maximum scroll extents respectively. The
126 127 128
  /// minimum will be equal to or less than the maximum. In the case of slivers,
  /// the minimum will be equal to or less than zero, the maximum will be equal
  /// to or greater than zero.
129 130 131 132 133 134 135 136
  ///
  /// The maximum scroll extent has the viewport dimension subtracted from it.
  /// For instance, if there is 100.0 pixels of scrollable content, and the
  /// viewport is 80.0 pixels high, then the minimum scroll extent will
  /// typically be 0.0 and the maximum scroll extent will typically be 20.0,
  /// because there's only 20.0 pixels of actual scroll slack.
  ///
  /// If applying the content dimensions changes the scroll offset, return
Adam Barth's avatar
Adam Barth committed
137
  /// false. Otherwise, return true. If you return false, the [RenderViewport]
138 139 140 141 142 143
  /// will be laid out again with the new scroll offset. This is expensive. (The
  /// return value is answering the question "did you accept these content
  /// dimensions unconditionally?"; if the new dimensions change the
  /// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
  /// be laid out again.)
  ///
Adam Barth's avatar
Adam Barth committed
144
  /// This is called at least once each time the [RenderViewport] is laid out,
145 146 147 148 149 150 151 152 153
  /// even if the values have not changed. It may be called many times if the
  /// scroll offset is corrected (if this returns false). This is always called
  /// after [applyViewportDimension], if that method is called.
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent);

  /// Apply a layout-time correction to the scroll offset.
  ///
  /// This method should change the [pixels] value by `correction`, but without
  /// calling [notifyListeners]. It is called during layout by the
Adam Barth's avatar
Adam Barth committed
154
  /// [RenderViewport], before [applyContentDimensions]. After this method is
155 156
  /// called, the layout will be recomputed and that may result in this method
  /// being called again, though this should be very rare.
157 158 159 160 161
  ///
  /// See also:
  ///
  ///  * [jumpTo], for also changing the scroll position when not in layout.
  ///    [jumpTo] applies the change immediately and notifies its listeners.
162 163
  void correctBy(double correction);

164
  /// Jumps [pixels] from its current value to the given value,
165
  /// without animation, and without checking if the new value is in range.
166 167 168 169 170
  ///
  /// See also:
  ///
  ///  * [correctBy], for changing the current offset in the middle of layout
  ///    and that defers the notification of its listeners until after layout.
171 172
  void jumpTo(double pixels);

173 174 175 176 177 178 179
  /// Animates [pixels] from its current value to the given value.
  ///
  /// The returned [Future] will complete when the animation ends, whether it
  /// completed successfully or whether it was interrupted prematurely.
  ///
  /// The duration must not be zero. To jump to a particular value without an
  /// animation, use [jumpTo].
180 181
  Future<void> animateTo(
    double to, {
182 183
    required Duration duration,
    required Curve curve,
184 185
  });

186 187 188 189 190 191 192
  /// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
  /// [animateTo] is called.
  ///
  /// If [animateTo] is called then [curve] defaults to [Curves.ease]. The
  /// [clamp] parameter is ignored by this stub implementation but subclasses
  /// like [ScrollPosition] handle it by adjusting [to] to prevent over or
  /// underscroll.
193 194
  Future<void> moveTo(
    double to, {
195 196 197
    Duration? duration,
    Curve? curve,
    bool? clamp,
198 199 200 201 202 203 204 205 206 207
  }) {
    assert(to != null);
    if (duration == null || duration == Duration.zero) {
      jumpTo(to);
      return Future<void>.value();
    } else {
      return animateTo(to, duration: duration, curve: curve ?? Curves.ease);
    }
  }

208
  /// The direction in which the user is trying to change [pixels], relative to
209
  /// the viewport's [RenderViewportBase.axisDirection].
210
  ///
211 212 213
  /// If the _user_ is not scrolling, this will return [ScrollDirection.idle]
  /// even if there is (for example) a [ScrollActivity] currently animating the
  /// position.
214
  ///
215 216 217 218 219
  /// This is exposed in [SliverConstraints.userScrollDirection], which is used
  /// by some slivers to determine how to react to a change in scroll offset.
  /// For example, [RenderSliverFloatingPersistentHeader] will only expand a
  /// floating app bar when the [userScrollDirection] is in the positive scroll
  /// offset direction.
220 221
  ScrollDirection get userScrollDirection;

222 223 224 225 226 227 228 229 230
  /// Whether a viewport is allowed to change [pixels] implicitly to respond to
  /// a call to [RenderObject.showOnScreen].
  ///
  /// [RenderObject.showOnScreen] is for example used to bring a text field
  /// fully on screen after it has received focus. This property controls
  /// whether the viewport associated with this offset is allowed to change the
  /// offset's [pixels] value to fulfill such a request.
  bool get allowImplicitScrolling;

231 232 233 234
  @override
  String toString() {
    final List<String> description = <String>[];
    debugFillDescription(description);
235
    return '${describeIdentity(this)}(${description.join(", ")})';
236 237
  }

238 239 240 241 242 243 244 245 246
  /// 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 [State] 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)`.
247 248
  @mustCallSuper
  void debugFillDescription(List<String> description) {
249 250 251
    if (hasPixels) {
      description.add('offset: ${pixels.toStringAsFixed(1)}');
    }
252 253 254 255 256 257 258 259 260 261 262 263
  }
}

class _FixedViewportOffset extends ViewportOffset {
  _FixedViewportOffset(this._pixels);
  _FixedViewportOffset.zero() : _pixels = 0.0;

  double _pixels;

  @override
  double get pixels => _pixels;

264 265 266
  @override
  bool get hasPixels => true;

267
  @override
268
  bool applyViewportDimension(double viewportDimension) => true;
269 270 271 272 273 274 275 276 277

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) => true;

  @override
  void correctBy(double correction) {
    _pixels += correction;
  }

278 279 280 281 282
  @override
  void jumpTo(double pixels) {
    // Do nothing, viewport is fixed.
  }

283
  @override
284 285
  Future<void> animateTo(
    double to, {
286 287
    required Duration duration,
    required Curve curve,
288
  }) async { }
289

290 291
  @override
  ScrollDirection get userScrollDirection => ScrollDirection.idle;
292 293 294

  @override
  bool get allowImplicitScrolling => false;
295
}