scroll_notification.dart 12.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';

8
import 'framework.dart';
9
import 'notification_listener.dart';
10
import 'scroll_metrics.dart';
11

12 13 14
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
/// have bubbled through.
///
Adam Barth's avatar
Adam Barth committed
15
/// This is used by [ScrollNotification] and [OverscrollIndicatorNotification].
16
mixin ViewportNotificationMixin on Notification {
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
  /// 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
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('depth: $depth (${ depth == 0 ? "local" : "remote"})');
  }
}

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
/// A mixin that allows [Element]s containing [Viewport] like widgets to correctly
/// modify the notification depth of a [ViewportNotificationMixin].
///
/// See also:
///   * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes
///     this in.
mixin ViewportElementMixin  on NotifiableElementMixin {
  @override
  bool onNotification(Notification notification) {
    if (notification is ViewportNotificationMixin) {
      notification._depth += 1;
    }
    return false;
  }
}

50 51 52 53 54 55 56 57 58 59 60 61 62 63
/// 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,
64
///    which indicate that the user has changed the direction in which they are
65 66 67
///    scrolling.
///  * A [ScrollEndNotification], which indicates that the widget has stopped
///    scrolling.
68
///  * A [UserScrollNotification], with a [UserScrollNotification.direction] of
69 70 71 72 73 74 75
///    [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.
76 77 78 79 80 81 82 83 84 85
///
/// 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.
///
86 87 88 89 90 91 92 93 94 95 96 97 98
/// {@tool dartpad}
/// This sample shows the difference between using a [ScrollController] or a
/// [NotificationListener] of type [ScrollNotification] to listen to scrolling
/// activities. Toggling the [Radio] button switches between the two.
/// Using a [ScrollNotification] will provide details about the scrolling
/// activity, along with the metrics of the [ScrollPosition], but not the scroll
/// position object itself. By listening with a [ScrollController], the position
/// object is directly accessible.
/// Both of these types of notifications are only triggered by scrolling.
///
/// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart **
/// {@end-tool}
///
99
/// To drive layout based on the scroll position, consider listening to the
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
/// [ScrollPosition] directly (or indirectly via a [ScrollController]). This
/// will not notify when the [ScrollMetrics] of a given scroll position changes,
/// such as when the window is resized, changing the dimensions of the
/// [Viewport]. In order to listen to changes in scroll metrics, use a
/// [NotificationListener] of type [ScrollMetricsNotification].
/// This type of notification differs from [ScrollNotification], as it is not
/// associated with the activity of scrolling, but rather the dimensions of
/// the scrollable area.
///
/// {@tool dartpad}
/// This sample shows how a [ScrollMetricsNotification] is dispatched when
/// the `windowSize` is changed. Press the floating action button to increase
/// the scrollable window's size.
///
/// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart **
/// {@end-tool}
///
Adam Barth's avatar
Adam Barth committed
117
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
118
  /// Initializes fields for subclasses.
Adam Barth's avatar
Adam Barth committed
119
  ScrollNotification({
120 121
    required this.metrics,
    required this.context,
122
  });
123

124
  /// A description of a [Scrollable]'s contents, useful for modeling the state
125
  /// of its viewport.
126
  final ScrollMetrics metrics;
127

128
  /// The build context of the widget that fired this notification.
129 130 131
  ///
  /// This can be used to find the scrollable's render objects to determine the
  /// size of the viewport, for instance.
132
  final BuildContext? context;
133 134 135 136

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
137
    description.add('$metrics');
138 139 140
  }
}

