Fix boundaries in applyClampedDragUpdate (#14444)
This commit is contained in:
parent
53b348a50d
commit
b2af326046
@ -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 {
|
class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDelegate {
|
||||||
_NestedScrollPosition({
|
_NestedScrollPosition({
|
||||||
@required ScrollPhysics physics,
|
@required ScrollPhysics physics,
|
||||||
@ -945,10 +949,31 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the amount of delta that was not used.
|
// 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) {
|
double applyClampedDragUpdate(double delta) {
|
||||||
assert(delta != 0.0);
|
assert(delta != 0.0);
|
||||||
final double min = delta < 0.0 ? -double.INFINITY : minScrollExtent;
|
// If we are going towards the maxScrollExtent (negative scroll offset),
|
||||||
final double max = delta > 0.0 ? double.INFINITY : maxScrollExtent;
|
// 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 oldPixels = pixels;
|
||||||
final double newPixels = (pixels - delta).clamp(min, max);
|
final double newPixels = (pixels - delta).clamp(min, max);
|
||||||
final double clampedDelta = newPixels - pixels;
|
final double clampedDelta = newPixels - pixels;
|
||||||
|
@ -566,4 +566,84 @@ void main() {
|
|||||||
expect(buildCount, expectedBuildCount);
|
expect(buildCount, expectedBuildCount);
|
||||||
expect(find.byType(NestedScrollView), isNot(paints..shadow()));
|
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;
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user