Unverified Commit b2af3260 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix boundaries in applyClampedDragUpdate (#14444)

parent 53b348a5
......@@ -895,6 +895,10 @@ class _NestedScrollController extends ScrollController {
}
}
// The _NestedScrollPosition is used by both the inner and outer viewports of a
// NestedScrollView. It tracks the offset to use for those viewports, and knows
// about the _NestedScrollCoordinator, so that when activities are triggered on
// this class, they can defer, or be influenced by, the coordinator.
class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDelegate {
_NestedScrollPosition({
@required ScrollPhysics physics,
......@@ -945,10 +949,31 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
}
// Returns the amount of delta that was not used.
//
// Positive delta means going down (exposing stuff above), negative delta
// going up (exposing stuff below).
double applyClampedDragUpdate(double delta) {
assert(delta != 0.0);
final double min = delta < 0.0 ? -double.INFINITY : minScrollExtent;
final double max = delta > 0.0 ? double.INFINITY : maxScrollExtent;
// If we are going towards the maxScrollExtent (negative scroll offset),
// then the furthest we can be in the minScrollExtent direction is negative
// infinity. For example, if we are already overscrolled, then scrolling to
// reduce the overscroll should not disallow the overscroll.
//
// If we are going towards the minScrollExtent (positive scroll offset),
// then the furthest we can be in the minScrollExtent direction is wherever
// we are now, if we are already overscrolled (in which case pixels is less
// than the minScrollExtent), or the minScrollExtent if we are not.
//
// In other words, we cannot, via applyClampedDragUpdate, _enter_ an
// overscroll situation.
//
// An overscroll situation might be nonetheless entered via several means.
// One is if the physics allow it, via applyFullDragUpdate (see below). An
// overscroll situation can also be forced, e.g. if the scroll position is
// artificially set using the scroll controller.
final double min = delta < 0.0 ? -double.INFINITY : math.min(minScrollExtent, pixels);
// The logic for max is equivalent but on the other side.
final double max = delta > 0.0 ? double.INFINITY : math.max(maxScrollExtent, pixels);
final double oldPixels = pixels;
final double newPixels = (pixels - delta).clamp(min, max);
final double clampedDelta = newPixels - pixels;
......
......@@ -566,4 +566,84 @@ void main() {
expect(buildCount, expectedBuildCount);
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
});
testWidgets('NestedScrollView and iOS bouncing', (WidgetTester tester) async {
// This verifies that overscroll bouncing works correctly on iOS. For
// example, this checks that if you pull to overscroll, friction is applied;
// it also makes sure that if you scroll back the other way, the scroll
// positions of the inner and outer list don't have a discontinuity.
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const Key key1 = const ValueKey<int>(1);
const Key key2 = const ValueKey<int>(2);
await tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new DefaultTabController(
length: 1,
child: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverPersistentHeader(
delegate: const TestHeader(key: key1),
),
];
},
body: new SingleChildScrollView(
child: new Container(
height: 1000.0,
child: const Placeholder(key: key2),
),
),
),
),
),
),
);
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), new Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0));
final TestGesture gesture = await tester.startGesture(const Offset(10.0, 10.0));
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll up
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, -10.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), new Rect.fromLTWH(0.0, 90.0, 800.0, 1000.0));
await gesture.moveBy(const Offset(0.0, 10.0)); // scroll back to origin
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)), new Rect.fromLTWH(0.0, 100.0, 800.0, 1000.0));
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
await gesture.moveBy(const Offset(0.0, 10.0)); // overscroll
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
expect(tester.getRect(find.byKey(key2)).top, lessThan(130.0));
await gesture.moveBy(const Offset(0.0, -1.0)); // scroll back a little
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, -1.0, 800.0, 100.0));
expect(tester.getRect(find.byKey(key2)).top, greaterThan(100.0));
expect(tester.getRect(find.byKey(key2)).top, lessThan(129.0));
await gesture.moveBy(const Offset(0.0, -10.0)); // scroll back a lot
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, -11.0, 800.0, 100.0));
await gesture.moveBy(const Offset(0.0, 20.0)); // overscroll again
await tester.pump();
expect(tester.getRect(find.byKey(key1)), new Rect.fromLTWH(0.0, 0.0, 800.0, 100.0));
await gesture.up();
debugDefaultTargetPlatformOverride = null;
});
}
class TestHeader extends SliverPersistentHeaderDelegate {
const TestHeader({ this.key });
final Key key;
@override
double get minExtent => 100.0;
@override
double get maxExtent => 100.0;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Placeholder(key: key);
}
@override
bool shouldRebuild(TestHeader oldDelegate) => false;
}
\ No newline at end of file
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