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 | import 'package:flutter/animation.dart'; |
6 | import '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. |
39 | enum 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]. |
71 | ScrollDirection 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. |
96 | abstract 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 | |
290 | class _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 | |