| // 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/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'framework.dart'; |
| import 'notification_listener.dart'; |
| import 'scroll_metrics.dart'; |
| |
| /// Mixin for [Notification]s that track how many [RenderAbstractViewport] they |
| /// have bubbled through. |
| /// |
| /// This is used by [ScrollNotification] and [OverscrollIndicatorNotification]. |
| abstract class ViewportNotificationMixin extends Notification { |
| // This class is intended to be used as a mixin, and should not be |
| // extended directly. |
| factory ViewportNotificationMixin._() => null; |
| |
| /// 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"})'); |
| } |
| } |
| |
| /// 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, |
| /// which indicate that the user has changed the direction in which they are |
| /// scrolling. |
| /// * A [ScrollEndNotification], which indicates that the widget has stopped |
| /// scrolling. |
| /// * A [UserScrollNotification], with a [UserScrollNotification.direction] of |
| /// [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. |
| /// |
| /// 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]). |
| abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin { |
| /// Initializes fields for subclasses. |
| ScrollNotification({ |
| @required this.metrics, |
| @required this.context, |
| }); |
| |
| /// A description of a [Scrollable]'s contents, useful for modeling the state |
| /// of its viewport. |
| final ScrollMetrics metrics; |
| |
| /// The build context of the widget that fired this notification. |
| /// |
| /// 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); |
| description.add('$metrics'); |
| } |
| } |
| |
| /// A notification that a [Scrollable] widget has started scrolling. |
| /// |
| /// See also: |
| /// |
| /// * [ScrollEndNotification], which indicates that scrolling has stopped. |
| /// * [ScrollNotification], which describes the notification lifecycle. |
| class ScrollStartNotification extends ScrollNotification { |
| /// Creates a notification that a [Scrollable] widget has started scrolling. |
| ScrollStartNotification({ |
| @required ScrollMetrics metrics, |
| @required BuildContext context, |
| this.dragDetails, |
| }) : super(metrics: metrics, context: context); |
| |
| /// If the [Scrollable] started scrolling because of a drag, the details about |
| /// that drag start. |
| /// |
| /// Otherwise, null. |
| final DragStartDetails dragDetails; |
| |
| @override |
| void debugFillDescription(List<String> description) { |
| super.debugFillDescription(description); |
| if (dragDetails != null) |
| description.add('$dragDetails'); |
| } |
| } |
| |
| /// 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. |
| class ScrollUpdateNotification extends ScrollNotification { |
| /// Creates a notification that a [Scrollable] widget has changed its scroll |
| /// position. |
| ScrollUpdateNotification({ |
| @required ScrollMetrics metrics, |
| @required BuildContext context, |
| this.dragDetails, |
| this.scrollDelta, |
| }) : super(metrics: metrics, context: context); |
| |
| /// If the [Scrollable] changed its scroll position because of a drag, the |
| /// details about that drag update. |
| /// |
| /// Otherwise, null. |
| final DragUpdateDetails dragDetails; |
| |
| /// The distance by which the [Scrollable] was scrolled, in logical pixels. |
| final double scrollDelta; |
| |
| @override |
| void debugFillDescription(List<String> description) { |
| super.debugFillDescription(description); |
| description.add('scrollDelta: $scrollDelta'); |
| if (dragDetails != null) |
| description.add('$dragDetails'); |
| } |
| } |
| |
| /// 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. |
| class OverscrollNotification extends ScrollNotification { |
| /// Creates a notification that a [Scrollable] widget has changed its scroll |
| /// position outside of its scroll bounds. |
| OverscrollNotification({ |
| @required ScrollMetrics metrics, |
| @required BuildContext context, |
| this.dragDetails, |
| @required this.overscroll, |
| this.velocity: 0.0, |
| }) : assert(overscroll != null), |
| assert(overscroll.isFinite), |
| assert(overscroll != 0.0), |
| assert(velocity != null), |
| super(metrics: metrics, context: context); |
| |
| /// If the [Scrollable] overscrolled because of a drag, the details about that |
| /// drag update. |
| /// |
| /// Otherwise, null. |
| final DragUpdateDetails dragDetails; |
| |
| /// The number of logical pixels that the [Scrollable] avoided scrolling. |
| /// |
| /// 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'); |
| } |
| } |
| |
| /// A notification that a [Scrollable] widget has stopped scrolling. |
| /// |
| /// See also: |
| /// |
| /// * [ScrollStartNotification], which indicates that scrolling has started. |
| /// * [ScrollNotification], which describes the notification lifecycle. |
| class ScrollEndNotification extends ScrollNotification { |
| /// Creates a notification that a [Scrollable] widget has stopped scrolling. |
| ScrollEndNotification({ |
| @required ScrollMetrics metrics, |
| @required BuildContext context, |
| this.dragDetails, |
| }) : super(metrics: metrics, context: context); |
| |
| /// 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 |
| /// be null. If the residual velocity is too small to trigger ballistic |
| /// scrolling, then the [ScrollEndNotification] will be dispatched immediately |
| /// and [dragDetails] will be non-null. |
| final DragEndDetails dragDetails; |
| |
| @override |
| void debugFillDescription(List<String> description) { |
| super.debugFillDescription(description); |
| if (dragDetails != null) |
| description.add('$dragDetails'); |
| } |
| } |
| |
| /// A notification that the user has changed the direction in which they are |
| /// scrolling. |
| /// |
| /// See also: |
| /// |
| /// * [ScrollNotification], which describes the notification lifecycle. |
| class UserScrollNotification extends ScrollNotification { |
| /// Creates a notification that the user has changed the direction in which |
| /// they are scrolling. |
| UserScrollNotification({ |
| @required ScrollMetrics metrics, |
| @required BuildContext context, |
| this.direction, |
| }) : super(metrics: metrics, context: context); |
| |
| /// The direction in which the user is scrolling. |
| final ScrollDirection direction; |
| |
| @override |
| void debugFillDescription(List<String> description) { |
| super.debugFillDescription(description); |
| description.add('direction: $direction'); |
| } |
| } |
| |
| /// 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 |
| /// `notification.depth == 0`, which means that the notification did not bubble |
| /// through any intervening scrolling widgets. |
| bool defaultScrollNotificationPredicate(ScrollNotification notification) { |
| return notification.depth == 0; |
| } |