From c6b0f833af9e431df1e67f15e8b51a76e8bc7d71 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sun, 30 Apr 2017 08:59:06 -0700 Subject: [PATCH] Document more scrolling classes (#9684) --- .../flutter/lib/src/rendering/sliver.dart | 4 + .../lib/src/widgets/nested_scroll_view.dart | 11 +- .../widgets/primary_scroll_controller.dart | 17 +++ .../lib/src/widgets/scroll_activity.dart | 126 ++++++++++++++++-- .../lib/src/widgets/scroll_context.dart | 33 +++++ .../lib/src/widgets/scroll_metrics.dart | 28 ++++ .../scroll_position_with_single_context.dart | 6 +- 7 files changed, 209 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index cf7c5d1989..1e8d69b041 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -1261,6 +1261,10 @@ abstract class RenderSliverSingleBoxAdapter extends RenderSliver with RenderObje child.parentData = new SliverPhysicalParentData(); } + /// Sets the [SliverPhysicalParentData.paintOffset] for the given child + /// according to the [SliverConstraints.axisDirection] and + /// [SliverConstraints.growthDirection] and the given geometry. + @protected void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) { final SliverPhysicalParentData childParentData = child.parentData; assert(constraints.axisDirection != null); diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index 6b5c9a1b2b..feeaa4fbb3 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -24,7 +24,8 @@ import 'scroll_view.dart'; import 'sliver.dart'; import 'ticker_provider.dart'; -typedef List NestedScrollViewOuterSliversBuilder(BuildContext context, bool innerBoxIsScrolled); +/// Signature used by [NestedScrollView] for building its header. +typedef List NestedScrollViewHeaderSliversBuilder(BuildContext context, bool innerBoxIsScrolled); class NestedScrollView extends StatefulWidget { NestedScrollView({ @@ -49,7 +50,7 @@ class NestedScrollView extends StatefulWidget { final ScrollPhysics physics; - final NestedScrollViewOuterSliversBuilder headerSliverBuilder; + final NestedScrollViewHeaderSliversBuilder headerSliverBuilder; final Widget body; @@ -415,9 +416,9 @@ class _NestedScrollCoorindator implements ScrollActivityDelegate { Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { final ScrollDragController drag = new ScrollDragController( - this, - details, - dragCancelCallback, + delegate: this, + details: details, + onDragCanceled: dragCancelCallback, ); beginActivity( new DragScrollActivity(_outerPosition, drag), diff --git a/packages/flutter/lib/src/widgets/primary_scroll_controller.dart b/packages/flutter/lib/src/widgets/primary_scroll_controller.dart index b2894eed41..5adac08066 100644 --- a/packages/flutter/lib/src/widgets/primary_scroll_controller.dart +++ b/packages/flutter/lib/src/widgets/primary_scroll_controller.dart @@ -7,7 +7,17 @@ import 'package:flutter/foundation.dart'; import 'framework.dart'; import 'scroll_controller.dart'; +/// Associates a [ScrollController] with a subtree. +/// +/// When a [ScrollView] has [ScrollView.primary] set to true and is not given +/// an explicit [ScrollController], the [ScrollView] uses [of] to find the +/// [ScrollController] associated with its subtree. +/// +/// This mechanism can be used to provide default behavior for scroll views in a +/// subtree. For example, the [Scaffold] uses this mechanism to implement the +/// scroll-to-top gesture on iOS. class PrimaryScrollController extends InheritedWidget { + /// Creates a widget that associates a [ScrollController] with a subtree. const PrimaryScrollController({ Key key, @required this.controller, @@ -15,14 +25,21 @@ class PrimaryScrollController extends InheritedWidget { }) : assert(controller != null), super(key: key, child: child); + /// Creates a subtree without an associated [ScrollController]. const PrimaryScrollController.none({ Key key, @required Widget child }) : controller = null, super(key: key, child: child); + /// The [ScrollController] associated with the subtree. final ScrollController controller; + /// Returns the [ScrollController] most closely associated with the given + /// context. + /// + /// Returns null if there is no [ScrollController] associated with the given + /// context. static ScrollController of(BuildContext context) { final PrimaryScrollController result = context.inheritFromWidgetOfExactType(PrimaryScrollController); return result?.controller; diff --git a/packages/flutter/lib/src/widgets/scroll_activity.dart b/packages/flutter/lib/src/widgets/scroll_activity.dart index 883b5d2733..271ca47639 100644 --- a/packages/flutter/lib/src/widgets/scroll_activity.dart +++ b/packages/flutter/lib/src/widgets/scroll_activity.dart @@ -17,13 +17,37 @@ import 'scroll_metrics.dart'; import 'scroll_notification.dart'; import 'ticker_provider.dart'; +/// A backend for a [ScrollActivity]. +/// +/// Used by subclases of [ScrollActivity] to manipulate the scroll view that +/// they are acting upon. +/// +/// See also: +/// +/// * [ScrollActivity], which uses this class as its delegate. abstract class ScrollActivityDelegate { + /// The direction in which the scroll view scrolls. AxisDirection get axisDirection; + /// Update the scroll position to the given pixel value. + /// + /// Returns the overscroll, if any. See [ScrollPosition.setPixels] for more + /// information. double setPixels(double pixels); + + /// Updates the scroll position by the given amount. + /// + /// Appropriate for when the user is directly manipulating the scroll + /// position, for example by dragging the scroll view. Typically applies + /// [ScrollPhysics.applyPhysicsToUserOffset] and other transformations that + /// are appropriate for user-driving scrolling. void applyUserOffset(double delta); + /// Terminate the current activity and start an idle activity. void goIdle(); + + /// Terminate the current activity and start a ballistic activity with the + /// given velocity. void goBallistic(double velocity); } @@ -31,11 +55,13 @@ abstract class ScrollActivityDelegate { /// /// See also: /// -/// * [ScrollPositionWithSingleContext], which uses [ScrollActivity] objects to -/// manage the [ScrollPosition] of a [Scrollable]. +/// * [ScrollPosition], which uses [ScrollActivity] objects to manage the +/// [ScrollPosition] of a [Scrollable]. abstract class ScrollActivity { + /// Initializes [delegate] for subclasses. ScrollActivity(this._delegate); + /// The delegate that this activity will use to actuate the scroll view. ScrollActivityDelegate get delegate => _delegate; ScrollActivityDelegate _delegate; @@ -58,30 +84,43 @@ abstract class ScrollActivity { /// [ScrollActivityDelegate.goBallistic]. void resetActivity() { } + /// Dispatch a [ScrollStartNotification] with the given metrics. void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) { new ScrollStartNotification(metrics: metrics, context: context).dispatch(context); } + /// Dispatch a [ScrollUpdateNotification] with the given metrics and scroll delta. void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) { new ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta).dispatch(context); } + /// Dispatch an [OverscrollNotification] with the given metrics and overscroll. void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) { new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll).dispatch(context); } + /// Dispatch a [ScrollEndNotification] with the given metrics and overscroll. void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) { new ScrollEndNotification(metrics: metrics, context: context).dispatch(context); } + /// Called when the user touches the scroll view that is performing this activity. void didTouch() { } + /// Called when the scroll view that is performing this activity changes its metrics. void applyNewDimensions() { } + /// Whether the scroll view should ignore pointer events while performing this + /// activity. bool get shouldIgnorePointer; + /// Whether performing this activity constitutes scrolling. + /// + /// Used, for example, to determine whether the user scroll direction is + /// [ScrollDirection.idle]. bool get isScrolling; + /// Called when the scroll view stops performing this activity. @mustCallSuper void dispose() { _delegate = null; @@ -91,7 +130,11 @@ abstract class ScrollActivity { String toString() => '$runtimeType'; } +/// A scroll activity that does nothing. +/// +/// When a scroll view is not scrolling, it is performing the idle activity. class IdleScrollActivity extends ScrollActivity { + /// Creates a scroll activity that does nothing. IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate); @override @@ -106,16 +149,31 @@ class IdleScrollActivity extends ScrollActivity { bool get isScrolling => false; } +/// Scrolls a scroll view as the user drags their finger across the screen. +/// +/// See also: +/// +/// * [DragScrollActivity], which is the activity the scroll view performs +/// while a drag is underway. class ScrollDragController implements Drag { - ScrollDragController( - ScrollActivityDelegate delegate, - DragStartDetails details, + /// Creates an object that scrolls a scroll view as the user drags their + /// finger across the screen. + /// + /// The [delegate] and `details` arguments must not be null. + ScrollDragController({ + @required ScrollActivityDelegate delegate, + @required DragStartDetails details, this.onDragCanceled, - ) : _delegate = delegate, _lastDetails = details; + }) : _delegate = delegate, _lastDetails = details { + assert(delegate != null); + assert(details != null); + } + /// The object that will actuate the scroll view as the user drags. ScrollActivityDelegate get delegate => _delegate; ScrollActivityDelegate _delegate; + /// Called when [dispose] is called. final VoidCallback onDragCanceled; bool get _reversed => axisDirectionIsReversed(delegate.axisDirection); @@ -159,6 +217,7 @@ class ScrollDragController implements Drag { delegate.goBallistic(0.0); } + /// Called when the delegate is no longer sending events to this object. @mustCallSuper void dispose() { _lastDetails = null; @@ -166,11 +225,22 @@ class ScrollDragController implements Drag { onDragCanceled(); } + /// The most recently observed [DragStartDetails], [DragUpdateDetails], or + /// [DragEndDetails] object. dynamic get lastDetails => _lastDetails; dynamic _lastDetails; } +/// The activity a scroll view performs when a the user drags their finger +/// across the screen. +/// +/// See also: +/// +/// * [ScrollDragController], which listens to the [Drag] and actually scrolls +/// the scroll view. class DragScrollActivity extends ScrollActivity { + /// Creates an activity for when the user drags their finger across the + /// screen. DragScrollActivity( ScrollActivityDelegate delegate, ScrollDragController controller, @@ -228,9 +298,23 @@ class DragScrollActivity extends ScrollActivity { } } +/// An activity that animates a scroll view based on a physics [simulation]. +/// +/// A [BallisticScrollActivity] is typically used when the user lifts their +/// finger off the screen to continue the scrolling gesture with the current velocity. +/// +/// [BallisticScrollActivity] is also used to restore a scroll view to a valid +/// scroll offset when the geometry of the scroll view changes. In these +/// situations, the [simulation] typically starts with a zero velocity. +/// +/// See also: +/// +/// * [DrivenScrollActivity], which animates a scroll view based on a set of +/// animation parameters. class BallisticScrollActivity extends ScrollActivity { - // /// - // /// The velocity should be in logical pixels per second. + /// Creates an activity that animates a scroll view based on a [simulation]. + /// + /// The [delegate], [simulation], and [vsync] arguments must not be null. BallisticScrollActivity( ScrollActivityDelegate delegate, Simulation simulation, @@ -245,6 +329,8 @@ class BallisticScrollActivity extends ScrollActivity { .whenComplete(_end); // won't trigger if we dispose _controller first } + /// The velocity at which the scroll offset is currently changing (in logical + /// pixels per second). double get velocity => _controller.velocity; AnimationController _controller; @@ -271,8 +357,8 @@ class BallisticScrollActivity extends ScrollActivity { /// Move the position to the given location. /// - /// If the new position was fully applied, return true. - /// If there was any overflow, return false. + /// If the new position was fully applied, returns true. If there was any + /// overflow, returns false. /// /// The default implementation calls [ScrollActivityDelegate.setPixels] /// and returns true if the overflow was zero. @@ -308,7 +394,20 @@ class BallisticScrollActivity extends ScrollActivity { } } +/// An activity that animates a scroll view based on animation parameters. +/// +/// For example, a [DrivenScrollActivity] is used to implement +/// [ScrollController.animateTo]. +/// +/// See also: +/// +/// * [BallisticScrollActivity], which animates a scroll view based on a +/// physics [Simulation]. class DrivenScrollActivity extends ScrollActivity { + /// Creates an activity that animates a scroll view based on animation + /// parameters. + /// + /// All of the parameters must be non-null. DrivenScrollActivity( ScrollActivityDelegate delegate, { @required double from, @@ -336,8 +435,15 @@ class DrivenScrollActivity extends ScrollActivity { Completer _completer; AnimationController _controller; + /// A [Future] that completes when the activity stops. + /// + /// For example, this [Future] will complete if the animation reaches the end + /// or if the user interacts with the scroll view in way that causes the + /// animation to stop before it reaches the end. Future get done => _completer.future; + /// The velocity at which the scroll offset is currently changing (in logical + /// pixels per second). double get velocity => _controller.velocity; @override diff --git a/packages/flutter/lib/src/widgets/scroll_context.dart b/packages/flutter/lib/src/widgets/scroll_context.dart index 5af908d023..10ba7b79a8 100644 --- a/packages/flutter/lib/src/widgets/scroll_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_context.dart @@ -8,11 +8,44 @@ import 'package:flutter/rendering.dart'; import 'framework.dart'; import 'ticker_provider.dart'; +/// An interface that [Scrollable] widgets implement in order to use +/// [ScrollPosition]. +/// +/// See also: +/// +/// * [ScrollableState], which is the most common implementation of this +/// interface. +/// * [ScrollPosition], which uses this interface to communicate with the +/// scrollable widget. abstract class ScrollContext { + /// The [BuildContext] that should be used when dispatching + /// [ScrollNotification]s. + /// + /// This context is typically different that the context of the scrollable + /// widget itself. For example, [Scrollable] uses a context outside the + /// [Viewport] but inside the widgets created by + /// [ScrollBehavior.buildViewportChrome]. BuildContext get notificationContext; + + /// A [TickerProvider] to use when animating the scroll position. TickerProvider get vsync; + + /// The direction in which the widget scrolls. AxisDirection get axisDirection; + /// Whether the contents of the widget should ignore [PointerEvent] inputs. + /// + /// Setting this value to true prevents the use from interacting with the + /// contents of the widget with pointer events. The widget itself is still + /// interactive. + /// + /// For example, if the scroll position is being driven by an animation, it + /// might be appropriate to set this value to ignore pointer events to + /// prevent the user from accidentially interacting with the contents of the + /// widget as it animates. The user will still be able to touch the widget, + /// potentially stopping the animation. void setIgnorePointer(bool value); + + /// Whether the user can drag the widget, for example to initiate a scroll. void setCanDrag(bool value); } diff --git a/packages/flutter/lib/src/widgets/scroll_metrics.dart b/packages/flutter/lib/src/widgets/scroll_metrics.dart index 24251e39dd..376243c52a 100644 --- a/packages/flutter/lib/src/widgets/scroll_metrics.dart +++ b/packages/flutter/lib/src/widgets/scroll_metrics.dart @@ -26,6 +26,11 @@ import 'package:flutter/rendering.dart'; /// The above values are also exposed in terms of [extentBefore], /// [extentInside], and [extentAfter], which may be more useful for use cases /// such as scroll bars; for example, see [Scrollbar]. +/// +/// See also: +/// +/// * [FixedScrollMetrics], which is an immutable object that implements this +/// interface. abstract class ScrollMetrics { /// Creates a [ScrollMetrics] that has the same properties as this object. /// @@ -33,16 +38,34 @@ abstract class ScrollMetrics { /// of the current state. ScrollMetrics cloneMetrics() => new FixedScrollMetrics.clone(this); + /// The minimum in-range value for [pixels]. + /// + /// The actual [pixels] value might be [outOfRange]. double get minScrollExtent; + + /// The maximum in-range value for [pixels]. + /// + /// The actual [pixels] value might be [outOfRange]. double get maxScrollExtent; + + /// The current scroll position, in logical pixels along the [axisDirection]. double get pixels; + + /// The extent of the viewport along the [axisDirection]. double get viewportDimension; + + /// The direction in which the scroll view scrolls. AxisDirection get axisDirection; + /// The axis in which the scroll view scrolls. Axis get axis => axisDirectionToAxis(axisDirection); + /// Whether the [pixels] value is outside the [minScrollExtent] and + /// [maxScrollExtent]. bool get outOfRange => pixels < minScrollExtent || pixels > maxScrollExtent; + /// Whether the [pixels] value is exactly at the [minScrollExtent] or the + /// [maxScrollExtent]. bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent; /// The quantity of content conceptually "above" the currently visible content @@ -72,8 +95,12 @@ abstract class ScrollMetrics { } } +/// An immutable snapshot of values associated with a [Scrollable] viewport. +/// +/// For details, see [ScrollMetrics], which defines this object's interfaces. @immutable class FixedScrollMetrics extends ScrollMetrics { + /// Creates an immutable snapshot of values associated with a [Scrollable] viewport. FixedScrollMetrics({ @required this.minScrollExtent, @required this.maxScrollExtent, @@ -82,6 +109,7 @@ class FixedScrollMetrics extends ScrollMetrics { @required this.axisDirection, }); + /// Creates an immutable snapshot of the given metrics. FixedScrollMetrics.clone(ScrollMetrics parent) : minScrollExtent = parent.minScrollExtent, maxScrollExtent = parent.maxScrollExtent, diff --git a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart index 38f12ed0fb..0ee15f94be 100644 --- a/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_position_with_single_context.dart @@ -304,7 +304,11 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc /// [ScrollDragController.onDragCanceled] for details. @override Drag drag(DragStartDetails details, VoidCallback onDragCanceled) { - final ScrollDragController drag = new ScrollDragController(this, details, onDragCanceled); + final ScrollDragController drag = new ScrollDragController( + delegate: this, + details: details, + onDragCanceled: onDragCanceled, + ); beginActivity(new DragScrollActivity(this, drag)); assert(_currentDrag == null); _currentDrag = drag;