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 'dart:math' as math;
6
7import 'package:flutter/gestures.dart';
8import 'package:flutter/physics.dart';
9import 'package:flutter/rendering.dart';
10
11import 'basic.dart';
12import 'framework.dart';
13import 'scroll_activity.dart';
14import 'scroll_context.dart';
15import 'scroll_notification.dart';
16import 'scroll_physics.dart';
17import '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.
39class 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