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
5import 'package:flutter/animation.dart';
6import 'package:flutter/foundation.dart';
7
8/// The direction of a scroll, relative to the positive scroll offset axis given
9/// by an [AxisDirection] and a [GrowthDirection].
10///
11/// This is similar to [GrowthDirection], but contrasts in that it has a third
12/// value, [idle], for the case where no scroll is occurring.
13///
14/// This is used by [RenderSliverFloatingPersistentHeader] to only expand when
15/// the user is scrolling in the same direction as the detected scroll offset
16/// change.
17///
18/// {@template flutter.rendering.ScrollDirection.sample}
19/// {@tool dartpad}
20/// This sample shows a [CustomScrollView], with [Radio] buttons in the
21/// [AppBar.bottom] that change the [AxisDirection] to illustrate different
22/// configurations. With a [NotificationListener] to listen to
23/// [UserScrollNotification]s, which occur when the [ScrollDirection] changes
24/// or stops.
25///
26/// ** See code in examples/api/lib/rendering/scroll_direction/scroll_direction.0.dart **
27/// {@end-tool}
28/// {@endtemplate}
29///
30/// See also:
31///
32/// * [AxisDirection], which is a directional version of this enum (with values
33/// like left and right, rather than just horizontal).
34/// * [GrowthDirection], the direction in which slivers and their content are
35/// ordered, relative to the scroll offset axis as specified by
36/// [AxisDirection].
37/// * [UserScrollNotification], which will notify listeners when the
38/// [ScrollDirection] changes.
39enum ScrollDirection {
40 /// No scrolling is underway.
41 idle,
42
43 /// Scrolling is happening in the negative scroll offset direction.
44 ///
45 /// For example, for the [GrowthDirection.forward] part of a vertical
46 /// [AxisDirection.down] list, which is the default directional configuration
47 /// of all scroll views, this means the content is going down, exposing
48 /// earlier content as it approaches the zero position.
49 ///
50 /// An anecdote for this most common case is 'forward is toward' the zero
51 /// position.
52 forward,
53
54 /// Scrolling is happening in the positive scroll offset direction.
55 ///
56 /// For example, for the [GrowthDirection.forward] part of a vertical
57 /// [AxisDirection.down] list, which is the default directional configuration
58 /// of all scroll views, this means the content is moving up, exposing
59 /// lower content.
60 ///
61 /// An anecdote for this most common case is reversing, or backing away, from
62 /// the zero position.
63 reverse,
64}
65
66/// Returns the opposite of the given [ScrollDirection].
67///
68/// Specifically, returns [ScrollDirection.reverse] for [ScrollDirection.forward]
69/// (and vice versa) and returns [ScrollDirection.idle] for
70/// [ScrollDirection.idle].
71ScrollDirection flipScrollDirection(ScrollDirection direction) {
72 switch (direction) {
73 case ScrollDirection.idle:
74 return ScrollDirection.idle;
75 case ScrollDirection.forward:
76 return ScrollDirection.reverse;
77 case ScrollDirection.reverse:
78 return ScrollDirection.forward;
79 }
80}
81
82/// Which part of the content inside the viewport should be visible.
83///
84/// The [pixels] value determines the scroll offset that the viewport uses to
85/// select which part of its content to display. As the user scrolls the
86/// viewport, this value changes, which changes the content that is displayed.
87///
88/// This object is a [Listenable] that notifies its listeners when [pixels]
89/// changes.
90///
91/// See also:
92///
93/// * [ScrollPosition], which is a commonly used concrete subclass.
94/// * [RenderViewportBase], which is a render object that uses viewport
95/// offsets.
96abstract class ViewportOffset extends ChangeNotifier {
97 /// Default constructor.
98 ///
99 /// Allows subclasses to construct this object directly.
100 ViewportOffset() {
101 if (kFlutterMemoryAllocationsEnabled) {
102 ChangeNotifier.maybeDispatchObjectCreation(this);
103 }
104 }
105
106 /// Creates a viewport offset with the given [pixels] value.
107 ///
108 /// The [pixels] value does not change unless the viewport issues a
109 /// correction.
110 factory ViewportOffset.fixed(double value) = _FixedViewportOffset;
111
112 /// Creates a viewport offset with a [pixels] value of 0.0.
113 ///
114 /// The [pixels] value does not change unless the viewport issues a
115 /// correction.
116 factory ViewportOffset.zero() = _FixedViewportOffset.zero;
117
118 /// The number of pixels to offset the children in the opposite of the axis direction.
119 ///
120 /// For example, if the axis direction is down, then the pixel value
121 /// represents the number of logical pixels to move the children _up_ the
122 /// screen. Similarly, if the axis direction is left, then the pixels value
123 /// represents the number of logical pixels to move the children to _right_.
124 ///
125 /// This object notifies its listeners when this value changes (except when
126 /// the value changes due to [correctBy]).
127 double get pixels;
128
129 /// Whether the [pixels] property is available.
130 bool get hasPixels;
131
132 /// Called when the viewport's extents are established.
133 ///
134 /// The argument is the dimension of the [RenderViewport] in the main axis
135 /// (e.g. the height, for a vertical viewport).
136 ///
137 /// This may be called redundantly, with the same value, each frame. This is
138 /// called during layout for the [RenderViewport]. If the viewport is
139 /// configured to shrink-wrap its contents, it may be called several times,
140 /// since the layout is repeated each time the scroll offset is corrected.
141 ///
142 /// If this is called, it is called before [applyContentDimensions]. If this
143 /// is called, [applyContentDimensions] will be called soon afterwards in the
144 /// same layout phase. If the viewport is not configured to shrink-wrap its
145 /// contents, then this will only be called when the viewport recomputes its
146 /// size (i.e. when its parent lays out), and not during normal scrolling.
147 ///
148 /// If applying the viewport dimensions changes the scroll offset, return
149 /// false. Otherwise, return true. If you return false, the [RenderViewport]
150 /// will be laid out again with the new scroll offset. This is expensive. (The
151 /// return value is answering the question "did you accept these viewport
152 /// dimensions unconditionally?"; if the new dimensions change the
153 /// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
154 /// be laid out again.)
155 bool applyViewportDimension(double viewportDimension);
156
157 /// Called when the viewport's content extents are established.
158 ///
159 /// The arguments are the minimum and maximum scroll extents respectively. The
160 /// minimum will be equal to or less than the maximum. In the case of slivers,
161 /// the minimum will be equal to or less than zero, the maximum will be equal
162 /// to or greater than zero.
163 ///
164 /// The maximum scroll extent has the viewport dimension subtracted from it.
165 /// For instance, if there is 100.0 pixels of scrollable content, and the
166 /// viewport is 80.0 pixels high, then the minimum scroll extent will
167 /// typically be 0.0 and the maximum scroll extent will typically be 20.0,
168 /// because there's only 20.0 pixels of actual scroll slack.
169 ///
170 /// If applying the content dimensions changes the scroll offset, return
171 /// false. Otherwise, return true. If you return false, the [RenderViewport]
172 /// will be laid out again with the new scroll offset. This is expensive. (The
173 /// return value is answering the question "did you accept these content
174 /// dimensions unconditionally?"; if the new dimensions change the
175 /// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
176 /// be laid out again.)
177 ///
178 /// This is called at least once each time the [RenderViewport] is laid out,
179 /// even if the values have not changed. It may be called many times if the
180 /// scroll offset is corrected (if this returns false). This is always called
181 /// after [applyViewportDimension], if that method is called.
182 bool applyContentDimensions(double minScrollExtent, double maxScrollExtent);
183
184 /// Apply a layout-time correction to the scroll offset.
185 ///
186 /// This method should change the [pixels] value by `correction`, but without
187 /// calling [notifyListeners]. It is called during layout by the
188 /// [RenderViewport], before [applyContentDimensions]. After this method is
189 /// called, the layout will be recomputed and that may result in this method
190 /// being called again, though this should be very rare.
191 ///
192 /// See also:
193 ///
194 /// * [jumpTo], for also changing the scroll position when not in layout.
195 /// [jumpTo] applies the change immediately and notifies its listeners.
196 void correctBy(double correction);
197
198 /// Jumps [pixels] from its current value to the given value,
199 /// without animation, and without checking if the new value is in range.
200 ///
201 /// See also:
202 ///
203 /// * [correctBy], for changing the current offset in the middle of layout
204 /// and that defers the notification of its listeners until after layout.
205 void jumpTo(double pixels);
206
207 /// Animates [pixels] from its current value to the given value.
208 ///
209 /// The returned [Future] will complete when the animation ends, whether it
210 /// completed successfully or whether it was interrupted prematurely.
211 ///
212 /// The duration must not be zero. To jump to a particular value without an
213 /// animation, use [jumpTo].
214 Future<void> animateTo(
215 double to, {
216 required Duration duration,
217 required Curve curve,
218 });
219
220 /// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
221 /// [animateTo] is called.
222 ///
223 /// If [animateTo] is called then [curve] defaults to [Curves.ease]. The
224 /// [clamp] parameter is ignored by this stub implementation but subclasses
225 /// like [ScrollPosition] handle it by adjusting [to] to prevent over or
226 /// underscroll.
227 Future<void> moveTo(
228 double to, {
229 Duration? duration,
230 Curve? curve,
231 bool? clamp,
232 }) {
233 if (duration == null || duration == Duration.zero) {
234 jumpTo(to);
235 return Future<void>.value();
236 } else {
237 return animateTo(to, duration: duration, curve: curve ?? Curves.ease);
238 }
239 }
240
241 /// The direction in which the user is trying to change [pixels], relative to
242 /// the viewport's [RenderViewportBase.axisDirection].
243 ///
244 /// If the _user_ is not scrolling, this will return [ScrollDirection.idle]
245 /// even if there is (for example) a [ScrollActivity] currently animating the
246 /// position.
247 ///
248 /// This is exposed in [SliverConstraints.userScrollDirection], which is used
249 /// by some slivers to determine how to react to a change in scroll offset.
250 /// For example, [RenderSliverFloatingPersistentHeader] will only expand a
251 /// floating app bar when the [userScrollDirection] is in the positive scroll
252 /// offset direction.
253 ///
254 /// {@macro flutter.rendering.ScrollDirection.sample}
255 ScrollDirection get userScrollDirection;
256
257 /// Whether a viewport is allowed to change [pixels] implicitly to respond to
258 /// a call to [RenderObject.showOnScreen].
259 ///
260 /// [RenderObject.showOnScreen] is, for example, used to bring a text field
261 /// fully on screen after it has received focus. This property controls
262 /// whether the viewport associated with this offset is allowed to change the
263 /// offset's [pixels] value to fulfill such a request.
264 bool get allowImplicitScrolling;
265
266 @override
267 String toString() {
268 final List<String> description = <String>[];
269 debugFillDescription(description);
270 return '${describeIdentity(this)}(${description.join(", ")})';
271 }
272
273 /// Add additional information to the given description for use by [toString].
274 ///
275 /// This method makes it easier for subclasses to coordinate to provide a
276 /// high-quality [toString] implementation. The [toString] implementation on
277 /// the [State] base class calls [debugFillDescription] to collect useful
278 /// information from subclasses to incorporate into its return value.
279 ///
280 /// Implementations of this method should start with a call to the inherited
281 /// method, as in `super.debugFillDescription(description)`.
282 @mustCallSuper
283 void debugFillDescription(List<String> description) {
284 if (hasPixels) {
285 description.add('offset: ${pixels.toStringAsFixed(1)}');
286 }
287 }
288}
289
290class _FixedViewportOffset extends ViewportOffset {
291 _FixedViewportOffset(this._pixels);
292 _FixedViewportOffset.zero() : _pixels = 0.0;
293
294 double _pixels;
295
296 @override
297 double get pixels => _pixels;
298
299 @override
300 bool get hasPixels => true;
301
302 @override
303 bool applyViewportDimension(double viewportDimension) => true;
304
305 @override
306 bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) => true;
307
308 @override
309 void correctBy(double correction) {
310 _pixels += correction;
311 }
312
313 @override
314 void jumpTo(double pixels) {
315 // Do nothing, viewport is fixed.
316 }
317
318 @override
319 Future<void> animateTo(
320 double to, {
321 required Duration duration,
322 required Curve curve,
323 }) async { }
324
325 @override
326 ScrollDirection get userScrollDirection => ScrollDirection.idle;
327
328 @override
329 bool get allowImplicitScrolling => false;
330}
331