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 'dart:math' as math; |
6 | |
7 | import 'package:flutter/gestures.dart'; |
8 | import 'package:flutter/physics.dart'; |
9 | import 'package:flutter/rendering.dart'; |
10 | |
11 | import 'basic.dart'; |
12 | import 'framework.dart'; |
13 | import 'scroll_activity.dart'; |
14 | import 'scroll_context.dart'; |
15 | import 'scroll_notification.dart'; |
16 | import 'scroll_physics.dart'; |
17 | import 'scroll_position.dart'; |
18 | |
19 | /// A scroll position that manages scroll activities for a single |
20 | /// [ScrollContext]. |
21 | /// |
22 | /// This class is a concrete subclass of [ScrollPosition] logic that handles a |
23 | /// single [ScrollContext], such as a [Scrollable]. An instance of this class |
24 | /// manages [ScrollActivity] instances, which change what content is visible in |
25 | /// the [Scrollable]'s [Viewport]. |
26 | /// |
27 | /// {@macro flutter.widgets.scrollPosition.listening} |
28 | /// |
29 | /// See also: |
30 | /// |
31 | /// * [ScrollPosition], which defines the underlying model for a position |
32 | /// within a [Scrollable] but is agnostic as to how that position is |
33 | /// changed. |
34 | /// * [ScrollView] and its subclasses such as [ListView], which use |
35 | /// [ScrollPositionWithSingleContext] to manage their scroll position. |
36 | /// * [ScrollController], which can manipulate one or more [ScrollPosition]s, |
37 | /// and which uses [ScrollPositionWithSingleContext] as its default class for |
38 | /// scroll positions. |
39 | class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollActivityDelegate { |
40 | /// Create a [ScrollPosition] object that manages its behavior using |
41 | /// [ScrollActivity] objects. |
42 | /// |
43 | /// The `initialPixels` argument can be null, but in that case it is |
44 | /// imperative that the value be set, using [correctPixels], as soon as |
45 | /// [applyNewDimensions] is invoked, before calling the inherited |
46 | /// implementation of that method. |
47 | /// |
48 | /// If [keepScrollOffset] is true (the default), the current scroll offset is |
49 | /// saved with [PageStorage] and restored it if this scroll position's scrollable |
50 | /// is recreated. |
51 | ScrollPositionWithSingleContext({ |
52 | required super.physics, |
53 | required super.context, |
54 | double? initialPixels = 0.0, |
55 | super.keepScrollOffset, |
56 | super.oldPosition, |
57 | super.debugLabel, |
58 | }) { |
59 | // If oldPosition is not null, the superclass will first call absorb(), |
60 | // which may set _pixels and _activity. |
61 | if (!hasPixels && initialPixels != null) { |
62 | correctPixels(initialPixels); |
63 | } |
64 | if (activity == null) { |
65 | goIdle(); |
66 | } |
67 | assert(activity != null); |
68 | } |
69 | |
70 | /// Velocity from a previous activity temporarily held by [hold] to potentially |
71 | /// transfer to a next activity. |
72 | double _heldPreviousVelocity = 0.0; |
73 | |
74 | @override |
75 | AxisDirection get axisDirection => context.axisDirection; |
76 | |
77 | @override |
78 | double setPixels(double newPixels) { |
79 | assert(activity!.isScrolling); |
80 | return super.setPixels(newPixels); |
81 | } |
82 | |
83 | @override |
84 | void absorb(ScrollPosition other) { |
85 | super.absorb(other); |
86 | if (other is! ScrollPositionWithSingleContext) { |
87 | goIdle(); |
88 | return; |
89 | } |
90 | activity!.updateDelegate(this); |
91 | _userScrollDirection = other._userScrollDirection; |
92 | assert(_currentDrag == null); |
93 | if (other._currentDrag != null) { |
94 | _currentDrag = other._currentDrag; |
95 | _currentDrag!.updateDelegate(this); |
96 | other._currentDrag = null; |
97 | } |
98 | } |
99 | |
100 | @override |
101 | void applyNewDimensions() { |
102 | super.applyNewDimensions(); |
103 | context.setCanDrag(physics.shouldAcceptUserOffset(this)); |
104 | } |
105 | |
106 | @override |
107 | void beginActivity(ScrollActivity? newActivity) { |
108 | _heldPreviousVelocity = 0.0; |
109 | if (newActivity == null) { |
110 | return; |
111 | } |
112 | assert(newActivity.delegate == this); |
113 | super.beginActivity(newActivity); |
114 | _currentDrag?.dispose(); |
115 | _currentDrag = null; |
116 | if (!activity!.isScrolling) { |
117 | updateUserScrollDirection(ScrollDirection.idle); |
118 | } |
119 | } |
120 | |
121 | @override |
122 | void applyUserOffset(double delta) { |
123 | updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse); |
124 | setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta)); |
125 | } |
126 | |
127 | @override |
128 | void goIdle() { |
129 | beginActivity(IdleScrollActivity(this)); |
130 | } |
131 | |
132 | /// Start a physics-driven simulation that settles the [pixels] position, |
133 | /// starting at a particular velocity. |
134 | /// |
135 | /// This method defers to [ScrollPhysics.createBallisticSimulation], which |
136 | /// typically provides a bounce simulation when the current position is out of |
137 | /// bounds and a friction simulation when the position is in bounds but has a |
138 | /// non-zero velocity. |
139 | /// |
140 | /// The velocity should be in logical pixels per second. |
141 | @override |
142 | void goBallistic(double velocity) { |
143 | assert(hasPixels); |
144 | final Simulation? simulation = physics.createBallisticSimulation(this, velocity); |
145 | if (simulation != null) { |
146 | beginActivity(BallisticScrollActivity( |
147 | this, |
148 | simulation, |
149 | context.vsync, |
150 | activity?.shouldIgnorePointer ?? true, |
151 | )); |
152 | } else { |
153 | goIdle(); |
154 | } |
155 | } |
156 | |
157 | @override |
158 | ScrollDirection get userScrollDirection => _userScrollDirection; |
159 | ScrollDirection _userScrollDirection = ScrollDirection.idle; |
160 | |
161 | /// Set [userScrollDirection] to the given value. |
162 | /// |
163 | /// If this changes the value, then a [UserScrollNotification] is dispatched. |
164 | @protected |
165 | @visibleForTesting |
166 | void updateUserScrollDirection(ScrollDirection value) { |
167 | if (userScrollDirection == value) { |
168 | return; |
169 | } |
170 | _userScrollDirection = value; |
171 | didUpdateScrollDirection(value); |
172 | } |
173 | |
174 | @override |
175 | Future<void> animateTo( |
176 | double to, { |
177 | required Duration duration, |
178 | required Curve curve, |
179 | }) { |
180 | if (nearEqual(to, pixels, physics.toleranceFor(this).distance)) { |
181 | // Skip the animation, go straight to the position as we are already close. |
182 | jumpTo(to); |
183 | return Future<void>.value(); |
184 | } |
185 | |
186 | final DrivenScrollActivity activity = DrivenScrollActivity( |
187 | this, |
188 | from: pixels, |
189 | to: to, |
190 | duration: duration, |
191 | curve: curve, |
192 | vsync: context.vsync, |
193 | ); |
194 | beginActivity(activity); |
195 | return activity.done; |
196 | } |
197 | |
198 | @override |
199 | void jumpTo(double value) { |
200 | goIdle(); |
201 | if (pixels != value) { |
202 | final double oldPixels = pixels; |
203 | forcePixels(value); |
204 | didStartScroll(); |
205 | didUpdateScrollPositionBy(pixels - oldPixels); |
206 | didEndScroll(); |
207 | } |
208 | goBallistic(0.0); |
209 | } |
210 | |
211 | @override |
212 | void pointerScroll(double delta) { |
213 | // If an update is made to pointer scrolling here, consider if the same |
214 | // (or similar) change should be made in |
215 | // _NestedScrollCoordinator.pointerScroll. |
216 | if (delta == 0.0) { |
217 | goBallistic(0.0); |
218 | return; |
219 | } |
220 | |
221 | final double targetPixels = |
222 | math.min(math.max(pixels + delta, minScrollExtent), maxScrollExtent); |
223 | if (targetPixels != pixels) { |
224 | goIdle(); |
225 | updateUserScrollDirection( |
226 | -delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse, |
227 | ); |
228 | final double oldPixels = pixels; |
229 | // Set the notifier before calling force pixels. |
230 | // This is set to false again after going ballistic below. |
231 | isScrollingNotifier.value = true; |
232 | forcePixels(targetPixels); |
233 | didStartScroll(); |
234 | didUpdateScrollPositionBy(pixels - oldPixels); |
235 | didEndScroll(); |
236 | goBallistic(0.0); |
237 | } |
238 | } |
239 | |
240 | |
241 | @Deprecated('This will lead to bugs.' ) // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/44609 |
242 | @override |
243 | void jumpToWithoutSettling(double value) { |
244 | goIdle(); |
245 | if (pixels != value) { |
246 | final double oldPixels = pixels; |
247 | forcePixels(value); |
248 | didStartScroll(); |
249 | didUpdateScrollPositionBy(pixels - oldPixels); |
250 | didEndScroll(); |
251 | } |
252 | } |
253 | |
254 | @override |
255 | ScrollHoldController hold(VoidCallback holdCancelCallback) { |
256 | final double previousVelocity = activity!.velocity; |
257 | final HoldScrollActivity holdActivity = HoldScrollActivity( |
258 | delegate: this, |
259 | onHoldCanceled: holdCancelCallback, |
260 | ); |
261 | beginActivity(holdActivity); |
262 | _heldPreviousVelocity = previousVelocity; |
263 | return holdActivity; |
264 | } |
265 | |
266 | ScrollDragController? _currentDrag; |
267 | |
268 | @override |
269 | Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) { |
270 | final ScrollDragController drag = ScrollDragController( |
271 | delegate: this, |
272 | details: details, |
273 | onDragCanceled: dragCancelCallback, |
274 | carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity), |
275 | motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold, |
276 | ); |
277 | beginActivity(DragScrollActivity(this, drag)); |
278 | assert(_currentDrag == null); |
279 | _currentDrag = drag; |
280 | return drag; |
281 | } |
282 | |
283 | @override |
284 | void dispose() { |
285 | _currentDrag?.dispose(); |
286 | _currentDrag = null; |
287 | super.dispose(); |
288 | } |
289 | |
290 | @override |
291 | void debugFillDescription(List<String> description) { |
292 | super.debugFillDescription(description); |
293 | description.add(' ${context.runtimeType}' ); |
294 | description.add(' $physics' ); |
295 | description.add(' $activity' ); |
296 | description.add(' $userScrollDirection' ); |
297 | } |
298 | } |
299 | |