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> { ...@@ -99,6 +99,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
child: new Text(index.toString()), child: new Text(index.toString()),
); );
}), }),
looping: true,
), ),
), ),
new Expanded( new Expanded(
...@@ -123,6 +124,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> { ...@@ -123,6 +124,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
child: new Text(index.toString()), child: new Text(index.toString()),
); );
}), }),
looping: true,
), ),
), ),
], ],
......
...@@ -18,7 +18,7 @@ const double _kForegroundScreenOpacityFraction = 0.7; ...@@ -18,7 +18,7 @@ const double _kForegroundScreenOpacityFraction = 0.7;
/// An iOS-styled picker. /// 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. /// calls back when the currently selected item changes.
/// ///
/// Can be used with [showModalBottomSheet] to display the picker modally at the /// Can be used with [showModalBottomSheet] to display the picker modally at the
...@@ -30,7 +30,7 @@ const double _kForegroundScreenOpacityFraction = 0.7; ...@@ -30,7 +30,7 @@ const double _kForegroundScreenOpacityFraction = 0.7;
/// the iOS design specific chrome. /// the iOS design specific chrome.
/// * <https://developer.apple.com/ios/human-interface-guidelines/controls/pickers/> /// * <https://developer.apple.com/ios/human-interface-guidelines/controls/pickers/>
class CupertinoPicker extends StatefulWidget { 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 /// The [diameterRatio] and [itemExtent] arguments must not be null. The
/// [itemExtent] must be greater than zero. /// [itemExtent] must be greater than zero.
...@@ -38,7 +38,52 @@ class CupertinoPicker extends StatefulWidget { ...@@ -38,7 +38,52 @@ class CupertinoPicker extends StatefulWidget {
/// The [backgroundColor] defaults to light gray. It can be set to null to /// The [backgroundColor] defaults to light gray. It can be set to null to
/// disable the background painting entirely; this is mildly more efficient /// disable the background painting entirely; this is mildly more efficient
/// than using [Colors.transparent]. /// 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, Key key,
this.diameterRatio = _kDefaultDiameterRatio, this.diameterRatio = _kDefaultDiameterRatio,
this.backgroundColor = _kDefaultBackground, this.backgroundColor = _kDefaultBackground,
...@@ -48,12 +93,15 @@ class CupertinoPicker extends StatefulWidget { ...@@ -48,12 +93,15 @@ class CupertinoPicker extends StatefulWidget {
this.scrollController, this.scrollController,
@required this.itemExtent, @required this.itemExtent,
@required this.onSelectedItemChanged, @required this.onSelectedItemChanged,
@required this.children, @required IndexedWidgetBuilder itemBuilder,
}) : assert(diameterRatio != null), int childCount,
}) : assert(itemBuilder != null),
assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage), assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(magnification > 0), assert(magnification > 0),
assert(itemExtent != null), assert(itemExtent != null),
assert(itemExtent > 0), assert(itemExtent > 0),
childDelegate = new ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount),
super(key: key); super(key: key);
/// Relative ratio between this picker's height and the simulated cylinder's diameter. /// Relative ratio between this picker's height and the simulated cylinder's diameter.
...@@ -102,8 +150,8 @@ class CupertinoPicker extends StatefulWidget { ...@@ -102,8 +150,8 @@ class CupertinoPicker extends StatefulWidget {
/// listen for [ScrollEndNotification] and read its [FixedExtentMetrics]. /// listen for [ScrollEndNotification] and read its [FixedExtentMetrics].
final ValueChanged<int> onSelectedItemChanged; final ValueChanged<int> onSelectedItemChanged;
/// [Widget]s in the picker's scroll wheel. /// A delegate that lazily instantiates children.
final List<Widget> children; final ListWheelChildDelegate childDelegate;
@override @override
State<StatefulWidget> createState() => new _CupertinoPickerState(); State<StatefulWidget> createState() => new _CupertinoPickerState();
...@@ -196,7 +244,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> { ...@@ -196,7 +244,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
Widget result = new Stack( Widget result = new Stack(
children: <Widget>[ children: <Widget>[
new Positioned.fill( new Positioned.fill(
child: new ListWheelScrollView( child: new ListWheelScrollView.useDelegate(
controller: widget.scrollController, controller: widget.scrollController,
physics: const FixedExtentScrollPhysics(), physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio, diameterRatio: widget.diameterRatio,
...@@ -205,7 +253,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> { ...@@ -205,7 +253,7 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
magnification: widget.magnification, magnification: widget.magnification,
itemExtent: widget.itemExtent, itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged, onSelectedItemChanged: _handleSelectedItemChanged,
children: widget.children, childDelegate: widget.childDelegate,
), ),
), ),
_buildGradientScreen(), _buildGradientScreen(),
......
...@@ -15,8 +15,44 @@ import 'viewport_offset.dart'; ...@@ -15,8 +15,44 @@ import 'viewport_offset.dart';
typedef double _ChildSizingFunction(RenderBox child); typedef double _ChildSizingFunction(RenderBox child);
/// A delegate used by [RenderListWheelViewport] to manage its children.
///
/// [RenderListWheelViewport] during layout will ask the delegate to create
/// children that are visible in the viewport and remove those that are not.
abstract class ListWheelChildManager {
/// The maximum number of children that can be provided to
/// [RenderListWheelViewport].
///
/// If non-null, the children will have index in the range [0, childCount - 1].
///
/// If null, then there's no explicit limits to the range of the children
/// except that it has to be contiguous. If [childExistsAt] for a certain
/// index returns false, that index is already past the limit.
int get childCount;
/// Checks whether the delegate is able to provide a child widget at the given
/// index.
///
/// This function is not about whether the child at the given index is
/// attached to the [RenderListWheelViewport] or not.
bool childExistsAt(int index);
/// Creates a new child at the given index and updates it to the child list
/// of [RenderListWheelViewport]. If no child corresponds to `index`, then do
/// nothing.
///
/// It is possible to create children with negative indices.
void createChild(int index, {@required RenderBox after});
/// Removes the child element corresponding with the given RenderBox.
void removeChild(RenderBox child);
}
/// [ParentData] for use with [RenderListWheelViewport]. /// [ParentData] for use with [RenderListWheelViewport].
class ListWheelParentData extends ContainerBoxParentData<RenderBox> { } class ListWheelParentData extends ContainerBoxParentData<RenderBox> {
/// Index of this child in its parent's child list.
int index;
}
/// Render, onto a wheel, a bigger sequential set of objects inside this viewport. /// Render, onto a wheel, a bigger sequential set of objects inside this viewport.
/// ///
...@@ -97,6 +133,7 @@ class RenderListWheelViewport ...@@ -97,6 +133,7 @@ class RenderListWheelViewport
/// ///
/// All arguments must not be null. Optional arguments have reasonable defaults. /// All arguments must not be null. Optional arguments have reasonable defaults.
RenderListWheelViewport({ RenderListWheelViewport({
@required this.childManager,
@required ViewportOffset offset, @required ViewportOffset offset,
double diameterRatio = defaultDiameterRatio, double diameterRatio = defaultDiameterRatio,
double perspective = defaultPerspective, double perspective = defaultPerspective,
...@@ -107,7 +144,8 @@ class RenderListWheelViewport ...@@ -107,7 +144,8 @@ class RenderListWheelViewport
bool clipToSize = true, bool clipToSize = true,
bool renderChildrenOutsideViewport = false, bool renderChildrenOutsideViewport = false,
List<RenderBox> children, List<RenderBox> children,
}) : assert(offset != null), }) : assert(childManager != null),
assert(offset != null),
assert(diameterRatio != null), assert(diameterRatio != null),
assert(diameterRatio > 0, diameterRatioZeroMessage), assert(diameterRatio > 0, diameterRatioZeroMessage),
assert(perspective != null), assert(perspective != null),
...@@ -159,6 +197,9 @@ class RenderListWheelViewport ...@@ -159,6 +197,9 @@ class RenderListWheelViewport
'Cannot renderChildrenOutsideViewport and clipToSize since children ' 'Cannot renderChildrenOutsideViewport and clipToSize since children '
'rendered outside will be clipped anyway.'; 'rendered outside will be clipped anyway.';
/// The delegate that manages the children of this object.
final ListWheelChildManager childManager;
/// The associated ViewportOffset object for the viewport describing the part /// The associated ViewportOffset object for the viewport describing the part
/// of the content inside that's visible. /// of the content inside that's visible.
/// ///
...@@ -221,7 +262,8 @@ class RenderListWheelViewport ...@@ -221,7 +262,8 @@ class RenderListWheelViewport
if (value == _diameterRatio) if (value == _diameterRatio)
return; return;
_diameterRatio = value; _diameterRatio = value;
_hasScrolled(); markNeedsPaint();
markNeedsSemanticsUpdate();
} }
/// {@template flutter.rendering.wheelList.perspective} /// {@template flutter.rendering.wheelList.perspective}
...@@ -250,7 +292,8 @@ class RenderListWheelViewport ...@@ -250,7 +292,8 @@ class RenderListWheelViewport
if (value == _perspective) if (value == _perspective)
return; return;
_perspective = value; _perspective = value;
_hasScrolled(); markNeedsPaint();
markNeedsSemanticsUpdate();
} }
/// {@template flutter.rendering.wheelList.offAxisFraction} /// {@template flutter.rendering.wheelList.offAxisFraction}
...@@ -357,7 +400,8 @@ class RenderListWheelViewport ...@@ -357,7 +400,8 @@ class RenderListWheelViewport
if (value == _clipToSize) if (value == _clipToSize)
return; return;
_clipToSize = value; _clipToSize = value;
_hasScrolled(); markNeedsPaint();
markNeedsSemanticsUpdate();
} }
/// {@template flutter.rendering.wheelList.renderChildrenOutsideViewport} /// {@template flutter.rendering.wheelList.renderChildrenOutsideViewport}
...@@ -381,11 +425,12 @@ class RenderListWheelViewport ...@@ -381,11 +425,12 @@ class RenderListWheelViewport
if (value == _renderChildrenOutsideViewport) if (value == _renderChildrenOutsideViewport)
return; return;
_renderChildrenOutsideViewport = value; _renderChildrenOutsideViewport = value;
_hasScrolled(); markNeedsLayout();
markNeedsSemanticsUpdate();
} }
void _hasScrolled() { void _hasScrolled() {
markNeedsPaint(); markNeedsLayout();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
...@@ -418,19 +463,21 @@ class RenderListWheelViewport ...@@ -418,19 +463,21 @@ class RenderListWheelViewport
/// Main axis scroll extent in the **scrollable layout coordinates** that puts /// Main axis scroll extent in the **scrollable layout coordinates** that puts
/// the first item in the center. /// the first item in the center.
double get _minScrollExtent { double get _minEstimatedScrollExtent {
assert(hasSize); assert(hasSize);
if (childManager.childCount == null)
return double.negativeInfinity;
return 0.0; return 0.0;
} }
/// Main axis scroll extent in the **scrollable layout coordinates** that puts /// Main axis scroll extent in the **scrollable layout coordinates** that puts
/// the last item in the center. /// the last item in the center.
double get _maxScrollExtent { double get _maxEstimatedScrollExtent {
assert(hasSize); assert(hasSize);
if (!(childCount > 0)) if (childManager.childCount == null)
return 0.0; return double.infinity;
return math.max(0.0, (childCount - 1) * _itemExtent); return math.max(0.0, (childManager.childCount - 1) * _itemExtent);
} }
/// Scroll extent distance in the untransformed plane between the center /// Scroll extent distance in the untransformed plane between the center
...@@ -441,7 +488,7 @@ class RenderListWheelViewport ...@@ -441,7 +488,7 @@ class RenderListWheelViewport
double get _topScrollMarginExtent { double get _topScrollMarginExtent {
assert(hasSize); assert(hasSize);
// Consider adding alignment options other than center. // Consider adding alignment options other than center.
return _minScrollExtent - size.height / 2.0 + _itemExtent / 2.0; return -size.height / 2.0 + _itemExtent / 2.0;
} }
/// Transforms a **scrollable layout coordinates**' y position to the /// Transforms a **scrollable layout coordinates**' y position to the
...@@ -451,24 +498,6 @@ class RenderListWheelViewport ...@@ -451,24 +498,6 @@ class RenderListWheelViewport
return layoutCoordinateY - _topScrollMarginExtent - offset.pixels; return layoutCoordinateY - _topScrollMarginExtent - offset.pixels;
} }
/// Children with offsets larger than this value in the **scrollable layout
/// coordinates** can be painted.
double get _firstVisibleLayoutOffset {
assert(hasSize);
if (_renderChildrenOutsideViewport)
return double.negativeInfinity;
return _minScrollExtent - size.height / 2.0 - _itemExtent / 2.0 + offset.pixels;
}
/// Children with offsets smaller than this value in the **scrollable layout
/// coordinates** can be painted.
double get _lastVisibleLayoutOffset {
assert(hasSize);
if (_renderChildrenOutsideViewport)
return double.infinity;
return _minScrollExtent + size.height / 2.0 + _itemExtent / 2.0 + offset.pixels;
}
/// Given the _diameterRatio, return the largest absolute angle of the item /// Given the _diameterRatio, return the largest absolute angle of the item
/// at the edge of the portion of the visible cylinder. /// at the edge of the portion of the visible cylinder.
/// ///
...@@ -509,16 +538,16 @@ class RenderListWheelViewport ...@@ -509,16 +538,16 @@ class RenderListWheelViewport
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
if (childCount > 0) if (childManager.childCount == null)
return childCount * _itemExtent; return 0.0;
return 0.0; return childManager.childCount * _itemExtent;
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
if (childCount > 0) if (childManager.childCount == null)
return childCount * _itemExtent; return 0.0;
return 0.0; return childManager.childCount * _itemExtent;
} }
@override @override
...@@ -529,35 +558,170 @@ class RenderListWheelViewport ...@@ -529,35 +558,170 @@ class RenderListWheelViewport
size = constraints.biggest; size = constraints.biggest;
} }
/// Gets the index of a child by looking at its parentData.
int indexOf(RenderBox child) {
assert(child != null);
final ListWheelParentData childParentData = child.parentData;
assert(childParentData.index != null);
return childParentData.index;
}
/// Returns the index of the child at the given offset.
int scrollOffsetToIndex(double scrollOffset) => (scrollOffset / itemExtent).floor();
/// Returns the scroll offset of the child with the given index.
double indexToScrollOffset(int index) => index * itemExtent;
void _createChild(int index, {RenderBox after}) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) {
assert(constraints == this.constraints);
childManager.createChild(index, after: after);
});
}
void _destroyChild(RenderBox child) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) {
assert(constraints == this.constraints);
childManager.removeChild(child);
});
}
void _layoutChild(RenderBox child, BoxConstraints constraints, int index) {
child.layout(constraints, parentUsesSize: true);
final ListWheelParentData childParentData = child.parentData;
// Centers the child horizontally.
final double crossPosition = size.width / 2.0 - child.size.width / 2.0;
childParentData.offset = new Offset(crossPosition, indexToScrollOffset(index));
}
/// Performs layout based on how [childManager] provides children.
///
/// From the current scroll offset, the minimum index and maximum index that
/// is visible in the viewport can be calculated. The index range of the
/// currently active children can also be acquired by looking directly at
/// the current child list. This function has to modify the current index
/// range to match the target index range by removing children that are no
/// longer visible and creating those that are visible but not yet provided
/// by [childManager].
@override @override
void performLayout() { void performLayout() {
double currentOffset = 0.0; final BoxConstraints childConstraints =
constraints.copyWith(
minHeight: _itemExtent,
maxHeight: _itemExtent,
minWidth: 0.0,
);
// The height, in pixel, that children will be visible and might be laid out
// and painted.
double visibleHeight = size.height;
// If renderChildrenOutsideViewport is true, we spawn extra children by
// doubling the visibility range, those that are in the backside of the
// cylinder won't be painted anyway.
if (renderChildrenOutsideViewport)
visibleHeight *= 2;
final double firstVisibleOffset =
offset.pixels + _itemExtent / 2 - visibleHeight / 2;
final double lastVisibleOffset = firstVisibleOffset + visibleHeight;
// The index range that we want to spawn children. We find indexes that
// are in the interval [firstVisibleOffset, lastVisibleOffset).
int targetFirstIndex = scrollOffsetToIndex(firstVisibleOffset);
int targetLastIndex = scrollOffsetToIndex(lastVisibleOffset);
// Because we exclude lastVisibleOffset, if there's a new child starting at
// that offset, it is removed.
if (targetLastIndex * _itemExtent == lastVisibleOffset)
targetLastIndex--;
// Validates the target index range.
while (!childManager.childExistsAt(targetFirstIndex) && targetFirstIndex <= targetLastIndex)
targetFirstIndex++;
while (!childManager.childExistsAt(targetLastIndex) && targetFirstIndex <= targetLastIndex)
targetLastIndex--;
// If it turns out there's no children to layout, we remove old children and
// return.
if (targetFirstIndex > targetLastIndex) {
while (firstChild != null)
_destroyChild(firstChild);
return;
}
// Now there are 2 cases:
// - The target index range and our current index range have intersection:
// We shorten and extend our current child list so that the two lists
// match. Most of the time we are in this case.
// - The target list and our current child list have no intersection:
// We first remove all children and then add one child from the target
// list => this case becomes the other case.
// Case when there is no intersection.
if (childCount > 0 &&
(indexOf(firstChild) > targetLastIndex || indexOf(lastChild) < targetFirstIndex)) {
while (firstChild != null)
_destroyChild(firstChild);
}
// If there is no child at this stage, we add the first one that is in
// target range.
if (childCount == 0) {
_createChild(targetFirstIndex);
_layoutChild(firstChild, childConstraints, targetFirstIndex);
}
int currentFirstIndex = indexOf(firstChild);
int currentLastIndex = indexOf(lastChild);
// Remove all unnecessary children by shortening the current child list, in
// both directions.
while (currentFirstIndex < targetFirstIndex) {
_destroyChild(firstChild);
currentFirstIndex++;
}
while (currentLastIndex > targetLastIndex) {
_destroyChild(lastChild);
currentLastIndex--;
}
// Relayout all active children.
RenderBox child = firstChild; RenderBox child = firstChild;
final BoxConstraints innerConstraints =
constraints.copyWith(
minHeight: _itemExtent,
maxHeight: _itemExtent,
minWidth: 0.0,
);
while (child != null) { while (child != null) {
child.layout(innerConstraints, parentUsesSize: true); child.layout(childConstraints, parentUsesSize: true);
final ListWheelParentData childParentData = child.parentData;
// Centers the child in the cross axis. Consider making it configurable.
final double crossPosition = size.width / 2.0 - child.size.width / 2.0;
childParentData.offset = new Offset(crossPosition, currentOffset);
currentOffset += _itemExtent;
child = childAfter(child); child = childAfter(child);
} }
// Spawning new children that are actually visible but not in child list yet.
while (currentFirstIndex > targetFirstIndex) {
_createChild(currentFirstIndex - 1);
_layoutChild(firstChild, childConstraints, --currentFirstIndex);
}
while (currentLastIndex < targetLastIndex) {
_createChild(currentLastIndex + 1, after: lastChild);
_layoutChild(lastChild, childConstraints, ++currentLastIndex);
}
offset.applyViewportDimension(_viewportExtent); offset.applyViewportDimension(_viewportExtent);
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
// Applying content dimensions bases on how the childManager builds widgets:
// if it is available to provide a child just out of target range, then
// we don't know whether there's a limit yet, and set the dimension to the
// estimated value. Otherwise, we set the dimension limited to our target
// range.
final double minScrollExtent = childManager.childExistsAt(targetFirstIndex - 1)
? _minEstimatedScrollExtent
: indexToScrollOffset(targetFirstIndex);
final double maxScrollExtent = childManager.childExistsAt(targetLastIndex + 1)
? _maxEstimatedScrollExtent
: indexToScrollOffset(targetLastIndex);
offset.applyContentDimensions(minScrollExtent, maxScrollExtent);
} }
bool _shouldClipAtCurrentOffset() { bool _shouldClipAtCurrentOffset() {
final double highestUntransformedPaintY = final double highestUntransformedPaintY =
_getUntransformedPaintingCoordinateY(0.0); _getUntransformedPaintingCoordinateY(0.0);
return highestUntransformedPaintY < 0.0 return highestUntransformedPaintY < 0.0
|| size.height < highestUntransformedPaintY + _maxScrollExtent + _itemExtent; || size.height < highestUntransformedPaintY + _maxEstimatedScrollExtent + _itemExtent;
} }
@override @override
...@@ -576,33 +740,12 @@ class RenderListWheelViewport ...@@ -576,33 +740,12 @@ class RenderListWheelViewport
} }
} }
/// Visits all the children until one is partially visible in the viewport.
RenderBox _getFirstVisibleChild() {
assert(childCount > 0);
final double firstVisibleLayoutOffset = _firstVisibleLayoutOffset;
RenderBox child = firstChild;
ListWheelParentData childParentData = child.parentData;
while (childParentData != null
&& childParentData.offset.dy <= firstVisibleLayoutOffset) {
child = childAfter(child);
childParentData = child?.parentData;
}
return child;
}
/// Paints all children visible in the current viewport. /// Paints all children visible in the current viewport.
void _paintVisibleChildren(PaintingContext context, Offset offset) { void _paintVisibleChildren(PaintingContext context, Offset offset) {
assert(childCount > 0); RenderBox childToPaint = firstChild;
final double lastVisibleLayoutOffset = _lastVisibleLayoutOffset;
RenderBox childToPaint = _getFirstVisibleChild();
ListWheelParentData childParentData = childToPaint?.parentData; ListWheelParentData childParentData = childToPaint?.parentData;
while (childParentData != null while (childParentData != null) {
&& childParentData.offset.dy < lastVisibleLayoutOffset) {
_paintTransformedChild(childToPaint, context, offset, childParentData.offset); _paintTransformedChild(childToPaint, context, offset, childParentData.offset);
childToPaint = childAfter(childToPaint); childToPaint = childAfter(childToPaint);
childParentData = childToPaint?.parentData; childParentData = childToPaint?.parentData;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
...@@ -20,6 +21,176 @@ import 'scroll_position.dart'; ...@@ -20,6 +21,176 @@ import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart'; import 'scroll_position_with_single_context.dart';
import 'scrollable.dart'; import 'scrollable.dart';
/// A delegate that supplies children for [ListWheelScrollView].
///
/// [ListWheelScrollView] lazily constructs its children during layout to avoid
/// creating more children than are visible through the [Viewport]. This
/// delegate is responsible for providing children to [ListWheelScrollView]
/// during that stage.
///
/// See also:
/// * [ListWheelChildListDelegate], a delegate that supplies children using an
/// explicit list.
/// * [ListWheelChildLoopingListDelegate], a delegate that supplies infinite
/// children by looping an explicit list.
/// * [ListWheelChildBuilderDelegate], a delegate that supplies children using
/// a builder callback.
abstract class ListWheelChildDelegate {
/// Return the child at the given index. If the child at the given
/// index does not exist, return null.
Widget build(BuildContext context, int index);
/// Returns an estimate of the number of children this delegate will build.
int get estimatedChildCount;
/// Returns the true index for a child built at a given index. Defaults to
/// the given index, however if the delegate is [ListWheelChildLoopingListDelegate],
/// this value is the index of the true element that the delegate is looping to.
///
///
/// Example: [ListWheelChildLoopingListDelegate] is built by looping a list of
/// length 8. Then, trueIndexOf(10) = 2 and trueIndexOf(-5) = 3.
int trueIndexOf(int index) => index;
/// Called to check whether this and the old delegate are actually 'different',
/// so that the caller can decide to rebuild or not.
bool shouldRebuild(covariant ListWheelChildDelegate oldDelegate);
}
/// A delegate that supplies children for [ListWheelScrollView] using an
/// explicit list.
///
/// [ListWheelScrollView] lazily constructs its children to avoid creating more
/// children than are visible through the [Viewport]. This delegate provides
/// children using an explicit list, which is convenient but reduces the benefit
/// of building children lazily.
///
/// In general building all the widgets in advance is not efficient. It is
/// better to create a delegate that builds them on demand using
/// [ListWheelChildBuilderDelegate] or by subclassing [ListWheelChildDelegate]
/// directly.
///
/// This class is provided for the cases where either the list of children is
/// known well in advance (ideally the children are themselves compile-time
/// constants, for example), and therefore will not be built each time the
/// delegate itself is created, or the list is small, such that it's likely
/// always visible (and thus there is nothing to be gained by building it on
/// demand). For example, the body of a dialog box might fit both of these
/// conditions.
class ListWheelChildListDelegate extends ListWheelChildDelegate {
/// Constructs the delegate from a concrete list of children.
ListWheelChildListDelegate({@required this.children}) : assert(children != null);
/// The list containing all children that can be supplied.
final List<Widget> children;
@override
int get estimatedChildCount => children.length;
@override
Widget build(BuildContext context, int index) {
if (index < 0 || index >= children.length)
return null;
return children[index];
}
@override
bool shouldRebuild(covariant ListWheelChildListDelegate oldDelegate) {
return children != oldDelegate.children;
}
}
/// A delegate that supplies infinite children for [ListWheelScrollView] by
/// looping an explicit list.
///
/// [ListWheelScrollView] lazily constructs its children to avoid creating more
/// children than are visible through the [Viewport]. This delegate provides
/// children using an explicit list, which is convenient but reduces the benefit
/// of building children lazily.
///
/// In general building all the widgets in advance is not efficient. It is
/// better to create a delegate that builds them on demand using
/// [ListWheelChildBuilderDelegate] or by subclassing [ListWheelChildDelegate]
/// directly.
///
/// This class is provided for the cases where either the list of children is
/// known well in advance (ideally the children are themselves compile-time
/// constants, for example), and therefore will not be built each time the
/// delegate itself is created, or the list is small, such that it's likely
/// always visible (and thus there is nothing to be gained by building it on
/// demand). For example, the body of a dialog box might fit both of these
/// conditions.
class ListWheelChildLoopingListDelegate extends ListWheelChildDelegate {
/// Constructs the delegate from a concrete list of children.
ListWheelChildLoopingListDelegate({@required this.children}) : assert(children != null);
/// The list containing all children that can be supplied.
final List<Widget> children;
@override
int get estimatedChildCount => null;
@override
int trueIndexOf(int index) => index % children.length;
@override
Widget build(BuildContext context, int index) {
if (children.isEmpty)
return null;
return children[index % children.length];
}
@override
bool shouldRebuild(covariant ListWheelChildLoopingListDelegate oldDelegate) {
return children != oldDelegate.children;
}
}
/// A delegate that supplies children for [ListWheelScrollView] using a builder
/// callback.
///
/// [ListWheelScrollView] lazily constructs its children to avoid creating more
/// children than are visible through the [Viewport]. This delegate provides
/// children using an [IndexedWidgetBuilder] callback, so that the children do
/// not have to be built until they are displayed.
class ListWheelChildBuilderDelegate extends ListWheelChildDelegate {
/// Constructs the delegate from a builder callback.
ListWheelChildBuilderDelegate({
@required this.builder,
this.childCount,
}) : assert(builder != null);
/// Called lazily to build children.
final IndexedWidgetBuilder builder;
/// {@template flutter.widgets.wheelList.childCount}
/// If non-null, [childCount] is the maximum number of children that can be
/// provided, and children are available from 0 to [childCount] - 1.
///
/// If null, then the lower and upper limit are not known. However the [builder]
/// must provide children for a contiguous segment. If the builder returns null
/// at some index, the segment terminates there.
/// {@endtemplate}
final int childCount;
@override
int get estimatedChildCount => childCount;
@override
Widget build(BuildContext context, int index) {
if (childCount == null)
return builder(context, index);
if (index < 0 || index >= childCount)
return null;
return builder(context, index);
}
@override
bool shouldRebuild(covariant ListWheelChildBuilderDelegate oldDelegate) {
return builder != oldDelegate.builder || childCount != oldDelegate.childCount;
}
}
/// A controller for scroll views whose items have the same size. /// A controller for scroll views whose items have the same size.
/// ///
/// Similar to a standard [ScrollController] but with the added convenience /// Similar to a standard [ScrollController] but with the added convenience
...@@ -388,8 +559,9 @@ class FixedExtentScrollPhysics extends ScrollPhysics { ...@@ -388,8 +559,9 @@ class FixedExtentScrollPhysics extends ScrollPhysics {
/// The children are rendered as if rotating on a wheel instead of scrolling on /// The children are rendered as if rotating on a wheel instead of scrolling on
/// a plane. /// a plane.
class ListWheelScrollView extends StatefulWidget { class ListWheelScrollView extends StatefulWidget {
/// Creates a box in which children are scrolled on a wheel. /// Constructs a list in which children are scrolled a wheel. Its children
const ListWheelScrollView({ /// are passed to a delegate and lazily built during layout.
ListWheelScrollView({
Key key, Key key,
this.controller, this.controller,
this.physics, this.physics,
...@@ -402,8 +574,43 @@ class ListWheelScrollView extends StatefulWidget { ...@@ -402,8 +574,43 @@ class ListWheelScrollView extends StatefulWidget {
this.onSelectedItemChanged, this.onSelectedItemChanged,
this.clipToSize = true, this.clipToSize = true,
this.renderChildrenOutsideViewport = false, this.renderChildrenOutsideViewport = false,
@required this.children, @required List<Widget> children,
}) : assert(diameterRatio != null), }) : assert(children != null),
assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(perspective != null),
assert(perspective > 0),
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(clipToSize != null),
assert(renderChildrenOutsideViewport != null),
assert(
!renderChildrenOutsideViewport || !clipToSize,
RenderListWheelViewport.clipToSizeAndRenderChildrenOutsideViewportConflict,
),
childDelegate = new ListWheelChildListDelegate(children: children),
super(key: key);
/// Constructs a list in which children are scrolled a wheel. Its children
/// are managed by a delegate and are lazily built during layout.
const ListWheelScrollView.useDelegate({
Key key,
this.controller,
this.physics,
this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio,
this.perspective = RenderListWheelViewport.defaultPerspective,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
@required this.itemExtent,
this.onSelectedItemChanged,
this.clipToSize = true,
this.renderChildrenOutsideViewport = false,
@required this.childDelegate,
}) : assert(childDelegate != null),
assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage), assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(perspective != null), assert(perspective != null),
assert(perspective > 0), assert(perspective > 0),
...@@ -472,8 +679,8 @@ class ListWheelScrollView extends StatefulWidget { ...@@ -472,8 +679,8 @@ class ListWheelScrollView extends StatefulWidget {
/// {@macro flutter.rendering.wheelList.renderChildrenOutsideViewport} /// {@macro flutter.rendering.wheelList.renderChildrenOutsideViewport}
final bool renderChildrenOutsideViewport; final bool renderChildrenOutsideViewport;
/// List of children to scroll on top of the cylinder. /// A delegate that helps lazily instantiating child.
final List<Widget> children; final ListWheelChildDelegate childDelegate;
@override @override
_ListWheelScrollViewState createState() => new _ListWheelScrollViewState(); _ListWheelScrollViewState createState() => new _ListWheelScrollViewState();
...@@ -517,7 +724,8 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> { ...@@ -517,7 +724,8 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
final int currentItemIndex = metrics.itemIndex; final int currentItemIndex = metrics.itemIndex;
if (currentItemIndex != _lastReportedItemIndex) { if (currentItemIndex != _lastReportedItemIndex) {
_lastReportedItemIndex = currentItemIndex; _lastReportedItemIndex = currentItemIndex;
widget.onSelectedItemChanged(currentItemIndex); final int trueIndex = widget.childDelegate.trueIndexOf(currentItemIndex);
widget.onSelectedItemChanged(trueIndex);
} }
} }
return false; return false;
...@@ -537,7 +745,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> { ...@@ -537,7 +745,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
clipToSize: widget.clipToSize, clipToSize: widget.clipToSize,
renderChildrenOutsideViewport: widget.renderChildrenOutsideViewport, renderChildrenOutsideViewport: widget.renderChildrenOutsideViewport,
offset: offset, offset: offset,
children: widget.children, childDelegate: widget.childDelegate,
); );
}, },
), ),
...@@ -545,11 +753,159 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> { ...@@ -545,11 +753,159 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
} }
} }
/// Element that supports building children lazily for [ListWheelViewport].
class ListWheelElement extends RenderObjectElement implements ListWheelChildManager {
/// Creates an element that lazily builds children for the given widget.
ListWheelElement(ListWheelViewport widget) : super(widget);
@override
ListWheelViewport get widget => super.widget;
@override
RenderListWheelViewport get renderObject => super.renderObject;
// We inflate widgets at two different times:
// 1. When we ourselves are told to rebuild (see performRebuild).
// 2. When our render object needs a new child (see createChild).
// In both cases, we cache the results of calling into our delegate to get the
// widget, so that if we do case 2 later, we don't call the builder again.
// Any time we do case 1, though, we reset the cache.
/// A cache of widgets so that we don't have to rebuild every time.
final Map<int, Widget> _childWidgets = new HashMap<int, Widget>();
/// The map containing all active child elements. SplayTreeMap is used so that
/// we have all elements ordered and iterable by their keys.
final SplayTreeMap<int, Element> _childElements = new SplayTreeMap<int, Element>();
@override
void update(ListWheelViewport newWidget) {
final ListWheelViewport oldWidget = widget;
super.update(newWidget);
final ListWheelChildDelegate newDelegate = newWidget.childDelegate;
final ListWheelChildDelegate oldDelegate = oldWidget.childDelegate;
if (newDelegate != oldDelegate &&
(newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
performRebuild();
}
@override
int get childCount => widget.childDelegate.estimatedChildCount;
@override
void performRebuild() {
_childWidgets.clear();
super.performRebuild();
if (_childElements.isEmpty)
return;
final int firstIndex = _childElements.firstKey();
final int lastIndex = _childElements.lastKey();
for (int index = firstIndex; index <= lastIndex; ++index) {
final Element newChild = updateChild(_childElements[index], retrieveWidget(index), index);
if (newChild != null) {
_childElements[index] = newChild;
} else {
_childElements.remove(index);
}
}
}
/// Asks the underlying delegate for a widget at the given index.
///
/// Normally the builder is only called once for each index and the result
/// will be cached. However when the element is rebuilt, the cache will be
/// cleared.
Widget retrieveWidget(int index) {
return _childWidgets.putIfAbsent(index, () => widget.childDelegate.build(this, index));
}
@override
bool childExistsAt(int index) => retrieveWidget(index) != null;
@override
void createChild(int index, { @required RenderBox after }) {
owner.buildScope(this, () {
final bool insertFirst = after == null;
assert(insertFirst || _childElements[index - 1] != null);
final Element newChild =
updateChild(_childElements[index], retrieveWidget(index), index);
if (newChild != null) {
_childElements[index] = newChild;
} else {
_childElements.remove(index);
}
});
}
@override
void removeChild(RenderBox child) {
final int index = renderObject.indexOf(child);
owner.buildScope(this, () {
assert(_childElements.containsKey(index));
final Element result = updateChild(_childElements[index], null, index);
assert(result == null);
_childElements.remove(index);
assert(!_childElements.containsKey(index));
});
}
@override
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
final ListWheelParentData oldParentData = child?.renderObject?.parentData;
final Element newChild = super.updateChild(child, newWidget, newSlot);
final ListWheelParentData newParentData = newChild?.renderObject?.parentData;
if (newParentData != null) {
newParentData.index = newSlot;
if (oldParentData != null)
newParentData.offset = oldParentData.offset;
}
return newChild;
}
@override
void insertChildRenderObject(RenderObject child, int slot) {
final RenderListWheelViewport renderObject = this.renderObject;
assert(renderObject.debugValidateChild(child));
renderObject.insert(child, after: _childElements[slot - 1]?.renderObject);
assert(renderObject == this.renderObject);
}
@override
void moveChildRenderObject(RenderObject child, dynamic slot) {
const String moveChildRenderObjectErrorMessage =
'Currently we maintain the list in contiguous increasing order, so '
'moving children around is not allowed.';
assert(false, moveChildRenderObjectErrorMessage);
}
@override
void removeChildRenderObject(RenderObject child) {
assert(child.parent == renderObject);
renderObject.remove(child);
}
@override
void visitChildren(ElementVisitor visitor) {
_childElements.forEach((int key, Element child) {
visitor(child);
});
}
@override
void forgetChild(Element child) {
_childElements.remove(child.slot);
}
}
/// A viewport showing a subset of children on a wheel. /// A viewport showing a subset of children on a wheel.
/// ///
/// Typically used with [ListWheelScrollView], this viewport is similar to /// Typically used with [ListWheelScrollView], this viewport is similar to
/// [Viewport] in that it shows a subset of children in a scrollable based /// [Viewport] in that it shows a subset of children in a scrollable based
/// on the scrolling offset and the childrens' dimensions. But uses /// on the scrolling offset and the children's dimensions. But uses
/// [RenderListWheelViewport] to display the children on a wheel. /// [RenderListWheelViewport] to display the children on a wheel.
/// ///
/// See also: /// See also:
...@@ -557,8 +913,8 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> { ...@@ -557,8 +913,8 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
/// * [ListWheelScrollView], widget that combines this viewport with a scrollable. /// * [ListWheelScrollView], widget that combines this viewport with a scrollable.
/// * [RenderListWheelViewport], the render object that renders the children /// * [RenderListWheelViewport], the render object that renders the children
/// on a wheel. /// on a wheel.
class ListWheelViewport extends MultiChildRenderObjectWidget { class ListWheelViewport extends RenderObjectWidget {
/// Create a viewport where children are rendered onto a wheel. /// Creates a viewport where children are rendered onto a wheel.
/// ///
/// The [diameterRatio] argument defaults to 2.0 and must not be null. /// The [diameterRatio] argument defaults to 2.0 and must not be null.
/// ///
...@@ -572,7 +928,7 @@ class ListWheelViewport extends MultiChildRenderObjectWidget { ...@@ -572,7 +928,7 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
/// not be null. /// not be null.
/// ///
/// The [offset] argument must be provided and must not be null. /// The [offset] argument must be provided and must not be null.
ListWheelViewport({ const ListWheelViewport({
Key key, Key key,
this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio, this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio,
this.perspective = RenderListWheelViewport.defaultPerspective, this.perspective = RenderListWheelViewport.defaultPerspective,
...@@ -583,8 +939,9 @@ class ListWheelViewport extends MultiChildRenderObjectWidget { ...@@ -583,8 +939,9 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
this.clipToSize = true, this.clipToSize = true,
this.renderChildrenOutsideViewport = false, this.renderChildrenOutsideViewport = false,
@required this.offset, @required this.offset,
List<Widget> children, @required this.childDelegate,
}) : assert(offset != null), }) : assert(childDelegate != null),
assert(offset != null),
assert(diameterRatio != null), assert(diameterRatio != null),
assert(diameterRatio > 0, RenderListWheelViewport.diameterRatioZeroMessage), assert(diameterRatio > 0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(perspective != null), assert(perspective != null),
...@@ -598,7 +955,7 @@ class ListWheelViewport extends MultiChildRenderObjectWidget { ...@@ -598,7 +955,7 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
!renderChildrenOutsideViewport || !clipToSize, !renderChildrenOutsideViewport || !clipToSize,
RenderListWheelViewport.clipToSizeAndRenderChildrenOutsideViewportConflict, RenderListWheelViewport.clipToSizeAndRenderChildrenOutsideViewportConflict,
), ),
super(key: key, children: children); super(key: key);
/// {@macro flutter.rendering.wheelList.diameterRatio} /// {@macro flutter.rendering.wheelList.diameterRatio}
final double diameterRatio; final double diameterRatio;
...@@ -628,9 +985,18 @@ class ListWheelViewport extends MultiChildRenderObjectWidget { ...@@ -628,9 +985,18 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
/// in the viewport. /// in the viewport.
final ViewportOffset offset; final ViewportOffset offset;
/// A delegate that lazily instantiates children.
final ListWheelChildDelegate childDelegate;
@override
ListWheelElement createElement() => new ListWheelElement(this);
@override @override
RenderListWheelViewport createRenderObject(BuildContext context) { RenderListWheelViewport createRenderObject(BuildContext context) {
final ListWheelElement childManager = context;
return new RenderListWheelViewport( return new RenderListWheelViewport(
childManager: childManager,
offset: offset,
diameterRatio: diameterRatio, diameterRatio: diameterRatio,
perspective: perspective, perspective: perspective,
offAxisFraction: offAxisFraction, offAxisFraction: offAxisFraction,
...@@ -639,13 +1005,13 @@ class ListWheelViewport extends MultiChildRenderObjectWidget { ...@@ -639,13 +1005,13 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
itemExtent: itemExtent, itemExtent: itemExtent,
clipToSize: clipToSize, clipToSize: clipToSize,
renderChildrenOutsideViewport: renderChildrenOutsideViewport, renderChildrenOutsideViewport: renderChildrenOutsideViewport,
offset: offset,
); );
} }
@override @override
void updateRenderObject(BuildContext context, RenderListWheelViewport renderObject) { void updateRenderObject(BuildContext context, RenderListWheelViewport renderObject) {
renderObject renderObject
..offset = offset
..diameterRatio = diameterRatio ..diameterRatio = diameterRatio
..perspective = perspective ..perspective = perspective
..offAxisFraction = offAxisFraction ..offAxisFraction = offAxisFraction
...@@ -653,7 +1019,6 @@ class ListWheelViewport extends MultiChildRenderObjectWidget { ...@@ -653,7 +1019,6 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
..magnification = magnification ..magnification = magnification
..itemExtent = itemExtent ..itemExtent = itemExtent
..clipToSize = clipToSize ..clipToSize = clipToSize
..renderChildrenOutsideViewport = renderChildrenOutsideViewport ..renderChildrenOutsideViewport = renderChildrenOutsideViewport;
..offset = offset;
} }
} }
...@@ -240,11 +240,10 @@ void main() { ...@@ -240,11 +240,10 @@ void main() {
1000.0, 1000.0,
); );
expect( // Should have been flung far enough that even the first item goes off
tester.getTopLeft(find.widgetWithText(Container, '0')).dy, // screen and gets removed.
// Should have been flung far enough to go off screen. expect(find.widgetWithText(Container, '0').evaluate().isEmpty, true);
greaterThan(600.0),
);
expect( expect(
selectedItems, selectedItems,
// This specific throw was fast enough that each scroll update landed // This specific throw was fast enough that each scroll update landed
......
...@@ -41,11 +41,11 @@ void main() { ...@@ -41,11 +41,11 @@ void main() {
testWidgets('ListWheelScrollView can have zero child', (WidgetTester tester) async { testWidgets('ListWheelScrollView can have zero child', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ListWheelScrollView( child: ListWheelScrollView(
itemExtent: 50.0, itemExtent: 50.0,
children: <Widget>[], children: const <Widget>[],
), ),
), ),
); );
...@@ -68,6 +68,149 @@ void main() { ...@@ -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', () { group('layout', () {
testWidgets("ListWheelScrollView takes parent's size with small children", (WidgetTester tester) async { testWidgets("ListWheelScrollView takes parent's size with small children", (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -113,11 +256,11 @@ void main() { ...@@ -113,11 +256,11 @@ void main() {
testWidgets("ListWheelScrollView children can't be bigger than itemExtent", (WidgetTester tester) async { testWidgets("ListWheelScrollView children can't be bigger than itemExtent", (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ListWheelScrollView( child: ListWheelScrollView(
itemExtent: 50.0, itemExtent: 50.0,
children: <Widget>[ children: const <Widget>[
SizedBox( SizedBox(
height: 200.0, height: 200.0,
width: 200.0, width: 200.0,
...@@ -132,6 +275,80 @@ void main() { ...@@ -132,6 +275,80 @@ void main() {
expect(tester.getSize(find.byType(SizedBox)), const Size(200.0, 50.0)); expect(tester.getSize(find.byType(SizedBox)), const Size(200.0, 50.0));
expect(find.text('blah'), findsOneWidget); 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', () { group('pre-transform viewport', () {
...@@ -1014,6 +1231,10 @@ void main() { ...@@ -1014,6 +1231,10 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.offset, 500.0); 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(); tester.renderObject(find.byWidget(innerChildren[9])).showOnScreen();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.offset, 900.0); 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