Unverified Commit 1057d39d authored by Viet Do's avatar Viet Do Committed by GitHub

Support infinite scrolling for CupertinoPicker. (#19789)

Allows the cupertino picker to be scroll infinitely by adding builder.
parent 9b309ba8
......@@ -99,6 +99,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
child: new Text(index.toString()),
);
}),
looping: true,
),
),
new Expanded(
......@@ -123,6 +124,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
child: new Text(index.toString()),
);
}),
looping: true,
),
),
],
......
......@@ -18,7 +18,7 @@ const double _kForegroundScreenOpacityFraction = 0.7;
/// An iOS-styled picker.
///
/// Displays the provided [children] widgets on a wheel for selection and
/// Displays its children widgets on a wheel for selection and
/// calls back when the currently selected item changes.
///
/// Can be used with [showModalBottomSheet] to display the picker modally at the
......@@ -30,7 +30,7 @@ const double _kForegroundScreenOpacityFraction = 0.7;
/// the iOS design specific chrome.
/// * <https://developer.apple.com/ios/human-interface-guidelines/controls/pickers/>
class CupertinoPicker extends StatefulWidget {
/// Creates a control used for selecting values.
/// Creates a picker from a concrete list of children.
///
/// The [diameterRatio] and [itemExtent] arguments must not be null. The
/// [itemExtent] must be greater than zero.
......@@ -38,7 +38,52 @@ class CupertinoPicker extends StatefulWidget {
/// The [backgroundColor] defaults to light gray. It can be set to null to
/// disable the background painting entirely; this is mildly more efficient
/// than using [Colors.transparent].
const CupertinoPicker({
///
/// The [looping] argument decides whether the child list loops and can be
/// scrolled infinitely. If set to true, scrolling past the end of the list
/// will loop the list back to the beginning. If set to false, the list will
/// stop scrolling when you reach the end or the beginning.
CupertinoPicker({
Key key,
this.diameterRatio = _kDefaultDiameterRatio,
this.backgroundColor = _kDefaultBackground,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
this.scrollController,
@required this.itemExtent,
@required this.onSelectedItemChanged,
@required List<Widget> children,
bool looping = false,
}) : assert(children != null),
assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
childDelegate = looping
? new ListWheelChildLoopingListDelegate(children: children)
: new ListWheelChildListDelegate(children: children),
super(key: key);
/// Creates a picker from an [IndexedWidgetBuilder] callback where the builder
/// is dynamically invoked during layout.
///
/// A child is lazily created when it starts becoming visible in the viewport.
/// All of the children provided by the builder are cached and reused, so
/// normally the builder is only called once for each index (except when
/// rebuilding - the cache is cleared).
///
/// The [itemBuilder] argument must not be null. The [childCount] argument
/// reflects the number of children that will be provided by the [itemBuilder].
/// {@macro flutter.widgets.wheelList.childCount}
///
/// The [itemExtent] argument must be non-null and positive.
///
/// The [backgroundColor] defaults to light gray. It can be set to null to
/// disable the background painting entirely; this is mildly more efficient
/// than using [Colors.transparent].
CupertinoPicker.builder({
Key key,
this.diameterRatio = _kDefaultDiameterRatio,
this.backgroundColor = _kDefaultBackground,
......@@ -48,12 +93,15 @@ class CupertinoPicker extends StatefulWidget {
this.scrollController,
@required this.itemExtent,
@required this.onSelectedItemChanged,
@required this.children,
}) : assert(diameterRatio != null),
@required IndexedWidgetBuilder itemBuilder,
int childCount,
}) : assert(itemBuilder != null),
assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
childDelegate = new ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount),
super(key: key);
/// Relative ratio between this picker's height and the simulated cylinder's diameter.
......@@ -102,8 +150,8 @@ class CupertinoPicker extends StatefulWidget {
/// listen for [ScrollEndNotification] and read its [FixedExtentMetrics].
final ValueChanged<int> onSelectedItemChanged;
/// [Widget]s in the picker's scroll wheel.
final List<Widget> children;
/// A delegate that lazily instantiates children.
final ListWheelChildDelegate childDelegate;
@override
State<StatefulWidget> createState() => new _CupertinoPickerState();
......@@ -196,7 +244,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
Widget result = new Stack(
children: <Widget>[
new Positioned.fill(
child: new ListWheelScrollView(
child: new ListWheelScrollView.useDelegate(
controller: widget.scrollController,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
......@@ -205,7 +253,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
magnification: widget.magnification,
itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
children: widget.children,
childDelegate: widget.childDelegate,
),
),
_buildGradientScreen(),
......
......@@ -240,11 +240,10 @@ void main() {
1000.0,
);
expect(
tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
// Should have been flung far enough to go off screen.
greaterThan(600.0),
);
// Should have been flung far enough that even the first item goes off
// screen and gets removed.
expect(find.widgetWithText(Container, '0').evaluate().isEmpty, true);
expect(
selectedItems,
// This specific throw was fast enough that each scroll update landed
......
......@@ -41,11 +41,11 @@ void main() {
testWidgets('ListWheelScrollView can have zero child', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 50.0,
children: <Widget>[],
children: const <Widget>[],
),
),
);
......@@ -68,6 +68,149 @@ void main() {
});
});
group('infinite scrolling', () {
testWidgets('infinite looping list', (WidgetTester tester) async {
final FixedExtentScrollController controller =
new FixedExtentScrollController();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) {},
childDelegate: new ListWheelChildLoopingListDelegate(
children: List<Widget>.generate(10, (int index) {
return new Container(
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(Container, '0')),
const Offset(0.0, 250.0)
);
// The last item is just before the first item.
expect(
tester.getTopLeft(find.widgetWithText(Container, '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(Container, '0')),
const Offset(0.0, 250.0)
);
});
testWidgets('infinite child builder', (WidgetTester tester) async {
final FixedExtentScrollController controller =
new FixedExtentScrollController();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) {},
childDelegate: new ListWheelChildBuilderDelegate(
builder: (BuildContext context, int index) {
return new Container(
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(Container, '-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(Container, '1000')),
const Offset(0.0, 250.0)
);
});
testWidgets('child builder with lower and upper limits', (WidgetTester tester) async {
final List<int> paintedChildren = <int>[];
final FixedExtentScrollController controller =
new FixedExtentScrollController(initialItem: -10);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) {},
childDelegate: new ListWheelChildBuilderDelegate(
builder: (BuildContext context, int index) {
if (index < -15 || index > -5)
return null;
return new Container(
width: 400.0,
height: 100.0,
child: new CustomPaint(
painter: new 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', () {
testWidgets("ListWheelScrollView takes parent's size with small children", (WidgetTester tester) async {
await tester.pumpWidget(
......@@ -113,11 +256,11 @@ void main() {
testWidgets("ListWheelScrollView children can't be bigger than itemExtent", (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
Directionality(
textDirection: TextDirection.ltr,
child: ListWheelScrollView(
itemExtent: 50.0,
children: <Widget>[
children: const <Widget>[
SizedBox(
height: 200.0,
width: 200.0,
......@@ -132,6 +275,80 @@ void main() {
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 = Set<int>();
final FixedExtentScrollController controller =
new FixedExtentScrollController();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) {},
childDelegate: new ListWheelChildBuilderDelegate(
builder: (BuildContext context, int index) {
expect(builtChildren.contains(index), false);
builtChildren.add(index);
return new Container(
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 =
new FixedExtentScrollController();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
onSelectedItemChanged: (_) {},
children: List<Widget>.generate(16, (int index) {
return new Text(index.toString());
}),
),
)
);
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Text)).parent;
// 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);
});
});
group('pre-transform viewport', () {
......@@ -1014,6 +1231,10 @@ void main() {
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);
......
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