scroll_notification.dart 10.8 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 6
// @dart = 2.8

7 8 9
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';

10
import 'framework.dart';
11
import 'notification_listener.dart';
12
import 'scroll_metrics.dart';
13

14 15 16
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
/// have bubbled through.
///
Adam Barth's avatar
Adam Barth committed
17
/// This is used by [ScrollNotification] and [OverscrollIndicatorNotification].
18
mixin ViewportNotificationMixin on Notification {
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
  /// The number of viewports that this notification has bubbled through.
  ///
  /// Typically listeners only respond to notifications with a [depth] of zero.
  ///
  /// Specifically, this is the number of [Widget]s representing
  /// [RenderAbstractViewport] render objects through which this notification
  /// has bubbled.
  int get depth => _depth;
  int _depth = 0;

  @override
  bool visitAncestor(Element element) {
    if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport)
      _depth += 1;
    return super.visitAncestor(element);
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('depth: $depth (${ depth == 0 ? "local" : "remote"})');
  }
}

43 44 45 46 47 48 49 50 51 52 53 54 55 56
/// A [Notification] related to scrolling.
///
/// [Scrollable] widgets notify their ancestors about scrolling-related changes.
/// The notifications have the following lifecycle:
///
///  * A [ScrollStartNotification], which indicates that the widget has started
///    scrolling.
///  * Zero or more [ScrollUpdateNotification]s, which indicate that the widget
///    has changed its scroll position, mixed with zero or more
///    [OverscrollNotification]s, which indicate that the widget has not changed
///    its scroll position because the change would have caused its scroll
///    position to go outside its scroll bounds.
///  * Interspersed with the [ScrollUpdateNotification]s and
///    [OverscrollNotification]s are zero or more [UserScrollNotification]s,
57
///    which indicate that the user has changed the direction in which they are
58 59 60
///    scrolling.
///  * A [ScrollEndNotification], which indicates that the widget has stopped
///    scrolling.
61
///  * A [UserScrollNotification], with a [UserScrollNotification.direction] of
62 63 64 65 66 67 68
///    [ScrollDirection.idle].
///
/// Notifications bubble up through the tree, which means a given
/// [NotificationListener] will receive notifications for all descendant
/// [Scrollable] widgets. To focus on notifications from the nearest
/// [Scrollable] descendant, check that the [depth] property of the notification
/// is zero.
69 70 71 72 73 74 75 76 77 78 79 80
///
/// When a scroll notification is received by a [NotificationListener], the
/// listener will have already completed build and layout, and it is therefore
/// too late for that widget to call [State.setState]. Any attempt to adjust the
/// build or layout based on a scroll notification would result in a layout that
/// lagged one frame behind, which is a poor user experience. Scroll
/// notifications are therefore primarily useful for paint effects (since paint
/// happens after layout). The [GlowingOverscrollIndicator] and [Scrollbar]
/// widgets are examples of paint effects that use scroll notifications.
///
/// To drive layout based on the scroll position, consider listening to the
/// [ScrollPosition] directly (or indirectly via a [ScrollController]).
Adam Barth's avatar
Adam Barth committed
81
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
82
  /// Initializes fields for subclasses.
Adam Barth's avatar
Adam Barth committed
83
  ScrollNotification({
84 85 86
    @required this.metrics,
    @required this.context,
  });
87

88
  /// A description of a [Scrollable]'s contents, useful for modeling the state
89
  /// of its viewport.
90
  final ScrollMetrics metrics;
91

92
  /// The build context of the widget that fired this notification.
93 94 95 96 97 98 99 100
  ///
  /// This can be used to find the scrollable's render objects to determine the
  /// size of the viewport, for instance.
  final BuildContext context;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
101
    description.add('$metrics');
102 103 104
  }
}

105 106 107 108 109 110
/// A notification that a [Scrollable] widget has started scrolling.
///
/// See also:
///
///  * [ScrollEndNotification], which indicates that scrolling has stopped.
///  * [ScrollNotification], which describes the notification lifecycle.
Adam Barth's avatar
Adam Barth committed
111
class ScrollStartNotification extends ScrollNotification {
112
  /// Creates a notification that a [Scrollable] widget has started scrolling.
113
  ScrollStartNotification({
114 115
    @required ScrollMetrics metrics,
    @required BuildContext context,
116
    this.dragDetails,
117
  }) : super(metrics: metrics, context: context);
118

119 120 121 122
  /// If the [Scrollable] started scrolling because of a drag, the details about
  /// that drag start.
  ///
  /// Otherwise, null.
123 124 125 126 127 128 129 130 131 132
  final DragStartDetails dragDetails;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (dragDetails != null)
      description.add('$dragDetails');
  }
}

