Commit 47c4d64f authored by xster's avatar xster Committed by GitHub

Match non-linear overscroll spring to iOS (#11065)

* Make the drag resistance non-linear

* Let the easing of overscroll have a spring effect too

* Add tests and prevent possible drift by having a slightly smaller resistance when easing the overscroll

* lint
parent e6bafd0b
......@@ -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
......
......@@ -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)));
});
......
......@@ -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()));
});
});
}
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