Commit bec3fdef authored by Hans Muller's avatar Hans Muller

Add a Scrollable builder, refactor ScrollableList, et al (#3950)

* Add a Scrollable builder, refactor ScrollableList, et al

* Add space between the dialog demo buttons

* removed vestigial code
parent 0e8f26dd
......@@ -197,6 +197,14 @@ class DialogDemoState extends State<DialogDemo> {
}
)
]
// Add a little space between the buttons
.map((Widget button) {
return new Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: button
);
})
.toList()
)
);
}
......
......@@ -130,74 +130,109 @@ class LazyBlockChildren extends LazyBlockDelegate {
/// Prefer [ScrollableList] when all the children have the same height because
/// it can use that property to be more efficient. Prefer [ScrollableViewport]
/// when there is only one child.
class LazyBlock extends Scrollable {
class LazyBlock extends StatelessWidget {
LazyBlock({
Key key,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ScrollListener onScrollStart,
ScrollListener onScroll,
ScrollListener onScrollEnd,
SnapOffsetCallback snapOffsetCallback,
this.delegate,
this.padding
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback
);
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.scrollableKey,
this.padding,
this.delegate
}) : super(key: key);
/// Provides children for this widget.
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// Called to determine the offset to which scrolling should snap,
/// when handling a fling.
///
/// See [LazyBlockDelegate] for details.
final LazyBlockDelegate delegate;
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// The key for the Scrollable created by this widget.
final Key scrollableKey;
/// The amount of space by which to inset the children inside the viewport.
final EdgeInsets padding;
@override
ScrollableState<LazyBlock> createState() => new _LazyBlockState();
}
class _LazyBlockState extends ScrollableState<LazyBlock> {
@override
BoundedBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
@override
BoundedBehavior get scrollBehavior => super.scrollBehavior;
/// Provides children for this widget.
///
/// See [LazyBlockDelegate] for details.
final LazyBlockDelegate delegate;
void _handleExtentsChanged(double contentExtent, double containerExtent, double minScrollOffset) {
setState(() {
didUpdateScrollBehavior(scrollBehavior.updateExtents(
void _handleExtentsChanged(
ScrollableState state,
double contentExtent,
double containerExtent,
double minScrollOffset) {
state.setState(() {
final BoundedBehavior scrollBehavior = state.scrollBehavior;
state.didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
minScrollOffset: minScrollOffset,
scrollOffset: scrollOffset
scrollOffset: state.scrollOffset
));
});
}
@override
Widget buildContent(BuildContext context) {
Widget _buildContent(BuildContext context, ScrollableState state) {
final bool clampOverscrolls = ClampOverscrolls.of(context);
final double startOffset = clampOverscrolls
? scrollOffset.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset)
: scrollOffset;
? state.scrollOffset.clamp(state.scrollBehavior.minScrollOffset, state.scrollBehavior.maxScrollOffset)
: state.scrollOffset;
Widget viewport = new LazyBlockViewport(
startOffset: startOffset,
mainAxis: config.scrollDirection,
padding: config.padding,
onExtentsChanged: _handleExtentsChanged,
delegate: config.delegate
mainAxis: scrollDirection,
padding: padding,
onExtentsChanged: (double contentExtent, double containerExtent, double minScrollOffset) {
_handleExtentsChanged(state, contentExtent, containerExtent, minScrollOffset);
},
delegate: delegate
);
if (clampOverscrolls)
viewport = new ClampOverscrolls(value: false, child: viewport);
return viewport;
}
@override
Widget build(BuildContext context) {
return new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
}
}
/// Signature used by [LazyBlockViewport] to report its interior and exterior dimensions.
......
......@@ -24,6 +24,7 @@ final Tolerance kPixelScrollTolerance = new Tolerance(
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
typedef Widget ScrollBuilder(BuildContext context, ScrollableState state);
typedef void ScrollListener(double scrollOffset);
typedef double SnapOffsetCallback(double scrollOffset, Size containerSize);
......@@ -34,7 +35,7 @@ typedef double SnapOffsetCallback(double scrollOffset, Size containerSize);
///
/// Widgets that subclass [Scrollable] typically use state objects that subclass
/// [ScrollableState].
abstract class Scrollable extends StatefulWidget {
class Scrollable extends StatefulWidget {
Scrollable({
Key key,
this.initialScrollOffset,
......@@ -43,7 +44,8 @@ abstract class Scrollable extends StatefulWidget {
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback
this.snapOffsetCallback,
this.builder
}) : super(key: key) {
assert(scrollDirection == Axis.vertical || scrollDirection == Axis.horizontal);
assert(scrollAnchor == ViewportAnchor.start || scrollAnchor == ViewportAnchor.end);
......@@ -106,6 +108,8 @@ abstract class Scrollable extends StatefulWidget {
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
final ScrollBuilder builder;
/// The state from the closest instance of this class that encloses the given context.
static ScrollableState of(BuildContext context) {
return context.ancestorStateOfType(const TypeMatcher<ScrollableState>());
......@@ -176,7 +180,7 @@ abstract class Scrollable extends StatefulWidget {
}
@override
ScrollableState createState();
ScrollableState createState() => new ScrollableState<Scrollable>();
}
/// Contains the state for common scrolling widgets that scroll only
......@@ -219,7 +223,7 @@ abstract class Scrollable extends StatefulWidget {
/// terms of the [pixelOffsetToScrollOffset] and
/// [scrollOffsetToPixelOffset] methods.
@optionalTypeArgs
abstract class ScrollableState<T extends Scrollable> extends State<T> {
class ScrollableState<T extends Scrollable> extends State<T> {
@override
void initState() {
super.initState();
......@@ -306,14 +310,14 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// Scroll behaviors control where the boundaries of the scrollable are placed
/// and how the scrolling physics should behave near those boundaries and
/// after the user stops directly manipulating the scrollable.
ScrollBehavior<double, double> get scrollBehavior {
ExtentScrollBehavior get scrollBehavior {
return _scrollBehavior ??= createScrollBehavior();
}
ScrollBehavior<double, double> _scrollBehavior;
ExtentScrollBehavior _scrollBehavior;
/// Subclasses should override this function to create the [ScrollBehavior]
/// they desire.
ScrollBehavior<double, double> createScrollBehavior();
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
bool _scrollOffsetIsInBounds(double scrollOffset) {
if (scrollBehavior is! ExtentScrollBehavior)
......@@ -612,7 +616,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return const <Type, GestureRecognizerFactory>{};
}
/// Subclasses should override this function to build the interior of their
/// Subclasses can override this function to build the interior of their
/// scrollable widget. Scrollable wraps the returned widget in a
/// [GestureDetector] to observe the user's interaction with this widget and
/// to adjust the scroll offset accordingly.
......@@ -628,7 +632,10 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
/// This callback should be used to update the scroll behavior, if
/// necessary, and then to call [updateGestureDetector] to update
/// the gesture detectors accordingly.
Widget buildContent(BuildContext context);
Widget buildContent(BuildContext context) {
// TBD: if config.builder is null throw a sensible error.
return config.builder(context, this);
}
}
/// Indicates if a [ScrollNotification] indicates the start, end or the
......@@ -658,71 +665,135 @@ class ScrollNotification extends Notification {
final ScrollableState scrollable;
}
/// A simple scrollable widget that has a single child. Use this widget if
/// A simple scrolling widget that has a single child. Use this widget if
/// you are not worried about offscreen widgets consuming resources.
class ScrollableViewport extends Scrollable {
class ScrollableViewport extends StatefulWidget {
ScrollableViewport({
Key key,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScrollStart,
ScrollListener onScroll,
ScrollListener onScrollEnd,
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.scrollableKey,
this.child
}) : super(
key: key,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
initialScrollOffset: initialScrollOffset,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd
);
/// The widget below this widget in the tree.
final Widget child;
}) : super(key: key);
@override
ScrollableState createState() => new _ScrollableViewportState();
}
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
@override
OverscrollWhenScrollableBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Whether to place first child at the start of the container or
/// the last child at the end of the container, when the scrollable
/// has not been scrolled and has no initial scroll offset.
///
/// For example, if the [scrollDirection] is [Axis.vertical] and
/// there are enough items to overflow the container, then
/// [ViewportAnchor.start] means that the top of the first item
/// should be aligned with the top of the scrollable with the last
/// item below the bottom, and [ViewportAnchor.end] means the bottom
/// of the last item should be aligned with the bottom of the
/// scrollable, with the first item above the top.
///
/// This also affects whether, when an item is added or removed, the
/// displacement will be towards the first item or the last item.
/// Continuing the earlier example, if a new item is inserted in the
/// middle of the list, in the [ViewportAnchor.start] case the items
/// after it (with greater indices, down to the item with the
/// highest index) will be pushed down, while in the
/// [ViewportAnchor.end] case the items before it (with lower
/// indices, up to the item with the index 0) will be pushed up.
///
/// Subclasses may ignore this value if, for instance, they do not
/// have a concept of an anchor, or have more complicated behavior
/// (e.g. they would by default put the middle item in the middle of
/// the container).
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// Called to determine the offset to which scrolling should snap,
/// when handling a fling.
///
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// The key for the Scrollable created by this widget.
final Key scrollableKey;
/// The widget that will be scrolled. It will become the child of a Scrollable.
final Widget child;
@override
OverscrollWhenScrollableBehavior get scrollBehavior => super.scrollBehavior;
_ScrollableViewportState createState() => new _ScrollableViewportState();
}
class _ScrollableViewportState extends State<ScrollableViewport> {
double _viewportSize = 0.0;
double _childSize = 0.0;
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) {
Offset _handlePaintOffsetUpdateNeeded(ScrollableState state, ViewportDimensions dimensions) {
// We make various state changes here but don't have to do so in a
// setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the
// render object via our return value.
_viewportSize = config.scrollDirection == Axis.vertical ? dimensions.containerSize.height : dimensions.containerSize.width;
_childSize = config.scrollDirection == Axis.vertical ? dimensions.contentSize.height : dimensions.contentSize.width;
didUpdateScrollBehavior(scrollBehavior.updateExtents(
state.didUpdateScrollBehavior(state.scrollBehavior.updateExtents(
contentExtent: _childSize,
containerExtent: _viewportSize,
scrollOffset: scrollOffset
scrollOffset: state.scrollOffset
));
updateGestureDetector();
return scrollOffsetToPixelDelta(scrollOffset);
state.updateGestureDetector();
return state.scrollOffsetToPixelDelta(state.scrollOffset);
}
@override
Widget buildContent(BuildContext context) {
Widget _buildContent(BuildContext context, ScrollableState state) {
return new Viewport(
paintOffset: scrollOffsetToPixelDelta(scrollOffset),
paintOffset: state.scrollOffsetToPixelDelta(state.scrollOffset),
mainAxis: config.scrollDirection,
anchor: config.scrollAnchor,
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded,
onPaintOffsetUpdateNeeded: (ViewportDimensions dimensions) {
return _handlePaintOffsetUpdateNeeded(state, dimensions);
},
child: config.child
);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
key: config.scrollableKey,
initialScrollOffset: config.initialScrollOffset,
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
onScrollStart: config.onScrollStart,
onScroll: config.onScroll,
onScrollEnd: config.onScrollEnd,
snapOffsetCallback: config.snapOffsetCallback,
builder: _buildContent
);
}
}
/// A mashup of [ScrollableViewport] and [BlockBody]. Useful when you have a small,
......
......@@ -8,67 +8,93 @@ import 'package:collection/collection.dart' show lowerBound;
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'scroll_behavior.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
/// A vertically scrollable grid.
///
/// Requires that delegate places its children in row-major order.
class ScrollableGrid extends Scrollable {
class ScrollableGrid extends StatelessWidget {
ScrollableGrid({
Key key,
double initialScrollOffset,
ScrollListener onScrollStart,
ScrollListener onScroll,
ScrollListener onScrollEnd,
SnapOffsetCallback snapOffsetCallback,
this.initialScrollOffset,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.scrollableKey,
this.delegate,
this.children
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
// TODO(abarth): Support horizontal offsets. For horizontally scrolling
// grids. For horizontally scrolling grids, we'll probably need to use a
// delegate that places children in column-major order.
scrollDirection: Axis.vertical,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback
);
}) : super(key: key);
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// Called to determine the offset to which scrolling should snap,
/// when handling a fling.
///
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// The key for the Scrollable created by this widget.
final Key scrollableKey;
final GridDelegate delegate;
final Iterable<Widget> children;
@override
ScrollableState createState() => new _ScrollableGridState();
}
class _ScrollableGridState extends ScrollableState<ScrollableGrid> {
@override
ExtentScrollBehavior createScrollBehavior() => new OverscrollBehavior();
@override
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleExtentsChanged(double contentExtent, double containerExtent) {
setState(() {
didUpdateScrollBehavior(scrollBehavior.updateExtents(
void _handleExtentsChanged(ScrollableState state, double contentExtent, double containerExtent) {
state.setState(() {
state.didUpdateScrollBehavior(state.scrollBehavior.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
scrollOffset: state.scrollOffset
));
});
}
@override
Widget buildContent(BuildContext context) {
Widget _buildContent(BuildContext context, ScrollableState state) {
return new GridViewport(
startOffset: scrollOffset,
delegate: config.delegate,
onExtentsChanged: _handleExtentsChanged,
children: config.children
startOffset: state.scrollOffset,
delegate: delegate,
onExtentsChanged: (double contentExtent, double containerExtent) {
_handleExtentsChanged(state, contentExtent, containerExtent);
},
children: children
);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
// TODO(abarth): Support horizontal offsets. For horizontally scrolling
// grids. For horizontally scrolling grids, we'll probably need to use a
// delegate that places children in column-major order.
scrollDirection: Axis.vertical,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
}
}
......
......@@ -45,82 +45,142 @@ class ClampOverscrolls extends InheritedWidget {
}
}
class ScrollableList extends Scrollable {
class ScrollableList extends StatelessWidget {
ScrollableList({
Key key,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScrollStart,
ScrollListener onScroll,
ScrollListener onScrollEnd,
SnapOffsetCallback snapOffsetCallback,
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.scrollableKey,
this.itemExtent,
this.itemsWrap: false,
this.padding,
this.children
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback
) {
}) : super(key: key) {
assert(itemExtent != null);
}
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Whether to place first child at the start of the container or
/// the last child at the end of the container, when the scrollable
/// has not been scrolled and has no initial scroll offset.
///
/// For example, if the [scrollDirection] is [Axis.vertical] and
/// there are enough items to overflow the container, then
/// [ViewportAnchor.start] means that the top of the first item
/// should be aligned with the top of the scrollable with the last
/// item below the bottom, and [ViewportAnchor.end] means the bottom
/// of the last item should be aligned with the bottom of the
/// scrollable, with the first item above the top.
///
/// This also affects whether, when an item is added or removed, the
/// displacement will be towards the first item or the last item.
/// Continuing the earlier example, if a new item is inserted in the
/// middle of the list, in the [ViewportAnchor.start] case the items
/// after it (with greater indices, down to the item with the
/// highest index) will be pushed down, while in the
/// [ViewportAnchor.end] case the items before it (with lower
/// indices, up to the item with the index 0) will be pushed up.
///
/// Subclasses may ignore this value if, for instance, they do not
/// have a concept of an anchor, or have more complicated behavior
/// (e.g. they would by default put the middle item in the middle of
/// the container).
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// Called to determine the offset to which scrolling should snap,
/// when handling a fling.
///
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// The key for the Scrollable created by this widget.
final Key scrollableKey;
/// The height of each item if [scrollDirection] is Axis.vertical, otherwise the width of each item.
final double itemExtent;
final bool itemsWrap;
/// The amount of space by which to inset the children inside the viewport.
final EdgeInsets padding;
/// The axis along which this widget should scroll.
final Iterable<Widget> children;
@override
ScrollableState createState() => new _ScrollableListState();
}
class _ScrollableListState extends ScrollableState<ScrollableList> {
@override
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
@override
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleExtentsChanged(double contentExtent, double containerExtent) {
setState(() {
didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: config.itemsWrap ? double.INFINITY : contentExtent,
void _handleExtentsChanged(ScrollableState state, double contentExtent, double containerExtent) {
state.setState(() {
state.didUpdateScrollBehavior(state.scrollBehavior.updateExtents(
contentExtent: itemsWrap ? double.INFINITY : contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
scrollOffset: state.scrollOffset
));
});
}
@override
Widget buildContent(BuildContext context) {
Widget _buildContent(BuildContext context, ScrollableState state) {
final bool clampOverscrolls = ClampOverscrolls.of(context);
final double listScrollOffset = clampOverscrolls
? scrollOffset.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset)
: scrollOffset;
? state.scrollOffset.clamp(state.scrollBehavior.minScrollOffset, state.scrollBehavior.maxScrollOffset)
: state.scrollOffset;
Widget viewport = new ListViewport(
onExtentsChanged: _handleExtentsChanged,
onExtentsChanged: (double contentExtent, double containerExtent) {
_handleExtentsChanged(state, contentExtent, containerExtent);
},
scrollOffset: listScrollOffset,
mainAxis: config.scrollDirection,
anchor: config.scrollAnchor,
itemExtent: config.itemExtent,
itemsWrap: config.itemsWrap,
padding: config.padding,
children: config.children
mainAxis: scrollDirection,
anchor: scrollAnchor,
itemExtent: itemExtent,
itemsWrap: itemsWrap,
padding: padding,
children: children
);
if (clampOverscrolls)
viewport = new ClampOverscrolls(value: false, child: viewport);
return viewport;
}
@override
Widget build(BuildContext context) {
return new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
}
}
class _VirtualListViewport extends VirtualViewport {
......@@ -318,70 +378,134 @@ class ListViewport extends _VirtualListViewport with VirtualViewportFromIterable
/// ScrollDirection.vertical itemExtent is the height of each item. Use this
/// widget when you have a large number of children or when you are concerned
/// about offscreen widgets consuming resources.
class ScrollableLazyList extends Scrollable {
class ScrollableLazyList extends StatelessWidget {
ScrollableLazyList({
Key key,
double initialScrollOffset,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
ScrollListener onScroll,
SnapOffsetCallback snapOffsetCallback,
this.initialScrollOffset,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.scrollableKey,
this.itemExtent,
this.itemCount,
this.itemBuilder,
this.padding
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScroll: onScroll,
snapOffsetCallback: snapOffsetCallback
) {
}) : super(key: key) {
assert(itemExtent != null);
assert(itemBuilder != null);
assert(itemCount != null || scrollAnchor == ViewportAnchor.start);
}
/// The scroll offset this widget should use when first created.
final double initialScrollOffset;
/// The axis along which this widget should scroll.
final Axis scrollDirection;
/// Whether to place first child at the start of the container or
/// the last child at the end of the container, when the scrollable
/// has not been scrolled and has no initial scroll offset.
///
/// For example, if the [scrollDirection] is [Axis.vertical] and
/// there are enough items to overflow the container, then
/// [ViewportAnchor.start] means that the top of the first item
/// should be aligned with the top of the scrollable with the last
/// item below the bottom, and [ViewportAnchor.end] means the bottom
/// of the last item should be aligned with the bottom of the
/// scrollable, with the first item above the top.
///
/// This also affects whether, when an item is added or removed, the
/// displacement will be towards the first item or the last item.
/// Continuing the earlier example, if a new item is inserted in the
/// middle of the list, in the [ViewportAnchor.start] case the items
/// after it (with greater indices, down to the item with the
/// highest index) will be pushed down, while in the
/// [ViewportAnchor.end] case the items before it (with lower
/// indices, up to the item with the index 0) will be pushed up.
///
/// Subclasses may ignore this value if, for instance, they do not
/// have a concept of an anchor, or have more complicated behavior
/// (e.g. they would by default put the middle item in the middle of
/// the container).
final ViewportAnchor scrollAnchor;
/// Called whenever this widget starts to scroll.
final ScrollListener onScrollStart;
/// Called whenever this widget's scroll offset changes.
final ScrollListener onScroll;
/// Called whenever this widget stops scrolling.
final ScrollListener onScrollEnd;
/// when handling a fling.
///
/// This callback, if set, will be called with the offset that the
/// Scrollable would have scrolled to in the absence of this
/// callback, and a Size describing the size of the Scrollable
/// itself.
///
/// The callback's return value is used as the new scroll offset to
/// aim for.
///
/// If the callback simply returns its first argument (the offset),
/// then it is as if the callback was null.
final SnapOffsetCallback snapOffsetCallback;
/// The key for the Scrollable created by this widget.
final Key scrollableKey;
/// The height of each item if [scrollDirection] is Axis.vertical, otherwise the width of each item.
final double itemExtent;
/// The total number of list items.
final int itemCount;
final ItemListBuilder itemBuilder;
/// The amount of space by which to inset the children inside the viewport.
/// The insets for the entire list.
final EdgeInsets padding;
@override
ScrollableState createState() => new _ScrollableLazyListState();
}
class _ScrollableLazyListState extends ScrollableState<ScrollableLazyList> {
@override
ExtentScrollBehavior createScrollBehavior() => new OverscrollBehavior();
@override
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleExtentsChanged(double contentExtent, double containerExtent) {
setState(() {
didUpdateScrollBehavior(scrollBehavior.updateExtents(
void _handleExtentsChanged(ScrollableState state, double contentExtent, double containerExtent) {
state.setState(() {
state.didUpdateScrollBehavior(state.scrollBehavior.updateExtents(
contentExtent: contentExtent,
containerExtent: containerExtent,
scrollOffset: scrollOffset
scrollOffset: state.scrollOffset
));
});
}
@override
Widget buildContent(BuildContext context) {
Widget _buildContent(BuildContext context, ScrollableState state) {
return new LazyListViewport(
onExtentsChanged: _handleExtentsChanged,
scrollOffset: scrollOffset,
mainAxis: config.scrollDirection,
anchor: config.scrollAnchor,
itemExtent: config.itemExtent,
itemCount: config.itemCount,
itemBuilder: config.itemBuilder,
padding: config.padding
onExtentsChanged: (double contentExtent, double containerExtent) {
_handleExtentsChanged(state, contentExtent, containerExtent);
},
scrollOffset: state.scrollOffset,
mainAxis: scrollDirection,
anchor: scrollAnchor,
itemExtent: itemExtent,
itemCount: itemCount,
itemBuilder: itemBuilder,
padding: padding
);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
}
}
......
......@@ -46,7 +46,7 @@ void main() {
expect(find.text('10'), findsNothing);
expect(find.text('100'), findsNothing);
ScrollableState targetState = tester.state(find.byType(ScrollableLazyList));
ScrollableState targetState = tester.state(find.byType(Scrollable));
targetState.scrollTo(1000.0);
await tester.pump(new Duration(seconds: 1));
......
......@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart';
Widget _buildScroller({Key key, List<String> log}) {
return new ScrollableViewport(
key: key,
scrollableKey: key,
onScrollStart: (double scrollOffset) {
log.add('scrollstart');
},
......
......@@ -13,8 +13,8 @@ void main() {
]
));
ScrollableState<ScrollableViewport> scrollable =
tester.state/*<ScrollableState<ScrollableViewport>>*/(find.byType(ScrollableViewport));
ScrollableState scrollable =
tester.state/*<ScrollableState>*/(find.byType(Scrollable));
expect(scrollable.scrollOffset, equals(0.0));
......
......@@ -75,10 +75,10 @@ void main() {
return result;
};
GlobalKey<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
FlipWidget testWidget = new FlipWidget(
left: new ScrollableLazyList(
key: scrollableKey,
scrollableKey: scrollableKey,
itemBuilder: itemBuilder,
itemExtent: 200.0,
initialScrollOffset: 300.0
......@@ -123,10 +123,10 @@ void main() {
return result;
};
GlobalKey<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
FlipWidget testWidget = new FlipWidget(
left: new ScrollableLazyList(
key: scrollableKey,
scrollableKey: scrollableKey,
itemBuilder: itemBuilder,
itemExtent: 200.0,
initialScrollOffset: 300.0,
......@@ -167,9 +167,9 @@ void main() {
return result;
};
GlobalKey<ScrollableState<ScrollableLazyList>> scrollableKey = new GlobalKey<ScrollableState<ScrollableLazyList>>();
GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
Widget testWidget = new ScrollableLazyList(
key: scrollableKey,
scrollableKey: scrollableKey,
itemBuilder: itemBuilder,
itemExtent: 300.0,
itemCount: 10
......
......@@ -29,7 +29,7 @@ Widget buildFrame() {
child: new Container(
height: itemExtent * 2.0,
child: new ScrollableList(
key: scrollableListKey,
scrollableKey: scrollableListKey,
snapOffsetCallback: snapOffsetCallback,
scrollDirection: scrollDirection,
itemExtent: itemExtent,
......
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