141 142 143 144 145 146
/// 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
147
class ScrollStartNotification extends ScrollNotification {
148
  /// Creates a notification that a [Scrollable] widget has started scrolling.
149
  ScrollStartNotification({
150 151
    required super.metrics,
    required super.context,
152
    this.dragDetails,
153
  });
154

155 156 157 158
  /// If the [Scrollable] started scrolling because of a drag, the details about
  /// that drag start.
  ///
  /// Otherwise, null.
159
  final DragStartDetails? dragDetails;
160 161 162 163

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

170 171 172 173 174 175 176 177
/// 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
178
class ScrollUpdateNotification extends ScrollNotification {
179 180
  /// Creates a notification that a [Scrollable] widget has changed its scroll
  /// position.
181
  ScrollUpdateNotification({
182 183
    required super.metrics,
    required BuildContext super.context,
184 185
    this.dragDetails,
    this.scrollDelta,
186
    int? depth,
187
  }) {
188 189 190 191
    if (depth != null) {
      _depth = depth;
    }
  }
192

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

Adam Barth's avatar
Adam Barth committed
199
  /// The distance by which the [Scrollable] was scrolled, in logical pixels.
200
  final double? scrollDelta;
201 202 203 204 205

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

212 213 214 215 216 217 218 219 220
/// 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
221
class OverscrollNotification extends ScrollNotification {
222 223
  /// Creates a notification that a [Scrollable] widget has changed its scroll
  /// position outside of its scroll bounds.
224
  OverscrollNotification({
225 226
    required super.metrics,
    required BuildContext super.context,
227
    this.dragDetails,
228
    required this.overscroll,
229
    this.velocity = 0.0,
230 231
  }) : assert(overscroll.isFinite),
       assert(overscroll != 0.0);
232

233 234 235 236
  /// If the [Scrollable] overscrolled because of a drag, the details about that
  /// drag update.
  ///
  /// Otherwise, null.
237
  final DragUpdateDetails? dragDetails;
238

Adam Barth's avatar
Adam Barth committed
239
  /// The number of logical pixels that the [Scrollable] avoided scrolling.
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
  ///
  /// 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)}');
258
    if (dragDetails != null) {
259
      description.add('$dragDetails');
260
    }
261 262 263
  }
}

264 265 266 267 268 269
/// 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
270
class ScrollEndNotification extends ScrollNotification {
271
  /// Creates a notification that a [Scrollable] widget has stopped scrolling.
272
  ScrollEndNotification({
273 274
    required super.metrics,
    required BuildContext super.context,
275
    this.dragDetails,
276
  });
277

278 279 280 281 282 283 284 285
  /// 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
286
  /// be null. If the residual velocity is too small to trigger ballistic
287 288
  /// scrolling, then the [ScrollEndNotification] will be dispatched immediately
  /// and [dragDetails] will be non-null.
289
  final DragEndDetails? dragDetails;
290 291 292 293

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

300 301 302 303 304 305 306
/// A notification that the user has changed the [ScrollDirection] in which they
/// are scrolling, or have stopped scrolling.
///
/// For the direction that the [ScrollView] is oriented to, and the direction
/// contents are being laid out in, see [AxisDirection] & [GrowthDirection].
///
/// {@macro flutter.rendering.ScrollDirection.sample}
307 308 309 310
///
/// See also:
///
///  * [ScrollNotification], which describes the notification lifecycle.
Adam Barth's avatar
Adam Barth committed
311
class UserScrollNotification extends ScrollNotification {
312 313
  /// Creates a notification that the user has changed the direction in which
  /// they are scrolling.
314
  UserScrollNotification({
315 316
    required super.metrics,
    required BuildContext super.context,
317
    required this.direction,
318
  });
319

320
  /// The direction in which the user is scrolling.
321 322 323 324 325 326 327
  ///
  /// This does not represent the current [AxisDirection] or [GrowthDirection]
  /// of the [Viewport], which respectively represent the direction that the
  /// scroll offset is increasing in, and the direction that contents are being
  /// laid out in.
  ///
  /// {@macro flutter.rendering.ScrollDirection.sample}
328 329 330 331 332 333 334 335
  final ScrollDirection direction;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('direction: $direction');
  }
}
336 337 338

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

341
/// A [ScrollNotificationPredicate] that checks whether
342
/// `notification.depth == 0`, which means that the notification did not bubble
343 344 345 346
/// through any intervening scrolling widgets.
bool defaultScrollNotificationPredicate(ScrollNotification notification) {
  return notification.depth == 0;
}