| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /// @docImport 'package:flutter/material.dart'; |
| 6 | /// |
| 7 | /// @docImport 'overscroll_indicator.dart'; |
| 8 | /// @docImport 'scroll_activity.dart'; |
| 9 | /// @docImport 'scroll_controller.dart'; |
| 10 | /// @docImport 'scroll_physics.dart'; |
| 11 | /// @docImport 'scroll_position.dart'; |
| 12 | /// @docImport 'scroll_view.dart'; |
| 13 | /// @docImport 'scrollable.dart'; |
| 14 | /// @docImport 'viewport.dart'; |
| 15 | library; |
| 16 | |
| 17 | import 'package:flutter/gestures.dart'; |
| 18 | import 'package:flutter/rendering.dart'; |
| 19 | |
| 20 | import 'framework.dart'; |
| 21 | import 'notification_listener.dart'; |
| 22 | import 'scroll_metrics.dart'; |
| 23 | |
| 24 | /// Mixin for [Notification]s that track how many [RenderAbstractViewport] they |
| 25 | /// have bubbled through. |
| 26 | /// |
| 27 | /// This is used by [ScrollNotification] and [OverscrollIndicatorNotification]. |
| 28 | mixin ViewportNotificationMixin on Notification { |
| 29 | /// The number of viewports that this notification has bubbled through. |
| 30 | /// |
| 31 | /// Typically listeners only respond to notifications with a [depth] of zero. |
| 32 | /// |
| 33 | /// Specifically, this is the number of [Widget]s representing |
| 34 | /// [RenderAbstractViewport] render objects through which this notification |
| 35 | /// has bubbled. |
| 36 | int get depth => _depth; |
| 37 | int _depth = 0; |
| 38 | |
| 39 | @override |
| 40 | void debugFillDescription(List<String> description) { |
| 41 | super.debugFillDescription(description); |
| 42 | description.add('depth: $depth ( ${depth == 0 ? "local" : "remote" })' ); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | /// A mixin that allows [Element]s containing [Viewport] like widgets to correctly |
| 47 | /// modify the notification depth of a [ViewportNotificationMixin]. |
| 48 | /// |
| 49 | /// See also: |
| 50 | /// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes |
| 51 | /// this in. |
| 52 | mixin ViewportElementMixin on NotifiableElementMixin { |
| 53 | @override |
| 54 | bool onNotification(Notification notification) { |
| 55 | if (notification is ViewportNotificationMixin) { |
| 56 | notification._depth += 1; |
| 57 | } |
| 58 | return false; |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// A [Notification] related to scrolling. |
| 63 | /// |
| 64 | /// [Scrollable] widgets notify their ancestors about scrolling-related changes. |
| 65 | /// The notifications have the following lifecycle: |
| 66 | /// |
| 67 | /// * A [ScrollStartNotification], which indicates that the widget has started |
| 68 | /// scrolling. |
| 69 | /// * Zero or more [ScrollUpdateNotification]s, which indicate that the widget |
| 70 | /// has changed its scroll position, mixed with zero or more |
| 71 | /// [OverscrollNotification]s, which indicate that the widget has not changed |
| 72 | /// its scroll position because the change would have caused its scroll |
| 73 | /// position to go outside its scroll bounds. |
| 74 | /// * Interspersed with the [ScrollUpdateNotification]s and |
| 75 | /// [OverscrollNotification]s are zero or more [UserScrollNotification]s, |
| 76 | /// which indicate that the user has changed the direction in which they are |
| 77 | /// scrolling. |
| 78 | /// * A [ScrollEndNotification], which indicates that the widget has stopped |
| 79 | /// scrolling. |
| 80 | /// * A [UserScrollNotification], with a [UserScrollNotification.direction] of |
| 81 | /// [ScrollDirection.idle]. |
| 82 | /// |
| 83 | /// Notifications bubble up through the tree, which means a given |
| 84 | /// [NotificationListener] will receive notifications for all descendant |
| 85 | /// [Scrollable] widgets. To focus on notifications from the nearest |
| 86 | /// [Scrollable] descendant, check that the [depth] property of the notification |
| 87 | /// is zero. |
| 88 | /// |
| 89 | /// When a scroll notification is received by a [NotificationListener], the |
| 90 | /// listener will have already completed build and layout, and it is therefore |
| 91 | /// too late for that widget to call [State.setState]. Any attempt to adjust the |
| 92 | /// build or layout based on a scroll notification would result in a layout that |
| 93 | /// lagged one frame behind, which is a poor user experience. Scroll |
| 94 | /// notifications are therefore primarily useful for paint effects (since paint |
| 95 | /// happens after layout). The [GlowingOverscrollIndicator] and [Scrollbar] |
| 96 | /// widgets are examples of paint effects that use scroll notifications. |
| 97 | /// |
| 98 | /// {@tool dartpad} |
| 99 | /// This sample shows the difference between using a [ScrollController] or a |
| 100 | /// [NotificationListener] of type [ScrollNotification] to listen to scrolling |
| 101 | /// activities. Toggling the [Radio] button switches between the two. |
| 102 | /// Using a [ScrollNotification] will provide details about the scrolling |
| 103 | /// activity, along with the metrics of the [ScrollPosition], but not the scroll |
| 104 | /// position object itself. By listening with a [ScrollController], the position |
| 105 | /// object is directly accessible. |
| 106 | /// Both of these types of notifications are only triggered by scrolling. |
| 107 | /// |
| 108 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_controller_notification.0.dart ** |
| 109 | /// {@end-tool} |
| 110 | /// |
| 111 | /// To drive layout based on the scroll position, consider listening to the |
| 112 | /// [ScrollPosition] directly (or indirectly via a [ScrollController]). This |
| 113 | /// will not notify when the [ScrollMetrics] of a given scroll position changes, |
| 114 | /// such as when the window is resized, changing the dimensions of the |
| 115 | /// [Viewport]. In order to listen to changes in scroll metrics, use a |
| 116 | /// [NotificationListener] of type [ScrollMetricsNotification]. |
| 117 | /// This type of notification differs from [ScrollNotification], as it is not |
| 118 | /// associated with the activity of scrolling, but rather the dimensions of |
| 119 | /// the scrollable area. |
| 120 | /// |
| 121 | /// {@tool dartpad} |
| 122 | /// This sample shows how a [ScrollMetricsNotification] is dispatched when |
| 123 | /// the `windowSize` is changed. Press the floating action button to increase |
| 124 | /// the scrollable window's size. |
| 125 | /// |
| 126 | /// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart ** |
| 127 | /// {@end-tool} |
| 128 | /// |
| 129 | abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin { |
| 130 | /// Initializes fields for subclasses. |
| 131 | ScrollNotification({required this.metrics, required this.context}); |
| 132 | |
| 133 | /// A description of a [Scrollable]'s contents, useful for modeling the state |
| 134 | /// of its viewport. |
| 135 | final ScrollMetrics metrics; |
| 136 | |
| 137 | /// The build context of the widget that fired this notification. |
| 138 | /// |
| 139 | /// This can be used to find the scrollable's render objects to determine the |
| 140 | /// size of the viewport, for instance. |
| 141 | final BuildContext? context; |
| 142 | |
| 143 | @override |
| 144 | void debugFillDescription(List<String> description) { |
| 145 | super.debugFillDescription(description); |
| 146 | description.add(' $metrics' ); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /// A notification that a [Scrollable] widget has started scrolling. |
| 151 | /// |
| 152 | /// See also: |
| 153 | /// |
| 154 | /// * [ScrollEndNotification], which indicates that scrolling has stopped. |
| 155 | /// * [ScrollNotification], which describes the notification lifecycle. |
| 156 | class ScrollStartNotification extends ScrollNotification { |
| 157 | /// Creates a notification that a [Scrollable] widget has started scrolling. |
| 158 | ScrollStartNotification({required super.metrics, required super.context, this.dragDetails}); |
| 159 | |
| 160 | /// If the [Scrollable] started scrolling because of a drag, the details about |
| 161 | /// that drag start. |
| 162 | /// |
| 163 | /// Otherwise, null. |
| 164 | final DragStartDetails? dragDetails; |
| 165 | |
| 166 | @override |
| 167 | void debugFillDescription(List<String> description) { |
| 168 | super.debugFillDescription(description); |
| 169 | if (dragDetails != null) { |
| 170 | description.add(' $dragDetails' ); |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | /// A notification that a [Scrollable] widget has changed its scroll position. |
| 176 | /// |
| 177 | /// See also: |
| 178 | /// |
| 179 | /// * [OverscrollNotification], which indicates that a [Scrollable] widget |
| 180 | /// has not changed its scroll position because the change would have caused |
| 181 | /// its scroll position to go outside its scroll bounds. |
| 182 | /// * [ScrollNotification], which describes the notification lifecycle. |
| 183 | class ScrollUpdateNotification extends ScrollNotification { |
| 184 | /// Creates a notification that a [Scrollable] widget has changed its scroll |
| 185 | /// position. |
| 186 | ScrollUpdateNotification({ |
| 187 | required super.metrics, |
| 188 | required BuildContext super.context, |
| 189 | this.dragDetails, |
| 190 | this.scrollDelta, |
| 191 | int? depth, |
| 192 | }) { |
| 193 | if (depth != null) { |
| 194 | _depth = depth; |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | /// If the [Scrollable] changed its scroll position because of a drag, the |
| 199 | /// details about that drag update. |
| 200 | /// |
| 201 | /// Otherwise, null. |
| 202 | final DragUpdateDetails? dragDetails; |
| 203 | |
| 204 | /// The distance by which the [Scrollable] was scrolled, in logical pixels. |
| 205 | final double? scrollDelta; |
| 206 | |
| 207 | @override |
| 208 | void debugFillDescription(List<String> description) { |
| 209 | super.debugFillDescription(description); |
| 210 | description.add('scrollDelta: $scrollDelta' ); |
| 211 | if (dragDetails != null) { |
| 212 | description.add(' $dragDetails' ); |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /// A notification that a [Scrollable] widget has not changed its scroll position |
| 218 | /// because the change would have caused its scroll position to go outside of |
| 219 | /// its scroll bounds. |
| 220 | /// |
| 221 | /// See also: |
| 222 | /// |
| 223 | /// * [ScrollUpdateNotification], which indicates that a [Scrollable] widget |
| 224 | /// has changed its scroll position. |
| 225 | /// * [ScrollNotification], which describes the notification lifecycle. |
| 226 | class OverscrollNotification extends ScrollNotification { |
| 227 | /// Creates a notification that a [Scrollable] widget has changed its scroll |
| 228 | /// position outside of its scroll bounds. |
| 229 | OverscrollNotification({ |
| 230 | required super.metrics, |
| 231 | required BuildContext super.context, |
| 232 | this.dragDetails, |
| 233 | required this.overscroll, |
| 234 | this.velocity = 0.0, |
| 235 | }) : assert(overscroll.isFinite), |
| 236 | assert(overscroll != 0.0); |
| 237 | |
| 238 | /// If the [Scrollable] overscrolled because of a drag, the details about that |
| 239 | /// drag update. |
| 240 | /// |
| 241 | /// Otherwise, null. |
| 242 | final DragUpdateDetails? dragDetails; |
| 243 | |
| 244 | /// The number of logical pixels that the [Scrollable] avoided scrolling. |
| 245 | /// |
| 246 | /// This will be negative for overscroll on the "start" side and positive for |
| 247 | /// overscroll on the "end" side. |
| 248 | final double overscroll; |
| 249 | |
| 250 | /// The velocity at which the [ScrollPosition] was changing when this |
| 251 | /// overscroll happened. |
| 252 | /// |
| 253 | /// This will typically be 0.0 for touch-driven overscrolls, and positive |
| 254 | /// for overscrolls that happened from a [BallisticScrollActivity] or |
| 255 | /// [DrivenScrollActivity]. |
| 256 | final double velocity; |
| 257 | |
| 258 | @override |
| 259 | void debugFillDescription(List<String> description) { |
| 260 | super.debugFillDescription(description); |
| 261 | description.add('overscroll: ${overscroll.toStringAsFixed(1)}' ); |
| 262 | description.add('velocity: ${velocity.toStringAsFixed(1)}' ); |
| 263 | if (dragDetails != null) { |
| 264 | description.add(' $dragDetails' ); |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | /// A notification that a [Scrollable] widget has stopped scrolling. |
| 270 | /// |
| 271 | /// {@tool dartpad} |
| 272 | /// This sample shows how you can trigger an auto-scroll, which aligns the last |
| 273 | /// partially visible fixed-height list item, by listening for this |
| 274 | /// notification with a [NotificationListener]. This sort of thing can also |
| 275 | /// be done by listening to the [ScrollController]'s |
| 276 | /// [ScrollPosition.isScrollingNotifier]. An alternative example is provided |
| 277 | /// with [ScrollPosition.isScrollingNotifier]. |
| 278 | /// |
| 279 | /// ** See code in examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.0.dart ** |
| 280 | /// {@end-tool} |
| 281 | /// |
| 282 | /// |
| 283 | /// {@tool dartpad} |
| 284 | /// This example auto-scrolls one special "aligned item" sliver to |
| 285 | /// the top or bottom of the viewport, whenever it's partially visible |
| 286 | /// (because it overlaps the top or bottom of the viewport). This |
| 287 | /// example differs from the previous one in that the layout of an |
| 288 | /// individual sliver is retrieved from its [RenderSliver] via a |
| 289 | /// [GlobalKey]. The example does not rely on all of the list items |
| 290 | /// having the same extent. |
| 291 | /// |
| 292 | /// ** See code in examples/api/lib/widgets/scroll_end_notification/scroll_end_notification.1.dart ** |
| 293 | /// {@end-tool} |
| 294 | /// See also: |
| 295 | /// |
| 296 | /// * [ScrollStartNotification], which indicates that scrolling has started. |
| 297 | /// * [ScrollNotification], which describes the notification lifecycle. |
| 298 | class ScrollEndNotification extends ScrollNotification { |
| 299 | /// Creates a notification that a [Scrollable] widget has stopped scrolling. |
| 300 | ScrollEndNotification({ |
| 301 | required super.metrics, |
| 302 | required BuildContext super.context, |
| 303 | this.dragDetails, |
| 304 | }); |
| 305 | |
| 306 | /// If the [Scrollable] stopped scrolling because of a drag, the details about |
| 307 | /// that drag end. |
| 308 | /// |
| 309 | /// Otherwise, null. |
| 310 | /// |
| 311 | /// If a drag ends with some residual velocity, a typical [ScrollPhysics] will |
| 312 | /// start a ballistic scroll, which delays the [ScrollEndNotification] until |
| 313 | /// the ballistic simulation completes, at which time [dragDetails] will |
| 314 | /// be null. If the residual velocity is too small to trigger ballistic |
| 315 | /// scrolling, then the [ScrollEndNotification] will be dispatched immediately |
| 316 | /// and [dragDetails] will be non-null. |
| 317 | final DragEndDetails? dragDetails; |
| 318 | |
| 319 | @override |
| 320 | void debugFillDescription(List<String> description) { |
| 321 | super.debugFillDescription(description); |
| 322 | if (dragDetails != null) { |
| 323 | description.add(' $dragDetails' ); |
| 324 | } |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | /// A notification that the user has changed the [ScrollDirection] in which they |
| 329 | /// are scrolling, or have stopped scrolling. |
| 330 | /// |
| 331 | /// For the direction that the [ScrollView] is oriented to, and the direction |
| 332 | /// contents are being laid out in, see [AxisDirection] & [GrowthDirection]. |
| 333 | /// |
| 334 | /// {@macro flutter.rendering.ScrollDirection.sample} |
| 335 | /// |
| 336 | /// See also: |
| 337 | /// |
| 338 | /// * [ScrollNotification], which describes the notification lifecycle. |
| 339 | class UserScrollNotification extends ScrollNotification { |
| 340 | /// Creates a notification that the user has changed the direction in which |
| 341 | /// they are scrolling. |
| 342 | UserScrollNotification({ |
| 343 | required super.metrics, |
| 344 | required BuildContext super.context, |
| 345 | required this.direction, |
| 346 | }); |
| 347 | |
| 348 | /// The direction in which the user is scrolling. |
| 349 | /// |
| 350 | /// This does not represent the current [AxisDirection] or [GrowthDirection] |
| 351 | /// of the [Viewport], which respectively represent the direction that the |
| 352 | /// scroll offset is increasing in, and the direction that contents are being |
| 353 | /// laid out in. |
| 354 | /// |
| 355 | /// {@macro flutter.rendering.ScrollDirection.sample} |
| 356 | final ScrollDirection direction; |
| 357 | |
| 358 | @override |
| 359 | void debugFillDescription(List<String> description) { |
| 360 | super.debugFillDescription(description); |
| 361 | description.add('direction: $direction' ); |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | /// A predicate for [ScrollNotification], used to customize widgets that |
| 366 | /// listen to notifications from their children. |
| 367 | typedef ScrollNotificationPredicate = bool Function(ScrollNotification notification); |
| 368 | |
| 369 | /// A [ScrollNotificationPredicate] that checks whether |
| 370 | /// `notification.depth == 0`, which means that the notification did not bubble |
| 371 | /// through any intervening scrolling widgets. |
| 372 | bool defaultScrollNotificationPredicate(ScrollNotification notification) { |
| 373 | return notification.depth == 0; |
| 374 | } |
| 375 | |