scroll_notification.dart 11 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2016 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 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';

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

13 14 15
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
/// have bubbled through.
///
Adam Barth's avatar
Adam Barth committed
16
/// This is used by [ScrollNotification] and [OverscrollIndicatorNotification].
17
abstract class ViewportNotificationMixin extends Notification {
18 19 20 21
  // This class is intended to be used as a mixin, and should not be
  // extended directly.
  factory ViewportNotificationMixin._() => null;

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
  /// 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"})');
  }
}

46 47 48 49 50 51 52 53 54 55 56 57 58 59
/// 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,
60
///    which indicate that the user has changed the direction in which they are
61 62 63
///    scrolling.
///  * A [ScrollEndNotification], which indicates that the widget has stopped
///    scrolling.
64
///  * A [UserScrollNotification], with a [UserScrollNotification.direction] of
65 66 67 68 69 70 71
///    [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.
72 73 74 75 76 77 78 79 80 81 82 83
///
/// 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
84
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
85
  /// Initializes fields for subclasses.
Adam Barth's avatar
Adam Barth committed
86
  ScrollNotification({
87 88 89
    @required this.metrics,
    @required this.context,
  });
90

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

95
  /// The build context of the widget that fired this notification.
96 97 98 99 100 101 102 103
  ///
  /// 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);
104
    description.add('$metrics');
105 106 107
  }
}

108 109 110 111 112 113
/// 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
114
class ScrollStartNotification extends ScrollNotification {
115
  /// Creates a notification that a [Scrollable] widget has started scrolling.
116
  ScrollStartNotification({
117 118
    @required ScrollMetrics metrics,
    @required BuildContext context,
119
    this.dragDetails,
120
  }) : super(metrics: metrics, context: context);
121

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

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

136 137 138 139 140 141 142 143
/// 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
144
class ScrollUpdateNotification extends ScrollNotification {
145 146
  /// Creates a notification that a [Scrollable] widget has changed its scroll
  /// position.
147
  ScrollUpdateNotification({
148 149
    @required ScrollMetrics metrics,
    @required BuildContext context,
150 151
    this.dragDetails,
    this.scrollDelta,
152
  }) : super(metrics: metrics, context: context);
153

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

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

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

172 173 174 175 176 177 178 179 180
/// 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
181
class OverscrollNotification extends ScrollNotification {
182 183
  /// Creates a notification that a [Scrollable] widget has changed its scroll
  /// position outside of its scroll bounds.
184
  OverscrollNotification({
185 186
    @required ScrollMetrics metrics,
    @required BuildContext context,
187 188 189
    this.dragDetails,
    @required this.overscroll,
    this.velocity: 0.0,
190 191 192 193 194
  }) : assert(overscroll != null),
       assert(overscroll.isFinite),
       assert(overscroll != 0.0),
       assert(velocity != null),
       super(metrics: metrics, context: context);
195

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

Adam Barth's avatar
Adam Barth committed
202
  /// The number of logical pixels that the [Scrollable] avoided scrolling.
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
  ///
  /// 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');
  }
}

226 227 228 229 230 231
/// 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
232
class ScrollEndNotification extends ScrollNotification {
233
  /// Creates a notification that a [Scrollable] widget has stopped scrolling.
234
  ScrollEndNotification({
235 236
    @required ScrollMetrics metrics,
    @required BuildContext context,
237
    this.dragDetails,
238
  }) : super(metrics: metrics, context: context);
239

240 241 242 243 244 245 246 247
  /// 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
248
  /// be null. If the residual velocity is too small to trigger ballistic
249 250
  /// scrolling, then the [ScrollEndNotification] will be dispatched immediately
  /// and [dragDetails] will be non-null.
251 252 253 254 255 256 257 258 259 260
  final DragEndDetails dragDetails;

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

261 262 263 264 265 266
/// 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
267
class UserScrollNotification extends ScrollNotification {
268 269
  /// Creates a notification that the user has changed the direction in which
  /// they are scrolling.
270
  UserScrollNotification({
271 272
    @required ScrollMetrics metrics,
    @required BuildContext context,
273
    this.direction,
274
  }) : super(metrics: metrics, context: context);
275

276
  /// The direction in which the user is scrolling.
277 278 279 280 281 282 283 284
  final ScrollDirection direction;

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('direction: $direction');
  }
}
285 286 287 288 289 290

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

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