blob: 2178f51bb9e867f8b2545dd28e005c0b4b0f6d68 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class TestScrollPhysics extends ScrollPhysics {
const TestScrollPhysics({
required this.name,
ScrollPhysics? parent,
}) : super(parent: parent);
final String name;
@override
TestScrollPhysics applyTo(ScrollPhysics? ancestor) {
return TestScrollPhysics(
name: name,
parent: parent?.applyTo(ancestor) ?? ancestor!,
);
}
TestScrollPhysics get namedParent => parent! as TestScrollPhysics;
String get names => parent == null ? name : '$name ${namedParent.names}';
@override
String toString() {
if (parent == null)
return '${objectRuntimeType(this, 'TestScrollPhysics')}($name)';
return '${objectRuntimeType(this, 'TestScrollPhysics')}($name) -> $parent';
}
}
void main() {
test('ScrollPhysics applyTo()', () {
const TestScrollPhysics a = TestScrollPhysics(name: 'a');
const TestScrollPhysics b = TestScrollPhysics(name: 'b');
const TestScrollPhysics c = TestScrollPhysics(name: 'c');
const TestScrollPhysics d = TestScrollPhysics(name: 'd');
const TestScrollPhysics e = TestScrollPhysics(name: 'e');
expect(a.parent, null);
expect(b.parent, null);
expect(c.parent, null);
final TestScrollPhysics ab = a.applyTo(b);
expect(ab.names, 'a b');
final TestScrollPhysics abc = ab.applyTo(c);
expect(abc.names, 'a b c');
final TestScrollPhysics de = d.applyTo(e);
expect(de.names, 'd e');
final TestScrollPhysics abcde = abc.applyTo(de);
expect(abcde.names, 'a b c d e');
});
test('ScrollPhysics subclasses applyTo()', () {
const ScrollPhysics bounce = BouncingScrollPhysics();
const ScrollPhysics clamp = ClampingScrollPhysics();
const ScrollPhysics never = NeverScrollableScrollPhysics();
const ScrollPhysics always = AlwaysScrollableScrollPhysics();
const ScrollPhysics page = PageScrollPhysics();
String types(ScrollPhysics? value) => value!.parent == null ? '${value.runtimeType}' : '${value.runtimeType} ${types(value.parent)}';
expect(
types(bounce.applyTo(clamp.applyTo(never.applyTo(always.applyTo(page))))),
'BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics PageScrollPhysics',
);
expect(
types(clamp.applyTo(never.applyTo(always.applyTo(page.applyTo(bounce))))),
'ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics PageScrollPhysics BouncingScrollPhysics',
);
expect(
types(never.applyTo(always.applyTo(page.applyTo(bounce.applyTo(clamp))))),
'NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics',
);
expect(
types(always.applyTo(page.applyTo(bounce.applyTo(clamp.applyTo(never))))),
'AlwaysScrollableScrollPhysics PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics',
);
expect(
types(page.applyTo(bounce.applyTo(clamp.applyTo(never.applyTo(always))))),
'PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics',
);
});
test("ScrollPhysics scrolling subclasses - Creating the simulation doesn't alter the velocity for time 0", () {
final ScrollMetrics position = FixedScrollMetrics(
minScrollExtent: 0.0,
maxScrollExtent: 100.0,
pixels: 20.0,
viewportDimension: 500.0,
axisDirection: AxisDirection.down,
);
const BouncingScrollPhysics bounce = BouncingScrollPhysics();
const ClampingScrollPhysics clamp = ClampingScrollPhysics();
const PageScrollPhysics page = PageScrollPhysics();
// Calls to createBallisticSimulation may happen on every frame (i.e. when the maxScrollExtent changes)
// Changing velocity for time 0 may cause a sudden, unwanted damping/speedup effect
expect(bounce.createBallisticSimulation(position, 1000)!.dx(0), moreOrLessEquals(1000));
expect(clamp.createBallisticSimulation(position, 1000)!.dx(0), moreOrLessEquals(1000));
expect(page.createBallisticSimulation(position, 1000)!.dx(0), moreOrLessEquals(1000));
});
group('BouncingScrollPhysics test', () {
late BouncingScrollPhysics physicsUnderTest;
setUp(() {
physicsUnderTest = const BouncingScrollPhysics();
});
test('overscroll is progressively harder', () {
final ScrollMetrics lessOverscrolledPosition = FixedScrollMetrics(
minScrollExtent: 0.0,
maxScrollExtent: 1000.0,
pixels: -20.0,
viewportDimension: 100.0,
axisDirection: AxisDirection.down,
);
final ScrollMetrics moreOverscrolledPosition = 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 = 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 = 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 = 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()));
});
test('overscroll a small list and a big list works the same way', () {
final ScrollMetrics smallListOverscrolledPosition = FixedScrollMetrics(
minScrollExtent: 0.0,
maxScrollExtent: 10.0,
pixels: -20.0,
viewportDimension: 100.0,
axisDirection: AxisDirection.down,
);
final ScrollMetrics bigListOverscrolledPosition = FixedScrollMetrics(
minScrollExtent: 0.0,
maxScrollExtent: 1000.0,
pixels: -20.0,
viewportDimension: 100.0,
axisDirection: AxisDirection.down,
);
final double smallListOverscrollApplied =
physicsUnderTest.applyPhysicsToUserOffset(smallListOverscrolledPosition, 10.0);
final double bigListOverscrollApplied =
physicsUnderTest.applyPhysicsToUserOffset(bigListOverscrolledPosition, 10.0);
expect(smallListOverscrollApplied, equals(bigListOverscrollApplied));
expect(smallListOverscrollApplied, greaterThan(1.0));
expect(smallListOverscrollApplied, lessThan(20.0));
});
});
test('ClampingScrollPhysics assertion test', () {
const ClampingScrollPhysics physics = ClampingScrollPhysics();
const double pixels = 500;
final ScrollMetrics position = FixedScrollMetrics(
pixels: pixels,
minScrollExtent: 0,
maxScrollExtent: 1000,
viewportDimension: 0,
axisDirection: AxisDirection.down,
);
expect(position.pixels, pixels);
late FlutterError error;
try {
physics.applyBoundaryConditions(position, pixels);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2], isA<DiagnosticsProperty<ScrollPhysics>>());
expect(error.diagnostics[2].style, DiagnosticsTreeStyle.errorProperty);
expect(error.diagnostics[2].value, physics);
expect(error.diagnostics[3], isA<DiagnosticsProperty<ScrollMetrics>>());
expect(error.diagnostics[3].style, DiagnosticsTreeStyle.errorProperty);
expect(error.diagnostics[3].value, position);
// RegExp matcher is required here due to flutter web and flutter mobile generating
// slightly different floating point numbers
// in Flutter web 0.0 sometimes just appears as 0. or 0
expect(
error.toStringDeep(),
matches(RegExp(
r'''
FlutterError
ClampingScrollPhysics\.applyBoundaryConditions\(\) was called
redundantly\.
The proposed new position\, 500(\.\d*)?, is exactly equal to the current
position of the given FixedScrollMetrics, 500(\.\d*)?\.
The applyBoundaryConditions method should only be called when the
value is going to actually change the pixels, otherwise it is
redundant\.
The physics object in question was\:
ClampingScrollPhysics
The position object in question was\:
FixedScrollMetrics\(500(\.\d*)?..\[0(\.\d*)?\]..500(\.\d*)?\)
''',
multiLine: true,
)),
);
}
});
testWidgets('PageScrollPhysics work with NestedScrollView', (WidgetTester tester) async {
// Regression test for: https://github.com/flutter/flutter/issues/47850
await tester.pumpWidget(Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: NestedScrollView(
physics: const PageScrollPhysics(),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(child: Container(height: 300, color: Colors.blue)),
];
},
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Text('Index $index');
},
itemCount: 100,
),
),
),
));
await tester.fling(find.text('Index 2'), const Offset(0.0, -300.0), 10000.0);
});
}