1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:newton/newton.dart';
const double _kSecondsPerMillisecond = 1000.0;
const double _kScrollDrag = 0.025;
/// An interface for controlling the behavior of scrollable widgets.
///
/// The type argument T is the type that describes the scroll offset.
/// The type argument U is the type that describes the scroll velocity.
abstract class ScrollBehavior<T, U> {
/// Returns a simulation that propels the scrollOffset.
///
/// This function is called when a drag gesture ends.
///
/// Returns null if the behavior is to do nothing.
Simulation createScrollSimulation(T position, U velocity) => null;
/// Returns an animation that ends at the snap offset.
///
/// This function is called when a drag gesture ends and a
/// [SnapOffsetCallback] is specified for the scrollable.
///
/// Returns null if the behavior is to do nothing.
Simulation createSnapScrollSimulation(T startOffset, T endOffset, U startVelocity, U endVelocity) => null;
/// Returns the scroll offset to use when the user attempts to scroll
/// from the given offset by the given delta.
T applyCurve(T scrollOffset, T scrollDelta) => scrollOffset;
/// Whether this scroll behavior currently permits scrolling
bool get isScrollable => true;
@override
String toString() {
List<String> description = <String>[];
debugFillDescription(description);
return '$runtimeType(${description.join("; ")})';
}
void debugFillDescription(List<String> description) {
description.add(isScrollable ? 'scrollable' : 'not scrollable');
}
}
/// A scroll behavior for a scrollable widget with linear extent (i.e.
/// that only scrolls along one axis).
abstract class ExtentScrollBehavior extends ScrollBehavior<double, double> {
ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: _contentExtent = contentExtent, _containerExtent = containerExtent;
/// The linear extent of the content inside the scrollable widget.
double get contentExtent => _contentExtent;
double _contentExtent;
/// The linear extent of the exterior of the scrollable widget.
double get containerExtent => _containerExtent;
double _containerExtent;
/// Updates either content or container extent (or both)
///
/// Returns the new scroll offset of the widget after the change in extent.
///
/// The [scrollOffset] parameter is the scroll offset of the widget before the
/// change in extent.
double updateExtents({
double contentExtent,
double containerExtent,
double scrollOffset: 0.0
}) {
assert(minScrollOffset <= maxScrollOffset);
if (contentExtent != null)
_contentExtent = contentExtent;
if (containerExtent != null)
_containerExtent = containerExtent;
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
}
/// The minimum value the scroll offset can obtain.
double get minScrollOffset;
/// The maximum value the scroll offset can obtain.
double get maxScrollOffset;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('content: ${contentExtent.toStringAsFixed(1)}');
description.add('container: ${contentExtent.toStringAsFixed(1)}');
description.add('range: ${minScrollOffset?.toStringAsFixed(1)} .. ${maxScrollOffset?.toStringAsFixed(1)}');
}
}
/// A scroll behavior that prevents the user from exceeding scroll bounds.
class BoundedBehavior extends ExtentScrollBehavior {
BoundedBehavior({
double contentExtent: 0.0,
double containerExtent: 0.0,
double minScrollOffset: 0.0
}) : _minScrollOffset = minScrollOffset,
super(contentExtent: contentExtent, containerExtent: containerExtent);
double _minScrollOffset;
@override
double updateExtents({
double contentExtent,
double containerExtent,
double minScrollOffset,
double scrollOffset: 0.0
}) {
if (minScrollOffset != null) {
_minScrollOffset = minScrollOffset;
assert(minScrollOffset <= maxScrollOffset);
}
return super.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
);
}
@override
double get minScrollOffset => _minScrollOffset;
@override
double get maxScrollOffset => math.max(minScrollOffset, minScrollOffset + _contentExtent - _containerExtent);
@override
double applyCurve(double scrollOffset, double scrollDelta) {
return (scrollOffset + scrollDelta).clamp(minScrollOffset, maxScrollOffset);
}
}
Simulation _createScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
final double startVelocity = velocity * _kSecondsPerMillisecond;
final SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
return new ScrollSimulation(position, startVelocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
}
Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
final double velocity = startVelocity * _kSecondsPerMillisecond;
return new FrictionSimulation.through(startOffset, endOffset, velocity, endVelocity);
}
/// A scroll behavior that does not prevent the user from exeeding scroll bounds.
class UnboundedBehavior extends ExtentScrollBehavior {
UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
@override
Simulation createScrollSimulation(double position, double velocity) {
double velocityPerSecond = velocity * 1000.0;
return new BoundedFrictionSimulation(
_kScrollDrag, position, velocityPerSecond, double.NEGATIVE_INFINITY, double.INFINITY
);
}
@override
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
}
@override
double get minScrollOffset => double.NEGATIVE_INFINITY;
@override
double get maxScrollOffset => double.INFINITY;
@override
double applyCurve(double scrollOffset, double scrollDelta) {
return scrollOffset + scrollDelta;
}
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
class OverscrollBehavior extends BoundedBehavior {
OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0, double minScrollOffset: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
@override
Simulation createScrollSimulation(double position, double velocity) {
return _createScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
}
@override
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
}
@override
double applyCurve(double scrollOffset, double scrollDelta) {
double newScrollOffset = scrollOffset + scrollDelta;
// If we're overscrolling, we want move the scroll offset 2x
// slower than we would otherwise. Therefore, we "rewind" the
// newScrollOffset by half the amount that we moved it above.
// Notice that we clamp the "old" value to 0.0 so that we only
// reduce the portion of scrollDelta that's applied beyond 0.0. We
// do similar things for overscroll in the other direction.
if (newScrollOffset < minScrollOffset) {
newScrollOffset -= (newScrollOffset - math.min(minScrollOffset, scrollOffset)) / 2.0;
} else if (newScrollOffset > maxScrollOffset) {
newScrollOffset -= (newScrollOffset - math.max(maxScrollOffset, scrollOffset)) / 2.0;
}
return newScrollOffset;
}
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
OverscrollWhenScrollableBehavior({ double contentExtent: 0.0, double containerExtent: 0.0, double minScrollOffset: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
@override
bool get isScrollable => contentExtent > containerExtent;
@override
Simulation createScrollSimulation(double position, double velocity) {
if (isScrollable || position < minScrollOffset || position > maxScrollOffset)
return super.createScrollSimulation(position, velocity);
return null;
}
@override
double applyCurve(double scrollOffset, double scrollDelta) {
if (isScrollable)
return super.applyCurve(scrollOffset, scrollDelta);
return minScrollOffset;
}
}