Commit 688da091 authored by Adam Barth's avatar Adam Barth

Flinging continuously should have one scroll start/end pair

Previously we got confused and started sending start/end pairs for each tick of
the fling animation.

Fixes #2430
parent 66859545
...@@ -327,15 +327,9 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -327,15 +327,9 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
}); });
PageStorage.of(context)?.writeState(context, _scrollOffset); PageStorage.of(context)?.writeState(context, _scrollOffset);
new ScrollNotification(this, _scrollOffset).dispatch(context); new ScrollNotification(this, _scrollOffset).dispatch(context);
final needsScrollStart = !_isBetweenOnScrollStartAndOnScrollEnd; _startScroll();
if (needsScrollStart) {
dispatchOnScrollStart();
assert(_isBetweenOnScrollStartAndOnScrollEnd);
}
dispatchOnScroll(); dispatchOnScroll();
assert(_isBetweenOnScrollStartAndOnScrollEnd); _endScroll();
if (needsScrollStart)
dispatchOnScrollEnd();
} }
/// Scroll this widget by the given scroll delta. /// Scroll this widget by the given scroll delta.
...@@ -372,8 +366,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -372,8 +366,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Future _animateTo(double newScrollOffset, Duration duration, Curve curve) { Future _animateTo(double newScrollOffset, Duration duration, Curve curve) {
_controller.stop(); _controller.stop();
_controller.value = scrollOffset; _controller.value = scrollOffset;
_dispatchOnScrollStartIfNeeded(); _startScroll();
return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_dispatchOnScrollEndIfNeeded); return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_endScroll);
} }
/// Fling the scroll offset with the given velocity. /// Fling the scroll offset with the given velocity.
...@@ -401,8 +395,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -401,8 +395,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity); Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
if (simulation == null) if (simulation == null)
return new Future.value(); return new Future.value();
_dispatchOnScrollStartIfNeeded(); _startScroll();
return _controller.animateWith(simulation).then(_dispatchOnScrollEndIfNeeded); return _controller.animateWith(simulation).then(_endScroll);
} }
/// Whether this scrollable should attempt to snap scroll offsets. /// Whether this scrollable should attempt to snap scroll offsets.
...@@ -453,14 +447,21 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -453,14 +447,21 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return simulation; return simulation;
} }
// When we start an scroll animation, we stop any previous scroll animation.
bool _isBetweenOnScrollStartAndOnScrollEnd = false; // However, the code that would deliver the onScrollEnd callback is watching
// for animations to end using a Future that resolves at the end of the
// microtask. That causes animations to "overlap" between the time we start a
// new animation and the end of the microtask. By the time the microtask is
// over and we check whether to deliver an onScrollEnd callback, we will have
// started the new animation (having skipped the onScrollStart) and therefore
// we won't deliver the onScrollEnd until the second animation is finished.
int _numberOfInProgressScrolls = 0;
/// Calls the onScroll callback. /// Calls the onScroll callback.
/// ///
/// Subclasses can override this function to hook the scroll callback. /// Subclasses can override this function to hook the scroll callback.
void dispatchOnScroll() { void dispatchOnScroll() {
assert(_isBetweenOnScrollStartAndOnScrollEnd); assert(_numberOfInProgressScrolls > 0);
if (config.onScroll != null) if (config.onScroll != null)
config.onScroll(_scrollOffset); config.onScroll(_scrollOffset);
} }
...@@ -470,11 +471,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -470,11 +471,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
void _handleDragStart(_) { void _handleDragStart(_) {
_dispatchOnScrollStartIfNeeded(); _startScroll();
} }
void _dispatchOnScrollStartIfNeeded() { void _startScroll() {
if (!_isBetweenOnScrollStartAndOnScrollEnd) _numberOfInProgressScrolls += 1;
if (_numberOfInProgressScrolls == 1)
dispatchOnScrollStart(); dispatchOnScrollStart();
} }
...@@ -482,8 +484,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -482,8 +484,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// ///
/// Subclasses can override this function to hook the scroll start callback. /// Subclasses can override this function to hook the scroll start callback.
void dispatchOnScrollStart() { void dispatchOnScrollStart() {
assert(!_isBetweenOnScrollStartAndOnScrollEnd); assert(_numberOfInProgressScrolls == 1);
_isBetweenOnScrollStartAndOnScrollEnd = true;
if (config.onScrollStart != null) if (config.onScrollStart != null)
config.onScrollStart(_scrollOffset); config.onScrollStart(_scrollOffset);
} }
...@@ -495,11 +496,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -495,11 +496,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Future _handleDragEnd(Velocity velocity) { Future _handleDragEnd(Velocity velocity) {
double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND; double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_dispatchOnScrollEndIfNeeded); return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_endScroll);
} }
void _dispatchOnScrollEndIfNeeded(_) { void _endScroll([_]) {
if (_isBetweenOnScrollStartAndOnScrollEnd) _numberOfInProgressScrolls -= 1;
if (_numberOfInProgressScrolls == 0)
dispatchOnScrollEnd(); dispatchOnScrollEnd();
} }
...@@ -507,8 +509,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -507,8 +509,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// ///
/// Subclasses can override this function to hook the scroll end callback. /// Subclasses can override this function to hook the scroll end callback.
void dispatchOnScrollEnd() { void dispatchOnScrollEnd() {
assert(_isBetweenOnScrollStartAndOnScrollEnd); assert(_numberOfInProgressScrolls == 0);
_isBetweenOnScrollStartAndOnScrollEnd = false;
if (config.onScrollEnd != null) if (config.onScrollEnd != null)
config.onScrollEnd(_scrollOffset); config.onScrollEnd(_scrollOffset);
} }
......
...@@ -95,4 +95,45 @@ void main() { ...@@ -95,4 +95,45 @@ void main() {
expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend'])); expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend']));
}); });
}); });
test('Scroll during animation', () {
testWidgets((WidgetTester tester) {
GlobalKey<ScrollableState> scrollKey = new GlobalKey<ScrollableState>();
List<String> log = <String>[];
tester.pumpWidget(_buildScroller(key: scrollKey, log: log));
expect(log, equals([]));
scrollKey.currentState.scrollTo(100.0, duration: const Duration(seconds: 1));
expect(log, equals(['scrollstart']));
tester.pump(const Duration(milliseconds: 100));
expect(log, equals(['scrollstart']));
tester.pump(const Duration(milliseconds: 100));
expect(log, equals(['scrollstart', 'scroll']));
scrollKey.currentState.scrollTo(100.0, duration: const Duration(seconds: 1));
expect(log, equals(['scrollstart', 'scroll']));
tester.pump(const Duration(milliseconds: 100));
expect(log, equals(['scrollstart', 'scroll']));
tester.pump(const Duration(milliseconds: 1500));
expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend']));
});
});
test('fling, fling generates one start/end pair', () {
testWidgets((WidgetTester tester) {
GlobalKey<ScrollableState> scrollKey = new GlobalKey<ScrollableState>();
List<String> log = <String>[];
tester.pumpWidget(_buildScroller(key: scrollKey, log: log));
expect(log, equals([]));
tester.flingFrom(new Point(100.0, 100.0), new Offset(-50.0, -50.0), 500.0);
tester.pump(new Duration(seconds: 1));
log.removeWhere((String value) => value == 'scroll');
expect(log, equals(['scrollstart']));
tester.flingFrom(new Point(100.0, 100.0), new Offset(-50.0, -50.0), 500.0);
tester.pump(new Duration(seconds: 1));
tester.pump(new Duration(seconds: 1));
log.removeWhere((String value) => value == 'scroll');
expect(log, equals(['scrollstart', 'scrollend']));
});
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment