diff --git a/packages/flutter/lib/src/widgets/scroll_physics.dart b/packages/flutter/lib/src/widgets/scroll_physics.dart index 1dd609006cc..c6329248e54 100644 --- a/packages/flutter/lib/src/widgets/scroll_physics.dart +++ b/packages/flutter/lib/src/widgets/scroll_physics.dart @@ -226,33 +226,51 @@ class BouncingScrollPhysics extends ScrollPhysics { /// The multiple applied to overscroll to make it appear that scrolling past /// the edge of the scrollable contents is harder than scrolling the list. + /// This is done by reducing the ratio of the scroll effect output vs the + /// scroll gesture input. /// - /// By default this is 0.5, meaning that overscroll is twice as hard as normal - /// scroll. - double get frictionFactor => 0.5; + /// This factor starts at 0.52 and progressively becomes harder to overscroll + /// as more of the area past the edge is dragged in (represented by a reducing + /// `inViewFraction` which starts at 1 when there is no overscroll). + double frictionFactor(double inViewFraction) => 0.52 * math.pow(inViewFraction.abs(), 2); @override double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { assert(offset != 0.0); assert(position.minScrollExtent <= position.maxScrollExtent); - if (offset > 0.0) - return _applyFriction(position.pixels, position.minScrollExtent, position.maxScrollExtent, offset, frictionFactor); - return -_applyFriction(-position.pixels, -position.maxScrollExtent, -position.minScrollExtent, -offset, frictionFactor); + + if (!position.outOfRange) + return offset; + + final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0); + final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0); + final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) + || (overscrollPastEnd > 0.0 && offset > 0.0); + + final double friction = easing + // Apply less resistance when easing the overscroll vs tensioning. + ? frictionFactor(math.min((position.extentInside + offset.abs()) / position.viewportDimension, 1.0)) + : frictionFactor(position.extentInside / position.viewportDimension); + final double direction = offset.sign; + + return direction * _applyFriction( + math.max(overscrollPastStart, overscrollPastEnd), + offset.abs(), + friction, + ); } - static double _applyFriction(double start, double lowLimit, double highLimit, double delta, double gamma) { - assert(lowLimit <= highLimit); - assert(delta > 0.0); + static double _applyFriction(double extentOutside, double absDelta, double gamma) { + assert(absDelta > 0); double total = 0.0; - if (start < lowLimit) { - final double distanceToLimit = lowLimit - start; - final double deltaToLimit = distanceToLimit / gamma; - if (delta < deltaToLimit) - return total + delta * gamma; - total += distanceToLimit; - delta -= deltaToLimit; + if (extentOutside > 0) { + final double deltaToLimit = extentOutside / gamma; + if (absDelta < deltaToLimit) + return absDelta * gamma; + total += extentOutside; + absDelta -= deltaToLimit; } - return total + delta; + return total + absDelta; } @override diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 07f4ea8922d..4220366b296 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -93,16 +93,18 @@ void main() { expect(leftOf(0), equals(0.0)); expect(sizeOf(0), equals(const Size(800.0, 600.0))); + // Going into overscroll. await tester.drag(find.byType(PageView), const Offset(100.0, 0.0)); await tester.pump(); - expect(leftOf(0), equals(100.0)); + expect(leftOf(0), greaterThan(0.0)); expect(sizeOf(0), equals(const Size(800.0, 600.0))); + // Easing overscroll past overscroll limit. await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0)); await tester.pump(); - expect(leftOf(0), equals(-100.0)); + expect(leftOf(0), lessThan(0.0)); expect(sizeOf(0), equals(const Size(800.0, 600.0))); }); diff --git a/packages/flutter/test/widgets/scroll_physics_test.dart b/packages/flutter/test/widgets/scroll_physics_test.dart index a2ce1399f68..cc753891bdc 100644 --- a/packages/flutter/test/widgets/scroll_physics_test.dart +++ b/packages/flutter/test/widgets/scroll_physics_test.dart @@ -75,4 +75,91 @@ void main() { expect(types(page.applyTo(bounce.applyTo(clamp.applyTo(never.applyTo(always))))), 'PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics'); }); + + group('BouncingScrollPhysics test', () { + BouncingScrollPhysics physicsUnderTest; + + setUp(() { + physicsUnderTest = const BouncingScrollPhysics(); + }); + + test('overscroll is progressively harder', () { + final ScrollMetrics lessOverscrolledPosition = new FixedScrollMetrics( + minScrollExtent: 0.0, + maxScrollExtent: 1000.0, + pixels: -20.0, + viewportDimension: 100.0, + axisDirection: AxisDirection.down, + ); + + final ScrollMetrics moreOverscrolledPosition = new FixedScrollMetrics( + minScrollExtent: 0.0, + maxScrollExtent: 1000.0, + pixels: -40.0, + viewportDimension: 100.0, + axisDirection: AxisDirection.down, + ); + + final double lessOverscrollApplied = + physicsUnderTest.applyPhysicsToUserOffset(lessOverscrolledPosition, 10.0); + + final double moreOverscrollApplied = + physicsUnderTest.applyPhysicsToUserOffset(moreOverscrolledPosition, 10.0); + + expect(lessOverscrollApplied, greaterThan(1.0)); + expect(lessOverscrollApplied, lessThan(20.0)); + + expect(moreOverscrollApplied, greaterThan(1.0)); + expect(moreOverscrollApplied, lessThan(20.0)); + + // Scrolling from a more overscrolled position meets more resistance. + expect(lessOverscrollApplied.abs(), greaterThan(moreOverscrollApplied.abs())); + }); + + test('easing an overscroll still has resistance', () { + final ScrollMetrics overscrolledPosition = new FixedScrollMetrics( + minScrollExtent: 0.0, + maxScrollExtent: 1000.0, + pixels: -20.0, + viewportDimension: 100.0, + axisDirection: AxisDirection.down, + ); + + final double easingApplied = + physicsUnderTest.applyPhysicsToUserOffset(overscrolledPosition, -10.0); + + expect(easingApplied, lessThan(-1.0)); + expect(easingApplied, greaterThan(-10.0)); + }); + + test('no resistance when not overscrolled', () { + final ScrollMetrics scrollPosition = new FixedScrollMetrics( + minScrollExtent: 0.0, + maxScrollExtent: 1000.0, + pixels: 300.0, + viewportDimension: 100.0, + axisDirection: AxisDirection.down, + ); + + expect(physicsUnderTest.applyPhysicsToUserOffset(scrollPosition, 10.0), 10.0); + expect(physicsUnderTest.applyPhysicsToUserOffset(scrollPosition, -10.0), -10.0); + }); + + test('easing an overscroll meets less resistance than tensioning', () { + final ScrollMetrics overscrolledPosition = new FixedScrollMetrics( + minScrollExtent: 0.0, + maxScrollExtent: 1000.0, + pixels: -20.0, + viewportDimension: 100.0, + axisDirection: AxisDirection.down, + ); + + final double easingApplied = + physicsUnderTest.applyPhysicsToUserOffset(overscrolledPosition, -10.0); + final double tensioningApplied = + physicsUnderTest.applyPhysicsToUserOffset(overscrolledPosition, 10.0); + + expect(easingApplied.abs(), greaterThan(tensioningApplied.abs())); + }); + }); }