133 134 135 136 137 138 139 140
/// A notification that a [Scrollable] widget has changed its scroll position.
///
/// See also:
///
///  * [OverscrollNotification], which indicates that a [Scrollable] widget
///    has not changed its scroll position because the change would have caused
///    its scroll position to go outside its scroll bounds.
///  * [ScrollNotification], which describes the notification lifecycle.
Adam Barth's avatar
Adam Barth committed
141
class ScrollUpdateNotification extends ScrollNotification {
142 143
  /// Creates a notification that a [Scrollable] widget has changed its scroll
  /// position.
144
  ScrollUpdateNotification({
145 146
    @required ScrollMetrics metrics,
    @required BuildContext context,
147 148
    this.dragDetails,
    this.scrollDelta,
149
  }) : super(metrics: metrics, context: context);
150

151 152 153 154
  /// If the [Scrollable] changed its scroll position because of a drag, the
  /// details about that drag update.
  ///
  /// Otherwise, null.
155 156
  final DragUpdateDetails dragDetails;

Adam Barth's avatar
Adam Barth committed
157
  /// The distance by which the [Scrollable] was scrolled, in logical pixels.
158 159 160 161 162 163 164 165 166 167 168
  final double scrollDelta;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('scrollDelta: $scrollDelta');
    if (dragDetails != null)
      description.add('$dragDetails');
  }
}

169 170 171 172 173 174 175 176 177
/// A notification that a [Scrollable] widget has not changed its scroll position
/// because the change would have caused its scroll position to go outside of
/// its scroll bounds.
///
/// See also:
///
///  * [ScrollUpdateNotification], which indicates that a [Scrollable] widget
///    has changed its scroll position.
///  * [ScrollNotification], which describes the notification lifecycle.
Adam Barth's avatar
Adam Barth committed
178
class OverscrollNotification extends ScrollNotification {
179 180
  /// Creates a notification that a [Scrollable] widget has changed its scroll
  /// position outside of its scroll bounds.
181
  OverscrollNotification({
182 183
    @required ScrollMetrics metrics,
    @required BuildContext context,
184 185
    this.dragDetails,
    @required this.overscroll,
186
    this.velocity = 0.0,
187 188 189 190 191
  }) : assert(overscroll != null),
       assert(overscroll.isFinite),
       assert(overscroll != 0.0),
       assert(velocity != null),
       super(metrics: metrics, context: context);
192

193 194 195 196
  /// If the [Scrollable] overscrolled because of a drag, the details about that
  /// drag update.
  ///
  /// Otherwise, null.
197 198
  final DragUpdateDetails dragDetails;

Adam Barth's avatar
Adam Barth committed
199
  /// The number of logical pixels that the [Scrollable] avoided scrolling.
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
  ///
  /// This will be negative for overscroll on the "start" side and positive for
  /// overscroll on the "end" side.
  final double overscroll;

  /// The velocity at which the [ScrollPosition] was changing when this
  /// overscroll happened.
  ///
  /// This will typically be 0.0 for touch-driven overscrolls, and positive
  /// for overscrolls that happened from a [BallisticScrollActivity] or
  /// [DrivenScrollActivity].
  final double velocity;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('overscroll: ${overscroll.toStringAsFixed(1)}');
    description.add('velocity: ${velocity.toStringAsFixed(1)}');
    if (dragDetails != null)
      description.add('$dragDetails');
  }
}

223 224 225 226 227 228
/// A notification that a [Scrollable] widget has stopped scrolling.
///
/// See also:
///
///  * [ScrollStartNotification], which indicates that scrolling has started.
///  * [ScrollNotification], which describes the notification lifecycle.
Adam Barth's avatar
Adam Barth committed
229
class ScrollEndNotification extends ScrollNotification {
230
  /// Creates a notification that a [Scrollable] widget has stopped scrolling.
231
  ScrollEndNotification({
232 233
    @required ScrollMetrics metrics,
    @required BuildContext context,
234
    this.dragDetails,
235
  }) : super(metrics: metrics, context: context);
236

237 238 239 240 241 242 243 244
  /// If the [Scrollable] stopped scrolling because of a drag, the details about
  /// that drag end.
  ///
  /// Otherwise, null.
  ///
  /// If a drag ends with some residual velocity, a typical [ScrollPhysics] will
  /// start a ballistic scroll, which delays the [ScrollEndNotification] until
  /// the ballistic simulation completes, at which time [dragDetails] will
245
  /// be null. If the residual velocity is too small to trigger ballistic
246 247
  /// scrolling, then the [ScrollEndNotification] will be dispatched immediately
  /// and [dragDetails] will be non-null.
248 249 250 251 252 253 254 255 256 257
  final DragEndDetails dragDetails;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (dragDetails != null)
      description.add('$dragDetails');
  }
}

258 259 260 261 262 263
/// A notification that the user has changed the direction in which they are
/// scrolling.
///
/// See also:
///
///  * [ScrollNotification], which describes the notification lifecycle.
Adam Barth's avatar
Adam Barth committed
264
class UserScrollNotification extends ScrollNotification {
265 266
  /// Creates a notification that the user has changed the direction in which
  /// they are scrolling.
267
  UserScrollNotification({
268 269
    @required ScrollMetrics metrics,
    @required BuildContext context,
270
    this.direction,
271
  }) : super(metrics: metrics, context: context);
272

273
  /// The direction in which the user is scrolling.
274 275 276 277 278 279 280 281
  final ScrollDirection direction;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('direction: $direction');
  }
}
282 283 284

/// A predicate for [ScrollNotification], used to customize widgets that
/// listen to notifications from their children.
285
typedef ScrollNotificationPredicate = bool Function(ScrollNotification notification);
286

287
/// A [ScrollNotificationPredicate] that checks whether
288
/// `notification.depth == 0`, which means that the notification did not bubble
289 290 291 292
/// through any intervening scrolling widgets.
bool defaultScrollNotificationPredicate(ScrollNotification notification) {
  return notification.depth == 0;
}