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 {
|
||||
_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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user