blob: 9ef92e998b7e977a41e117f8c288317d7e0a51ef [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.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../rendering/rendering_tester.dart' show TestCallbackPainter, TestClipPaintingContext;
void main() {
testWidgets('ListWheelScrollView respects clipBehavior', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 2000.0, // huge extent to trigger clip
children: <Widget>[Container()],
),
),
);
// 1st, check that the render object has received the default clip behavior.
final RenderListWheelViewport renderObject = tester.allRenderObjects.whereType<RenderListWheelViewport>().first;
expect(renderObject.clipBehavior, equals(Clip.hardEdge));
// 2nd, check that the painting context has received the default clip behavior.
final TestClipPaintingContext context = TestClipPaintingContext();
renderObject.paint(context, Offset.zero);
expect(context.clipBehavior, equals(Clip.hardEdge));
// 3rd, pump a new widget to check that the render object can update its clip behavior.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 2000.0, // huge extent to trigger clip
clipBehavior: Clip.antiAlias,
children: <Widget>[Container()],
),
),
);
expect(renderObject.clipBehavior, equals(Clip.antiAlias));
// 4th, check that a non-default clip behavior can be sent to the painting context.
renderObject.paint(context, Offset.zero);
expect(context.clipBehavior, equals(Clip.antiAlias));
});
group('construction check', () {
testWidgets('ListWheelScrollView needs positive diameter ratio', (WidgetTester tester) async {
expect(
() => ListWheelScrollView(
diameterRatio: nonconst(-2.0),
itemExtent: 20.0,
children: const <Widget>[],
),
throwsA(isAssertionError.having(
(AssertionError error) => error.message,
'message',
contains("You can't set a diameterRatio of 0"),
)),
);
});
testWidgets('ListWheelScrollView can have zero child', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 50.0,
children: const <Widget>[],
),
),
);
expect(tester.getSize(find.byType(ListWheelScrollView)), const Size(800.0, 600.0));
});
testWidgets('ListWheelScrollView needs positive magnification', (WidgetTester tester) async {
expect(
() {
ListWheelScrollView(
useMagnifier: true,
magnification: -1.0,
itemExtent: 20.0,
children: <Widget>[Container()],
);
},
throwsAssertionError,
);
});
testWidgets('ListWheelScrollView needs valid overAndUnderCenterOpacity', (WidgetTester tester) async {
expect(
() {
ListWheelScrollView(
overAndUnderCenterOpacity: -1,
itemExtent: 20.0,
children: <Widget>[Container()],
);
},
throwsAssertionError,
);
expect(
() {
ListWheelScrollView(
overAndUnderCenterOpacity: 2,
itemExtent: 20.0,
children: <Widget>[Container()],
);
},
throwsAssertionError,
);
expect(
() {
ListWheelScrollView(
itemExtent: 20.0,
children: <Widget>[Container()],
);
},
isNot(throwsAssertionError),
);
expect(
() {
ListWheelScrollView(
overAndUnderCenterOpacity: 0,
itemExtent: 20.0,
children: <Widget>[Container()],
);
},
isNot(throwsAssertionError),
);
});
});
group('infinite scrolling', () {
testWidgets('infinite looping list', (WidgetTester tester) async {
final FixedExtentScrollController controller =
FixedExtentScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
childDelegate: ListWheelChildLoopingListDelegate(
children: List<Widget>.generate(10, (int index) {
return SizedBox(
width: 400.0,
height: 100.0,
child: Text(index.toString()),
);
}),
),
),
),
);
// The first item is at the center of the viewport.
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')),
const Offset(0.0, 250.0),
);
// The last item is just before the first item.
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '9')),
const Offset(0.0, 150.0),
);
controller.jumpTo(1000.0);
await tester.pump();
// We have passed the end of the list, the list should have looped back.
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')),
const Offset(0.0, 250.0),
);
});
testWidgets('infinite child builder', (WidgetTester tester) async {
final FixedExtentScrollController controller =
FixedExtentScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
childDelegate: ListWheelChildBuilderDelegate(
builder: (BuildContext context, int index) {
return SizedBox(
width: 400.0,
height: 100.0,
child: Text(index.toString()),
);
},
),
),
),
);
// Can be scrolled infinitely for negative indexes.
controller.jumpTo(-100000.0);
await tester.pump();
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '-1000')),
const Offset(0.0, 250.0),
);
// Can be scrolled infinitely for positive indexes.
controller.jumpTo(100000.0);
await tester.pump();
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '1000')),
const Offset(0.0, 250.0),
);
});
testWidgets('child builder with lower and upper limits', (WidgetTester tester) async {
// Adjust the content dimensions at the end of `RenderListWheelViewport.performLayout()`
final List<int> paintedChildren = <int>[];
final FixedExtentScrollController controller =
FixedExtentScrollController(initialItem: -10);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
childDelegate: ListWheelChildBuilderDelegate(
builder: (BuildContext context, int index) {
if (index < -15 || index > -5) {
return null;
}
return SizedBox(
width: 400.0,
height: 100.0,
child: CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
),
);
},
),
),
),
);
expect(paintedChildren, <int>[-13, -12, -11, -10, -9, -8, -7]);
// Flings with high velocity and stop at the lower limit.
paintedChildren.clear();
await tester.fling(
find.byType(ListWheelScrollView),
const Offset(0.0, 1000.0),
1000.0,
);
await tester.pumpAndSettle();
expect(controller.selectedItem, -15);
// Flings with high velocity and stop at the upper limit.
await tester.fling(
find.byType(ListWheelScrollView),
const Offset(0.0, -1000.0),
1000.0,
);
await tester.pumpAndSettle();
expect(controller.selectedItem, -5);
});
});
group('layout', () {
// Regression test for https://github.com/flutter/flutter/issues/90953
testWidgets('ListWheelScrollView childDelegate update test 2', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController( initialItem: 2 );
Widget buildFrame(int childCount) {
return Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 400.0,
onSelectedItemChanged: (_) { },
childDelegate: ListWheelChildBuilderDelegate(
childCount: childCount,
builder: (BuildContext context, int index) {
return SizedBox(
width: 400.0,
height: 400.0,
child: Text(index.toString()),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame(5));
expect(find.text('0'), findsNothing);
expect(tester.renderObject(find.text('1')).attached, true);
expect(tester.renderObject(find.text('2')).attached, true);
expect(tester.renderObject(find.text('3')).attached, true);
expect(find.text('4'), findsNothing);
// Remove the last 3 items.
await tester.pumpWidget(buildFrame(2));
expect(tester.renderObject(find.text('0')).attached, true);
expect(tester.renderObject(find.text('1')).attached, true);
expect(find.text('3'), findsNothing);
// Add 3 items at the end.
await tester.pumpWidget(buildFrame(5));
expect(tester.renderObject(find.text('0')).attached, true);
expect(tester.renderObject(find.text('1')).attached, true);
expect(tester.renderObject(find.text('2')).attached, true);
expect(find.text('3'), findsNothing);
expect(find.text('4'), findsNothing);
// Scroll to the last item.
final TestGesture scrollGesture = await tester.startGesture(const Offset(10.0, 10.0));
await scrollGesture.moveBy(const Offset(0.0, -1200.0));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsNothing);
expect(tester.renderObject(find.text('3')).attached, true);
expect(tester.renderObject(find.text('4')).attached, true);
// Remove the last 3 items.
await tester.pumpWidget(buildFrame(2));
expect(tester.renderObject(find.text('0')).attached, true);
expect(tester.renderObject(find.text('1')).attached, true);
expect(find.text('3'), findsNothing);
});
// Regression test for https://github.com/flutter/flutter/issues/58144
testWidgets('ListWheelScrollView childDelegate update test', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController();
Widget buildFrame(int childCount) {
return Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
childDelegate: ListWheelChildBuilderDelegate(
childCount: childCount,
builder: (BuildContext context, int index) {
return SizedBox(
width: 400.0,
height: 100.0,
child: Text(index.toString()),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame(1));
expect(tester.renderObject(find.text('0')).attached, true);
await tester.pumpWidget(buildFrame(2));
expect(tester.renderObject(find.text('0')).attached, true);
expect(tester.renderObject(find.text('1')).attached, true);
});
testWidgets("ListWheelScrollView takes parent's size with small children", (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
// Inner children smaller than the outer window.
itemExtent: 50.0,
children: <Widget>[
Container(
height: 50.0,
color: const Color(0xFFFFFFFF),
),
],
),
),
);
expect(tester.getTopLeft(find.byType(ListWheelScrollView)), Offset.zero);
// Standard test screen size.
expect(tester.getBottomRight(find.byType(ListWheelScrollView)), const Offset(800.0, 600.0));
});
testWidgets("ListWheelScrollView takes parent's size with large children", (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
// Inner children 5000.0px.
itemExtent: 50.0,
children: List<Widget>.generate(100, (int index) {
return Container(
height: 50.0,
color: const Color(0xFFFFFFFF),
);
}),
),
),
);
expect(tester.getTopLeft(find.byType(ListWheelScrollView)), Offset.zero);
// Still fills standard test screen size.
expect(tester.getBottomRight(find.byType(ListWheelScrollView)), const Offset(800.0, 600.0));
});
testWidgets("ListWheelScrollView children can't be bigger than itemExtent", (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 50.0,
children: const <Widget>[
SizedBox(
height: 200.0,
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
expect(tester.getSize(find.byType(SizedBox)), const Size(200.0, 50.0));
expect(find.text('blah'), findsOneWidget);
});
testWidgets('builder is never called twice for same index', (WidgetTester tester) async {
final Set<int> builtChildren = <int>{};
final FixedExtentScrollController controller =
FixedExtentScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
childDelegate: ListWheelChildBuilderDelegate(
builder: (BuildContext context, int index) {
expect(builtChildren.contains(index), false);
builtChildren.add(index);
return SizedBox(
width: 400.0,
height: 100.0,
child: Text(index.toString()),
);
},
),
),
),
);
// Scrolls up and down to check if builder is called twice.
controller.jumpTo(-10000.0);
await tester.pump();
controller.jumpTo(10000.0);
await tester.pump();
controller.jumpTo(-10000.0);
await tester.pump();
});
testWidgets('only visible children are maintained as children of the rendered viewport', (WidgetTester tester) async {
final FixedExtentScrollController controller =
FixedExtentScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
children: List<Widget>.generate(16, (int index) {
return Text(index.toString());
}),
),
),
);
final RenderListWheelViewport viewport = tester.renderObject(find.byType(ListWheelViewport)) as RenderListWheelViewport;
// Item 0 is in the middle. There are 3 children visible after it, so the
// value of childCount should be 4.
expect(viewport.childCount, 4);
controller.jumpToItem(8);
await tester.pump();
// Item 8 is in the middle. There are 3 children visible before it and 3
// after it, so the value of childCount should be 7.
expect(viewport.childCount, 7);
controller.jumpToItem(15);
await tester.pump();
// Item 15 is in the middle. There are 3 children visible before it, so the
// value of childCount should be 4.
expect(viewport.childCount, 4);
});
testWidgets('a tighter squeeze lays out more children', (WidgetTester tester) async {
final FixedExtentScrollController controller =
FixedExtentScrollController(initialItem: 10);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) { },
children: List<Widget>.generate(20, (int index) {
return Text(index.toString());
}),
),
),
);
final RenderListWheelViewport viewport = tester.renderObject(find.byType(ListWheelViewport)) as RenderListWheelViewport;
// The screen is vertically 600px. Since the middle item is centered,
// half of the first and last items are visible, making 7 children visible.
expect(viewport.childCount, 7);
// Pump the same widget again but with double the squeeze.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
squeeze: 2,
onSelectedItemChanged: (_) { },
children: List<Widget>.generate(20, (int index) {
return Text(index.toString());
}),
),
),
);
// 12 instead of 6 children are laid out + 1 because the middle item is
// centered.
expect(viewport.childCount, 13);
});
});
group('pre-transform viewport', () {
testWidgets('ListWheelScrollView starts and ends from the middle', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
final List<int> paintedChildren = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
);
}),
),
),
);
// Screen is 600px tall and the first item starts at 250px. The first 4
// children are visible.
expect(paintedChildren, <int>[0, 1, 2, 3]);
controller.jumpTo(1000.0);
paintedChildren.clear();
await tester.pump();
// Item number 10 is now in the middle of the screen at 250px. 9, 8, 7 are
// visible before it and 11, 12, 13 are visible after it.
expect(paintedChildren, <int>[7, 8, 9, 10, 11, 12, 13]);
// Move to the last item.
controller.jumpTo(9900.0);
paintedChildren.clear();
await tester.pump();
// Item 99 is in the middle at 250px.
expect(paintedChildren, <int>[96, 97, 98, 99]);
});
testWidgets('A child gets painted as soon as its first pixel is in the viewport', (WidgetTester tester) async {
final ScrollController controller = ScrollController(initialScrollOffset: 50.0);
final List<int> paintedChildren = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: List<Widget>.generate(10, (int index) {
return CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
);
}),
),
),
);
// Screen is 600px tall and the first item starts at 200px. The first 4
// children are visible.
expect(paintedChildren, <int>[0, 1, 2, 3]);
paintedChildren.clear();
// Move down by 1px.
await tester.drag(find.byType(ListWheelScrollView), const Offset(0.0, -1.0));
await tester.pump();
// Now the first pixel of item 5 enters the viewport.
expect(paintedChildren, <int>[0, 1, 2, 3, 4]);
});
testWidgets('A child is no longer painted after its last pixel leaves the viewport', (WidgetTester tester) async {
final ScrollController controller = ScrollController(initialScrollOffset: 250.0);
final List<int> paintedChildren = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: List<Widget>.generate(10, (int index) {
return CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
);
}),
),
),
);
// The first item is at 0px and the 600px screen is full in the
// **untransformed plane's viewport painting coordinates**
expect(paintedChildren, <int>[0, 1, 2, 3, 4, 5]);
paintedChildren.clear();
// Go down another 99px.
controller.jumpTo(349.0);
await tester.pump();
// One more item now visible with the last pixel of 0 showing.
expect(paintedChildren, <int>[0, 1, 2, 3, 4, 5, 6]);
paintedChildren.clear();
// Go down one more pixel.
controller.jumpTo(350.0);
await tester.pump();
// Item 0 no longer visible.
expect(paintedChildren, <int>[1, 2, 3, 4, 5, 6]);
});
});
group('viewport transformation', () {
testWidgets('Center child is magnified', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RepaintBoundary(
key: const Key('list_wheel_scroll_view'),
child: ListWheelScrollView(
useMagnifier: true,
magnification: 2.0,
itemExtent: 50.0,
children: List<Widget>.generate(10, (int index) {
return const Placeholder();
}),
),
),
),
);
await expectLater(
find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile('list_wheel_scroll_view.center_child.magnified.png'),
);
});
testWidgets('Default middle transform', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 100.0,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
final RenderListWheelViewport viewport = tester.renderObject(find.byType(ListWheelViewport)) as RenderListWheelViewport;
expect(viewport, paints..transform(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
-1.2 /* origin centering multiplied */, -0.9/* origin centering multiplied*/, 1.0, -0.003 /* inverse of perspective */,
moreOrLessEquals(0.0), moreOrLessEquals(0.0), 0.0, moreOrLessEquals(1.0),
]),
));
});
testWidgets('Curve the wheel to the left', (WidgetTester tester) async {
final ScrollController controller = ScrollController(initialScrollOffset: 300.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RepaintBoundary(
key: const Key('list_wheel_scroll_view'),
child: ListWheelScrollView(
controller: controller,
offAxisFraction: 0.5,
itemExtent: 50.0,
children: List<Widget>.generate(32, (int index) {
return const Placeholder();
}),
),
),
),
);
await expectLater(
find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile('list_wheel_scroll_view.curved_wheel.left.png'),
);
});
testWidgets('Scrolling, diameterRatio, perspective all changes matrix', (WidgetTester tester) async {
final ScrollController controller = ScrollController(initialScrollOffset: 200.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
final RenderListWheelViewport viewport = tester.renderObject(find.byType(ListWheelViewport)) as RenderListWheelViewport;
expect(viewport, paints..transform(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
moreOrLessEquals(-0.41042417199080244), moreOrLessEquals(0.6318744917928065), moreOrLessEquals(0.3420201433256687), moreOrLessEquals(-0.0010260604299770061),
moreOrLessEquals(-1.12763114494309), moreOrLessEquals(-1.1877435020329863), moreOrLessEquals(0.9396926207859084), moreOrLessEquals(-0.0028190778623577253),
moreOrLessEquals(166.54856463138663), moreOrLessEquals(-62.20844875763376), moreOrLessEquals(-138.79047052615562), moreOrLessEquals(1.4163714115784667),
]),
));
// Increase diameter.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
diameterRatio: 3.0,
itemExtent: 100.0,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
expect(viewport, paints..transform(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
moreOrLessEquals(-0.26954971336161726), moreOrLessEquals(0.7722830529455648), moreOrLessEquals(0.22462476113468105), moreOrLessEquals(-0.0006738742834040432),
moreOrLessEquals(-1.1693344055601331), moreOrLessEquals(-1.101625565304781), moreOrLessEquals(0.9744453379667777), moreOrLessEquals(-0.002923336013900333),
moreOrLessEquals(108.46394900436536), moreOrLessEquals(-113.14792465797223), moreOrLessEquals(-90.38662417030434), moreOrLessEquals(1.2711598725109134),
]),
));
// Decrease perspective.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
perspective: 0.0001,
itemExtent: 100.0,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
expect(viewport, paints..transform(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
moreOrLessEquals(-0.01368080573302675), moreOrLessEquals(0.9294320164861384), moreOrLessEquals(0.3420201433256687), moreOrLessEquals(-0.000034202014332566874),
moreOrLessEquals(-0.03758770483143634), moreOrLessEquals(-0.370210921949246), moreOrLessEquals(0.9396926207859084), moreOrLessEquals(-0.00009396926207859085),
moreOrLessEquals(5.551618821046304), moreOrLessEquals(-182.95615811538906), moreOrLessEquals(-138.79047052615562), moreOrLessEquals(1.0138790470526158),
]),
));
// Scroll a bit.
controller.jumpTo(300.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
expect(viewport, paints..transform(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
-0.6, moreOrLessEquals(0.41602540378443875), moreOrLessEquals(0.5), moreOrLessEquals(-0.0015),
moreOrLessEquals(-1.0392304845413265), moreOrLessEquals(-1.2794228634059948), moreOrLessEquals(0.8660254037844387), moreOrLessEquals(-0.0025980762113533163),
moreOrLessEquals(276.46170927520404), moreOrLessEquals(-52.46133917892857), moreOrLessEquals(-230.38475772933677), moreOrLessEquals(1.69115427318801),
]),
));
});
testWidgets('offAxisFraction, magnification changes matrix', (WidgetTester tester) async {
final ScrollController controller = ScrollController(initialScrollOffset: 200.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
offAxisFraction: 0.5,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
final RenderListWheelViewport viewport = tester.renderObject(find.byType(ListWheelViewport)) as RenderListWheelViewport;
expect(
viewport,
paints
..transform(
matrix4: equals(<dynamic>[
1.0,
0.0,
0.0,
0.0,
0.0,
moreOrLessEquals(0.6318744917928063),
moreOrLessEquals(0.3420201433256688),
moreOrLessEquals(-0.0010260604299770066),
0.0,
moreOrLessEquals(-1.1877435020329863),
moreOrLessEquals(0.9396926207859083),
moreOrLessEquals(-0.002819077862357725),
0.0,
moreOrLessEquals(-62.20844875763376),
moreOrLessEquals(-138.79047052615562),
moreOrLessEquals(1.4163714115784667),
]),
),
);
controller.jumpTo(0.0);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
offAxisFraction: 0.5,
useMagnifier: true,
magnification: 1.5,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
expect(
viewport,
paints
..transform(
matrix4: equals(<dynamic>[
1.5,
0.0,
0.0,
0.0,
0.0,
1.5,
0.0,
0.0,
0.0,
0.0,
1.5,
0.0,
0.0,
-150.0,
0.0,
1.0,
]),
),
);
});
});
group('scroll notifications', () {
testWidgets('no onSelectedItemChanged callback on first build', (WidgetTester tester) async {
bool itemChangeCalled = false;
void onItemChange(int _) { itemChangeCalled = true; }
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 100.0,
onSelectedItemChanged: onItemChange,
children: const <Widget>[
SizedBox(
width: 200.0,
child: Center(
child: Text('blah'),
),
),
],
),
),
);
expect(itemChangeCalled, false);
});
testWidgets('onSelectedItemChanged when a new item is closest to center', (WidgetTester tester) async {
final List<int> selectedItems = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 100.0,
onSelectedItemChanged: (int index) { selectedItems.add(index); },
children: List<Widget>.generate(10, (int index) {
return const Placeholder();
}),
),
),
);
final TestGesture scrollGesture = await tester.startGesture(const Offset(10.0, 10.0));
// Item 0 is still closest to the center. No updates.
await scrollGesture.moveBy(const Offset(0.0, -49.0));
expect(selectedItems.isEmpty, true);
// Now item 1 is closest to the center.
await scrollGesture.moveBy(const Offset(0.0, -1.0));
expect(selectedItems, <int>[1]);
// Now item 1 is still closest to the center for another full itemExtent (100px).
await scrollGesture.moveBy(const Offset(0.0, -99.0));
expect(selectedItems, <int>[1]);
await scrollGesture.moveBy(const Offset(0.0, -1.0));
expect(selectedItems, <int>[1, 2]);
// Going back triggers previous item indices.
await scrollGesture.moveBy(const Offset(0.0, 50.0));
expect(selectedItems, <int>[1, 2, 1]);
});
testWidgets('onSelectedItemChanged reports only in valid range', (WidgetTester tester) async {
final List<int> selectedItems = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 100.0,
onSelectedItemChanged: (int index) { selectedItems.add(index); },
// So item 0 is at 0 and item 9 is at 900 in the scrollable range.
children: List<Widget>.generate(10, (int index) {
return const Placeholder();
}),
),
),
);
final TestGesture scrollGesture = await tester.startGesture(const Offset(10.0, 10.0));
// First move back past the beginning.
await scrollGesture.moveBy(const Offset(0.0, 70.0));
for (double verticalOffset = 0.0; verticalOffset > -2000.0; verticalOffset -= 10.0) {
// Then gradually move down by a total vertical extent much higher than
// the scrollable extent.
await scrollGesture.moveTo(Offset(0.0, verticalOffset));
}
// The list should only cover the list of valid items. Item 0 would not
// be included because the current item never left the 0 index until it
// went to 1.
expect(selectedItems, <int>[1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
});
group('scroll controller', () {
testWidgets('initialItem', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 10);
final List<int> paintedChildren = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
);
}),
),
),
);
// Screen is 600px tall. Item 10 is in the center and each item is 100px tall.
expect(paintedChildren, <int>[7, 8, 9, 10, 11, 12, 13]);
expect(controller.selectedItem, 10);
});
testWidgets('controller jump', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 10);
final List<int> paintedChildren = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
);
}),
),
),
);
// Screen is 600px tall. Item 10 is in the center and each item is 100px tall.
expect(paintedChildren, <int>[7, 8, 9, 10, 11, 12, 13]);
paintedChildren.clear();
controller.jumpToItem(0);
await tester.pump();
expect(paintedChildren, <int>[0, 1, 2, 3]);
expect(controller.selectedItem, 0);
});
testWidgets('controller animateToItem', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 10);
final List<int> paintedChildren = <int>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return CustomPaint(
painter: TestCallbackPainter(onPaint: () {
paintedChildren.add(index);
}),
);
}),
),
),
);
// Screen is 600px tall. Item 10 is in the center and each item is 100px tall.
expect(paintedChildren, <int>[7, 8, 9, 10, 11, 12, 13]);
paintedChildren.clear();
controller.animateToItem(
0,
duration: const Duration(seconds: 1),
curve: Curves.linear,
);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(paintedChildren, <int>[0, 1, 2, 3]);
expect(controller.selectedItem, 0);
});
testWidgets('onSelectedItemChanged and controller are in sync', (WidgetTester tester) async {
final List<int> selectedItems = <int>[];
final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 10);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (int index) { selectedItems.add(index); },
children: List<Widget>.generate(100, (int index) {
return const Placeholder();
}),
),
),
);
final TestGesture scrollGesture = await tester.startGesture(const Offset(10.0, 10.0));
await scrollGesture.moveBy(const Offset(0.0, -49.0));
await tester.pump();
expect(selectedItems.isEmpty, true);
expect(controller.selectedItem, 10);
await scrollGesture.moveBy(const Offset(0.0, -1.0));
await tester.pump();
expect(selectedItems, <int>[11]);
expect(controller.selectedItem, 11);
await scrollGesture.moveBy(const Offset(0.0, 70.0));
await tester.pump();
expect(selectedItems, <int>[11, 10]);
expect(controller.selectedItem, 10);
});
testWidgets('controller hot swappable', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return const Placeholder();
}),
),
),
);
// Item 5 is now selected.
await tester.drag(find.byType(ListWheelScrollView), const Offset(0.0, -500.0));
await tester.pump();
final FixedExtentScrollController newController =
FixedExtentScrollController(initialItem: 30);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
controller: newController,
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return const Placeholder();
}),
),
),
);
// initialItem doesn't do anything since the scroll position was already
// created.
expect(newController.selectedItem, 5);
newController.jumpToItem(50);
expect(newController.selectedItem, 50);
expect(newController.position.pixels, 5000.0);
// Now remove the controller
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return const Placeholder();
}),
),
),
);
// Internally, that same controller is still attached and still at the
// same place.
expect(newController.selectedItem, 50);
});
});
group('physics', () {
testWidgets('fling velocities too low snaps back to the same item', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 40);
final List<double> scrolledPositions = <double>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
scrolledPositions.add(notification.metrics.pixels);
}
return false;
},
child: ListWheelScrollView(
controller: controller,
physics: const FixedExtentScrollPhysics(),
itemExtent: 1000.0,
children: List<Widget>.generate(100, (int index) {
return const Placeholder();
}),
),
),
),
);
await tester.fling(
find.byType(ListWheelScrollView),
const Offset(0.0, -50.0),
800.0,
);
// At this moment, the ballistics is started but 50px is still inside the
// initial item.
expect(controller.selectedItem, 40);
// A tester.fling creates and pumps 50 pointer events.
expect(scrolledPositions.length, 50);
expect(scrolledPositions.last, moreOrLessEquals(40 * 1000.0 + 50.0, epsilon: 0.2));
// Let the spring back simulation finish.
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// The simulation actually did stuff after start ballistics.
expect(scrolledPositions.length, greaterThan(50));
// Though it still lands back to the same item with the same scroll offset.
expect(controller.selectedItem, 40);
expect(scrolledPositions.last, moreOrLessEquals(40 * 1000.0, epsilon: 0.2));
});
testWidgets('high fling velocities lands exactly on items', (WidgetTester tester) async {
final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 40);
final List<double> scrolledPositions = <double>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
scrolledPositions.add(notification.metrics.pixels);
}
return false;
},
child: ListWheelScrollView(
controller: controller,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100.0,
children: List<Widget>.generate(100, (int index) {
return const Placeholder();
}),
),
),
),
);
await tester.fling(
find.byType(ListWheelScrollView),
// High and random numbers that's unlikely to land on exact multiples of 100.
const Offset(0.0, -567.0),
// macOS has reduced ballistic distance, need to increase speed to compensate.
debugDefaultTargetPlatformOverride == TargetPlatform.macOS ? 1678.0 : 678.0,
);
// After the drag, 40 + 567px should be on the 46th item.
expect(controller.selectedItem, 46);
// A tester.fling creates and pumps 50 pointer events.
expect(scrolledPositions.length, 50);
// iOS flings ease-in initially.
expect(scrolledPositions.last, moreOrLessEquals(40 * 100.0 + 556.826666666673, epsilon: 0.2));
// Let the spring back simulation finish.
await tester.pumpAndSettle();
// The simulation actually did stuff after start ballistics.
expect(scrolledPositions.length, greaterThan(50));
// Lands on 49.
expect(controller.selectedItem, 49);
// More importantly, lands tightly on 49.
expect(scrolledPositions.last, moreOrLessEquals(49 * 100.0, epsilon: 0.3));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
});
testWidgets('ListWheelScrollView getOffsetToReveal', (WidgetTester tester) async {
List<Widget> outerChildren;
final List<Widget> innerChildren = List<Widget>.generate(10, (int index) => Container());
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 500.0,
width: 300.0,
child: ListWheelScrollView(
controller: ScrollController(initialScrollOffset: 300.0),
itemExtent: 100.0,
children: outerChildren = List<Widget>.generate(10, (int i) {
return Center(
child: innerChildren[i] = SizedBox(
height: 50.0,
width: 50.0,
child: Text('Item $i'),
),
);
}),
),
),
),
),
);
final RenderListWheelViewport viewport = tester.allRenderObjects.whereType<RenderListWheelViewport>().first;
// direct child of viewport
RenderObject target = tester.renderObject(find.byWidget(outerChildren[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 200.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(0.0, 200.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(40.0, 240.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(40.0, 240.0, 10.0, 10.0));
// descendant of viewport, not direct child
target = tester.renderObject(find.byWidget(innerChildren[5]));
revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(125.0, 225.0, 50.0, 50.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(125.0, 225.0, 50.0, 50.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(165.0, 265.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: const Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, const Rect.fromLTWH(165.0, 265.0, 10.0, 10.0));
});
testWidgets('ListWheelScrollView showOnScreen', (WidgetTester tester) async {
List<Widget> outerChildren;
final List<Widget> innerChildren = List<Widget>.generate(10, (int index) => Container());
ScrollController controller;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 500.0,
width: 300.0,
child: ListWheelScrollView(
controller: controller = ScrollController(initialScrollOffset: 300.0),
itemExtent: 100.0,
children:
outerChildren = List<Widget>.generate(10, (int i) {
return Center(
child: innerChildren[i] = SizedBox(
height: 50.0,
width: 50.0,
child: Text('Item $i'),
),
);
}),
),
),
),
),
);
expect(controller.offset, 300.0);
tester.renderObject(find.byWidget(outerChildren[5])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 500.0);
tester.renderObject(find.byWidget(outerChildren[7])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 700.0);
tester.renderObject(find.byWidget(innerChildren[9])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 900.0);
tester.renderObject(find.byWidget(outerChildren[7])).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controller.offset, lessThan(900.0));
expect(controller.offset, greaterThan(700.0));
await tester.pumpAndSettle();
expect(controller.offset, 700.0);
});
}