Commit f7f1259b authored by Adam Barth's avatar Adam Barth

Scrollable physics should be reasonable when sizes change

Previously, when the content extent changed during a scroll interaction, we'd
stop the current scroll interaction and reset the scroll offset. Now we try to
continue the scroll interaction (e.g., drag, fling, or overscroll) even through
the underlying scroll behavior has changed.

For physics-based scroll interactions, we keep the current position and
velocity and recompute the operative forces. For drag interactions, we keep the
current position and continue to let the user drag the scroll offset.

After this patch, we still disrupt non-physical scroll animations that are
operating outside the new scroll bounds because it's not clear how we can
sensibly modify them to work with the new scroll bounds.
parent 9cce0833
...@@ -124,6 +124,12 @@ class AnimationController extends Animation<double> ...@@ -124,6 +124,12 @@ class AnimationController extends Animation<double>
_checkStatusChanged(); _checkStatusChanged();
} }
/// The amount of time that has passed between the time the animation started and the most recent tick of the animation.
///
/// If the controller is not animating, the last elapsed duration is null;
Duration get lastElapsedDuration => _lastElapsedDuration;
Duration _lastElapsedDuration;
/// Whether this animation is currently animating in either the forward or reverse direction. /// Whether this animation is currently animating in either the forward or reverse direction.
bool get isAnimating => _ticker.isTicking; bool get isAnimating => _ticker.isTicking;
...@@ -205,6 +211,7 @@ class AnimationController extends Animation<double> ...@@ -205,6 +211,7 @@ class AnimationController extends Animation<double>
assert(simulation != null); assert(simulation != null);
assert(!isAnimating); assert(!isAnimating);
_simulation = simulation; _simulation = simulation;
_lastElapsedDuration = const Duration();
_value = simulation.x(0.0).clamp(lowerBound, upperBound); _value = simulation.x(0.0).clamp(lowerBound, upperBound);
Future<Null> result = _ticker.start(); Future<Null> result = _ticker.start();
_checkStatusChanged(); _checkStatusChanged();
...@@ -214,6 +221,7 @@ class AnimationController extends Animation<double> ...@@ -214,6 +221,7 @@ class AnimationController extends Animation<double>
/// Stops running this animation. /// Stops running this animation.
void stop() { void stop() {
_simulation = null; _simulation = null;
_lastElapsedDuration = null;
_ticker.stop(); _ticker.stop();
} }
...@@ -233,6 +241,7 @@ class AnimationController extends Animation<double> ...@@ -233,6 +241,7 @@ class AnimationController extends Animation<double>
} }
void _tick(Duration elapsed) { void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND; double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound); _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) if (_simulation.isDone(elapsedInSeconds))
......
...@@ -768,7 +768,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect ...@@ -768,7 +768,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
} }
void _updateScrollBehavior() { void _updateScrollBehavior() {
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width, containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width,
contentExtent: _tabWidths.reduce((double sum, double width) => sum + width), contentExtent: _tabWidths.reduce((double sum, double width) => sum + width),
scrollOffset: scrollOffset scrollOffset: scrollOffset
...@@ -943,11 +943,11 @@ class _TabBarViewState<T> extends PageableListState<TabBarView<T>> implements Ta ...@@ -943,11 +943,11 @@ class _TabBarViewState<T> extends PageableListState<TabBarView<T>> implements Ta
void _updateScrollBehaviorForSelectedIndex(int selectedIndex) { void _updateScrollBehaviorForSelectedIndex(int selectedIndex) {
if (selectedIndex == 0) { if (selectedIndex == 0) {
scrollTo(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0)); didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0));
} else if (selectedIndex == _tabCount - 1) { } else if (selectedIndex == _tabCount - 1) {
scrollTo(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0)); didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0));
} else { } else {
scrollTo(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0)); didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0));
} }
} }
......
...@@ -235,7 +235,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -235,7 +235,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
// render object via our return value. // render object via our return value.
_containerWidth = dimensions.containerSize.width; _containerWidth = dimensions.containerSize.width;
_contentWidth = dimensions.contentSize.width; _contentWidth = dimensions.contentSize.width;
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: _contentWidth, contentExtent: _contentWidth,
containerExtent: _containerWidth, containerExtent: _containerWidth,
// Set the scroll offset to match the content width so that the // Set the scroll offset to match the content width so that the
......
...@@ -147,7 +147,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> { ...@@ -147,7 +147,7 @@ class PageableListState<T extends PageableList> extends ScrollableState<T> {
void _updateScrollBehavior() { void _updateScrollBehavior() {
config.scrollableListPainter?.contentExtent = _itemCount.toDouble(); config.scrollableListPainter?.contentExtent = _itemCount.toDouble();
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: _itemCount.toDouble(), contentExtent: _itemCount.toDouble(),
containerExtent: 1.0, containerExtent: 1.0,
scrollOffset: scrollOffset scrollOffset: scrollOffset
......
...@@ -226,10 +226,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -226,10 +226,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
_scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0; _scrollOffset = PageStorage.of(context)?.readState(context) ?? config.initialScrollOffset ?? 0.0;
} }
Simulation _simulation;
AnimationController _controller; AnimationController _controller;
@override @override
void dispose() { void dispose() {
_simulation = null;
_controller.stop(); _controller.stop();
super.dispose(); super.dispose();
} }
...@@ -358,6 +360,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -358,6 +360,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return new Future<Null>.value(); return new Future<Null>.value();
if (duration == null) { if (duration == null) {
_simulation = null;
_controller.stop(); _controller.stop();
_setScrollOffset(newScrollOffset); _setScrollOffset(newScrollOffset);
return new Future<Null>.value(); return new Future<Null>.value();
...@@ -368,12 +371,27 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -368,12 +371,27 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
Future<Null> _animateTo(double newScrollOffset, Duration duration, Curve curve) { Future<Null> _animateTo(double newScrollOffset, Duration duration, Curve curve) {
_simulation = null;
_controller.stop(); _controller.stop();
_controller.value = scrollOffset; _controller.value = scrollOffset;
_startScroll(); _startScroll();
return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_endScroll); return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_endScroll);
} }
void didUpdateScrollBehavior(double newScrollOffset) {
if (newScrollOffset == _scrollOffset)
return;
if (_numberOfInProgressScrolls > 0) {
if (_simulation != null) {
double dx = _simulation.dx(_controller.lastElapsedDuration.inMicroseconds / Duration.MICROSECONDS_PER_SECOND);
// TODO(abarth): We should be consistent about the units we use for velocity (i.e., per second).
_startToEndAnimation(dx / Duration.MILLISECONDS_PER_SECOND);
}
return;
}
scrollTo(newScrollOffset);
}
/// Fling the scroll offset with the given velocity. /// Fling the scroll offset with the given velocity.
/// ///
/// Calling this function starts a physics-based animation of the scroll /// Calling this function starts a physics-based animation of the scroll
...@@ -390,17 +408,18 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -390,17 +408,18 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// Calling this function starts a physics-based animation of the scroll /// Calling this function starts a physics-based animation of the scroll
/// offset either to a snap point or to within the scrolling bounds. The /// offset either to a snap point or to within the scrolling bounds. The
/// physics simulation used is determined by the scroll behavior. /// physics simulation used is determined by the scroll behavior.
Future<Null> settleScrollOffset() { Future<Null> settleScrollOffset() {
return _startToEndAnimation(0.0); return _startToEndAnimation(0.0);
} }
Future<Null> _startToEndAnimation(double scrollVelocity) { Future<Null> _startToEndAnimation(double scrollVelocity) {
_simulation = null;
_controller.stop(); _controller.stop();
Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity); _simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
if (simulation == null) if (_simulation == null)
return new Future<Null>.value(); return new Future<Null>.value();
_startScroll(); _startScroll();
return _controller.animateWith(simulation).then(_endScroll); return _controller.animateWith(_simulation).then(_endScroll);
} }
/// Whether this scrollable should attempt to snap scroll offsets. /// Whether this scrollable should attempt to snap scroll offsets.
...@@ -471,6 +490,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -471,6 +490,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
void _handleDragDown(_) { void _handleDragDown(_) {
_simulation = null;
_controller.stop(); _controller.stop();
} }
...@@ -653,7 +673,7 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> { ...@@ -653,7 +673,7 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
// render object via our return value. // render object via our return value.
_viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width; _viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width;
_childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width; _childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width;
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: _childSize, contentExtent: _childSize,
containerExtent: _viewportSize, containerExtent: _viewportSize,
scrollOffset: scrollOffset scrollOffset: scrollOffset
...@@ -819,7 +839,7 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg ...@@ -819,7 +839,7 @@ class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidg
// setState() callback because we are called during layout and all // setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the // we're updating is the new offset, which we are providing to the
// render object via our return value. // render object via our return value.
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: dimensions.contentSize.height, contentExtent: dimensions.contentSize.height,
containerExtent: dimensions.containerSize.height, containerExtent: dimensions.containerSize.height,
scrollOffset: scrollOffset scrollOffset: scrollOffset
......
...@@ -50,7 +50,7 @@ class _ScrollableGridState extends ScrollableState<ScrollableGrid> { ...@@ -50,7 +50,7 @@ class _ScrollableGridState extends ScrollableState<ScrollableGrid> {
void _handleExtentsChanged(double contentExtent, double containerExtent) { void _handleExtentsChanged(double contentExtent, double containerExtent) {
setState(() { setState(() {
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: contentExtent, contentExtent: contentExtent,
containerExtent: containerExtent, containerExtent: containerExtent,
scrollOffset: scrollOffset scrollOffset: scrollOffset
......
...@@ -55,7 +55,7 @@ class _ScrollableListState extends ScrollableState<ScrollableList> { ...@@ -55,7 +55,7 @@ class _ScrollableListState extends ScrollableState<ScrollableList> {
void _handleExtentsChanged(double contentExtent, double containerExtent) { void _handleExtentsChanged(double contentExtent, double containerExtent) {
config.scrollableListPainter?.contentExtent = contentExtent; config.scrollableListPainter?.contentExtent = contentExtent;
setState(() { setState(() {
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: config.itemsWrap ? double.INFINITY : contentExtent, contentExtent: config.itemsWrap ? double.INFINITY : contentExtent,
containerExtent: containerExtent, containerExtent: containerExtent,
scrollOffset: scrollOffset scrollOffset: scrollOffset
...@@ -338,7 +338,7 @@ class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> { ...@@ -338,7 +338,7 @@ class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
void _handleExtentsChanged(double contentExtent, double containerExtent) { void _handleExtentsChanged(double contentExtent, double containerExtent) {
config.scrollableListPainter?.contentExtent = contentExtent; config.scrollableListPainter?.contentExtent = contentExtent;
setState(() { setState(() {
scrollTo(scrollBehavior.updateExtents( didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: contentExtent, contentExtent: contentExtent,
containerExtent: containerExtent, containerExtent: containerExtent,
scrollOffset: scrollOffset scrollOffset: scrollOffset
......
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