Commit c1d42a2f authored by Hans Muller's avatar Hans Muller

Revised PageableList

parent 4f22f3ec
...@@ -120,18 +120,11 @@ class PageableListAppState extends State<PageableListApp> { ...@@ -120,18 +120,11 @@ class PageableListAppState extends State<PageableListApp> {
} }
Widget _buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
Widget list = new PageableList<CardModel>( return new PageableList<CardModel>(
items: cardModels, items: cardModels,
itemsWrap: itemsWrap, itemsWrap: itemsWrap,
itemBuilder: buildCard, itemBuilder: buildCard,
scrollDirection: scrollDirection, scrollDirection: scrollDirection
itemExtent: (scrollDirection == ScrollDirection.vertical)
? pageSize.height
: pageSize.width
);
return new SizeObserver(
onSizeChanged: updatePageSize,
child: list
); );
} }
......
...@@ -3,20 +3,13 @@ ...@@ -3,20 +3,13 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:newton/newton.dart'; import 'package:newton/newton.dart';
const double _kSecondsPerMillisecond = 1000.0; const double _kSecondsPerMillisecond = 1000.0;
const double _kScrollDrag = 0.025; const double _kScrollDrag = 0.025;
// TODO(abarth): These values won't work well if there's a scale transform. /// An interface for controlling the behavior of scrollable widgets.
final Tolerance _kDefaultScrollTolerance = new Tolerance(
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
/// An interface for controlling the behavior of scrollable widgets
abstract class ScrollBehavior { abstract class ScrollBehavior {
/// Called when a drag gesture ends. Returns a simulation that /// Called when a drag gesture ends. Returns a simulation that
/// propels the scrollOffset. /// propels the scrollOffset.
...@@ -24,10 +17,10 @@ abstract class ScrollBehavior { ...@@ -24,10 +17,10 @@ abstract class ScrollBehavior {
/// Called when a drag gesture ends and toSnapOffset is specified. /// Called when a drag gesture ends and toSnapOffset is specified.
/// Returns an animation that ends at the snap offset. /// Returns an animation that ends at the snap offset.
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double velocity) => null; Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) => null;
/// Return the scroll offset to use when the user attempts to scroll /// Return the scroll offset to use when the user attempts to scroll
/// from the given offset by the given delta /// from the given offset by the given delta.
double applyCurve(double scrollOffset, double scrollDelta); double applyCurve(double scrollOffset, double scrollDelta);
/// Whether this scroll behavior currently permits scrolling /// Whether this scroll behavior currently permits scrolling
...@@ -39,11 +32,11 @@ abstract class ExtentScrollBehavior extends ScrollBehavior { ...@@ -39,11 +32,11 @@ abstract class ExtentScrollBehavior extends ScrollBehavior {
ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 }) ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: _contentExtent = contentExtent, _containerExtent = containerExtent; : _contentExtent = contentExtent, _containerExtent = containerExtent;
/// The linear extent of the content inside the scrollable widget /// The linear extent of the content inside the scrollable widget.
double get contentExtent => _contentExtent; double get contentExtent => _contentExtent;
double _contentExtent; double _contentExtent;
/// The linear extent of the exterior of the scrollable widget /// The linear extent of the exterior of the scrollable widget.
double get containerExtent => _containerExtent; double get containerExtent => _containerExtent;
double _containerExtent; double _containerExtent;
...@@ -64,14 +57,14 @@ abstract class ExtentScrollBehavior extends ScrollBehavior { ...@@ -64,14 +57,14 @@ abstract class ExtentScrollBehavior extends ScrollBehavior {
return scrollOffset.clamp(minScrollOffset, maxScrollOffset); return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
} }
/// The minimum value the scroll offset can obtain /// The minimum value the scroll offset can obtain.
double get minScrollOffset; double get minScrollOffset;
/// The maximum value the scroll offset can obatin /// The maximum value the scroll offset can obtain.
double get maxScrollOffset; double get maxScrollOffset;
} }
/// A scroll behavior that prevents the user from exeeding scroll bounds /// A scroll behavior that prevents the user from exeeding scroll bounds.
class BoundedBehavior extends ExtentScrollBehavior { class BoundedBehavior extends ExtentScrollBehavior {
BoundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 }) BoundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent); : super(contentExtent: contentExtent, containerExtent: containerExtent);
...@@ -84,7 +77,18 @@ class BoundedBehavior extends ExtentScrollBehavior { ...@@ -84,7 +77,18 @@ class BoundedBehavior extends ExtentScrollBehavior {
} }
} }
/// A scroll behavior that does not prevent the user from exeeding scroll bounds Simulation _createFlingScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
final double startVelocity = velocity * _kSecondsPerMillisecond;
final SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
return new ScrollSimulation(position, startVelocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
}
Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
final double velocity = startVelocity * _kSecondsPerMillisecond;
return new FrictionSimulation.through(startOffset, endOffset, velocity, endVelocity);
}
/// A scroll behavior that does not prevent the user from exeeding scroll bounds.
class UnboundedBehavior extends ExtentScrollBehavior { class UnboundedBehavior extends ExtentScrollBehavior {
UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 }) UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent); : super(contentExtent: contentExtent, containerExtent: containerExtent);
...@@ -96,8 +100,8 @@ class UnboundedBehavior extends ExtentScrollBehavior { ...@@ -96,8 +100,8 @@ class UnboundedBehavior extends ExtentScrollBehavior {
); );
} }
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double velocity) { Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return _createSnapScrollSimulation(startOffset, endOffset, velocity); return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
} }
double get minScrollOffset => double.NEGATIVE_INFINITY; double get minScrollOffset => double.NEGATIVE_INFINITY;
...@@ -108,33 +112,17 @@ class UnboundedBehavior extends ExtentScrollBehavior { ...@@ -108,33 +112,17 @@ class UnboundedBehavior extends ExtentScrollBehavior {
} }
} }
Simulation _createFlingScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset, Tolerance tolerance) { /// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
double startVelocity = velocity * _kSecondsPerMillisecond;
SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
ScrollSimulation simulation =
new ScrollSimulation(position, startVelocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag)
..tolerance = tolerance ?? _kDefaultScrollTolerance;
return simulation;
}
Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double velocity) {
double startVelocity = velocity * _kSecondsPerMillisecond;
double endVelocity = velocity.sign * _kDefaultScrollTolerance.velocity;
return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
}
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance
class OverscrollBehavior extends BoundedBehavior { class OverscrollBehavior extends BoundedBehavior {
OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 }) OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent); : super(contentExtent: contentExtent, containerExtent: containerExtent);
Simulation createFlingScrollSimulation(double position, double velocity, { Tolerance tolerance }) { Simulation createFlingScrollSimulation(double position, double velocity) {
return _createFlingScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, tolerance); return _createFlingScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
} }
Simulation createSnapScrollSimulation(double startOffset, double endOffset, double velocity) { Simulation createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
return _createSnapScrollSimulation(startOffset, endOffset, velocity); return _createSnapScrollSimulation(startOffset, endOffset, startVelocity, endVelocity);
} }
double applyCurve(double scrollOffset, double scrollDelta) { double applyCurve(double scrollOffset, double scrollDelta) {
...@@ -154,13 +142,13 @@ class OverscrollBehavior extends BoundedBehavior { ...@@ -154,13 +142,13 @@ class OverscrollBehavior extends BoundedBehavior {
} }
} }
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint /// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
class OverscrollWhenScrollableBehavior extends OverscrollBehavior { class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
bool get isScrollable => contentExtent > containerExtent; bool get isScrollable => contentExtent > containerExtent;
Simulation createFlingScrollSimulation(double position, double velocity, { Tolerance tolerance }) { Simulation createFlingScrollSimulation(double position, double velocity) {
if (isScrollable || position < minScrollOffset || position > maxScrollOffset) if (isScrollable || position < minScrollOffset || position > maxScrollOffset)
return super.createFlingScrollSimulation(position, velocity, tolerance: tolerance); return super.createFlingScrollSimulation(position, velocity);
return null; return null;
} }
......
...@@ -105,7 +105,8 @@ abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGest ...@@ -105,7 +105,8 @@ abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGest
Offset velocity = tracker.getVelocity(); Offset velocity = tracker.getVelocity();
if (velocity != null && _isFlingGesture(velocity)) if (velocity != null && _isFlingGesture(velocity))
onEnd(velocity); onEnd(velocity);
onEnd(Offset.zero); else
onEnd(Offset.zero);
} }
_velocityTrackers.clear(); _velocityTrackers.clear();
} }
......
...@@ -11,49 +11,39 @@ import 'basic.dart'; ...@@ -11,49 +11,39 @@ import 'basic.dart';
typedef List<Widget> ListBuilder(BuildContext context, int startIndex, int count); typedef List<Widget> ListBuilder(BuildContext context, int startIndex, int count);
class HomogeneousViewport extends RenderObjectWidget { abstract class _ViewportBase extends RenderObjectWidget {
HomogeneousViewport({ _ViewportBase({
Key key, Key key,
this.builder, this.builder,
this.itemsWrap: false, this.itemsWrap: false,
this.itemExtent, // required, must be non-zero this.itemCount,
this.itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
this.direction: ScrollDirection.vertical, this.direction: ScrollDirection.vertical,
this.startOffset: 0.0, this.startOffset: 0.0,
this.overlayPainter this.overlayPainter
}) : super(key: key) { }) : super(key: key);
assert(itemExtent != null);
assert(itemExtent > 0);
}
final ListBuilder builder; final ListBuilder builder;
final bool itemsWrap; final bool itemsWrap;
final double itemExtent;
final int itemCount; final int itemCount;
final ScrollDirection direction; final ScrollDirection direction;
final double startOffset; final double startOffset;
final Painter overlayPainter; final Painter overlayPainter;
_HomogeneousViewportElement createElement() => new _HomogeneousViewportElement(this);
// we don't pass constructor arguments to the RenderBlockViewport() because until // we don't pass constructor arguments to the RenderBlockViewport() because until
// we know our children, the constructor arguments we could give have no effect // we know our children, the constructor arguments we could give have no effect
RenderBlockViewport createRenderObject() => new RenderBlockViewport(); RenderBlockViewport createRenderObject() => new RenderBlockViewport();
bool isLayoutDifferentThan(HomogeneousViewport oldWidget) { bool isLayoutDifferentThan(_ViewportBase oldWidget) {
// changing the builder doesn't imply the layout changed // changing the builder doesn't imply the layout changed
return itemsWrap != oldWidget.itemsWrap || return itemsWrap != oldWidget.itemsWrap ||
itemExtent != oldWidget.itemExtent ||
itemCount != oldWidget.itemCount || itemCount != oldWidget.itemCount ||
direction != oldWidget.direction || direction != oldWidget.direction ||
startOffset != oldWidget.startOffset; startOffset != oldWidget.startOffset;
} }
// all the actual work is done in the element
} }
class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewport> { abstract class _ViewportBaseElement<T extends _ViewportBase> extends RenderObjectElement<T> {
_HomogeneousViewportElement(HomogeneousViewport widget) : super(widget); _ViewportBaseElement(T widget) : super(widget);
List<Element> _children = const <Element>[]; List<Element> _children = const <Element>[];
int _layoutFirstIndex; int _layoutFirstIndex;
...@@ -86,7 +76,7 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor ...@@ -86,7 +76,7 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor
super.unmount(); super.unmount();
} }
void update(HomogeneousViewport newWidget) { void update(T newWidget) {
bool needLayout = newWidget.isLayoutDifferentThan(widget); bool needLayout = newWidget.isLayoutDifferentThan(widget);
super.update(newWidget); super.update(newWidget);
if (needLayout) if (needLayout)
...@@ -99,6 +89,86 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor ...@@ -99,6 +89,86 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor
_updateChildren(); _updateChildren();
} }
void layout(BoxConstraints constraints);
void _updateChildren() {
assert(_layoutFirstIndex != null);
assert(_layoutItemCount != null);
List<Widget> newWidgets;
if (_layoutItemCount > 0)
newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount).map((Widget widget) {
return new RepaintBoundary(key: new ValueKey<Key>(widget.key), child: widget);
}).toList();
else
newWidgets = <Widget>[];
_children = updateChildren(_children, newWidgets);
}
double getTotalExtent(BoxConstraints constraints);
double getMinCrossAxisExtent(BoxConstraints constraints) {
return 0.0;
}
double getMaxCrossAxisExtent(BoxConstraints constraints) {
if (widget.direction == ScrollDirection.vertical)
return constraints.maxWidth;
return constraints.maxHeight;
}
void insertChildRenderObject(RenderObject child, Element slot) {
RenderObject nextSibling = slot?.renderObject;
renderObject.add(child, before: nextSibling);
}
void moveChildRenderObject(RenderObject child, Element slot) {
assert(child.parent == renderObject);
RenderObject nextSibling = slot?.renderObject;
renderObject.move(child, before: nextSibling);
}
void removeChildRenderObject(RenderObject child) {
assert(child.parent == renderObject);
renderObject.remove(child);
}
}
class HomogeneousViewport extends _ViewportBase {
HomogeneousViewport({
Key key,
ListBuilder builder,
bool itemsWrap: false,
int itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
ScrollDirection direction: ScrollDirection.vertical,
double startOffset: 0.0,
Painter overlayPainter,
this.itemExtent // required, must be non-zero
}) : super(
key: key,
builder: builder,
itemsWrap: itemsWrap,
itemCount: itemCount,
direction: direction,
startOffset: startOffset,
overlayPainter: overlayPainter
) {
assert(itemExtent != null);
assert(itemExtent > 0);
}
final double itemExtent;
_HomogeneousViewportElement createElement() => new _HomogeneousViewportElement(this);
bool isLayoutDifferentThan(HomogeneousViewport oldWidget) {
return itemExtent != oldWidget.itemExtent || super.isLayoutDifferentThan(oldWidget);
}
}
class _HomogeneousViewportElement extends _ViewportBaseElement<HomogeneousViewport> {
_HomogeneousViewportElement(HomogeneousViewport widget) : super(widget);
void layout(BoxConstraints constraints) { void layout(BoxConstraints constraints) {
// We enter a build scope (meaning that markNeedsBuild() is forbidden) // We enter a build scope (meaning that markNeedsBuild() is forbidden)
// because we are in the middle of layout and if we allowed people to set // because we are in the middle of layout and if we allowed people to set
...@@ -144,48 +214,85 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor ...@@ -144,48 +214,85 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor
}, building: true); }, building: true);
} }
void _updateChildren() {
assert(_layoutFirstIndex != null);
assert(_layoutItemCount != null);
List<Widget> newWidgets;
if (_layoutItemCount > 0)
newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount).map((Widget widget) {
return new RepaintBoundary(key: new ValueKey<Key>(widget.key), child: widget);
}).toList();
else
newWidgets = <Widget>[];
_children = updateChildren(_children, newWidgets);
}
double getTotalExtent(BoxConstraints constraints) { double getTotalExtent(BoxConstraints constraints) {
// constraints is null when called by layout() above // constraints is null when called by layout() above
return widget.itemCount != null ? widget.itemCount * widget.itemExtent : double.INFINITY; return widget.itemCount != null ? widget.itemCount * widget.itemExtent : double.INFINITY;
} }
}
double getMinCrossAxisExtent(BoxConstraints constraints) { class HomogeneousPageViewport extends _ViewportBase {
return 0.0; HomogeneousPageViewport({
} Key key,
ListBuilder builder,
bool itemsWrap: false,
int itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
ScrollDirection direction: ScrollDirection.vertical,
double startOffset: 0.0,
Painter overlayPainter
}) : super(
key: key,
builder: builder,
itemsWrap: itemsWrap,
itemCount: itemCount,
direction: direction,
startOffset: startOffset,
overlayPainter: overlayPainter
);
double getMaxCrossAxisExtent(BoxConstraints constraints) { _HomogeneousPageViewportElement createElement() => new _HomogeneousPageViewportElement(this);
if (widget.direction == ScrollDirection.vertical) }
return constraints.maxWidth;
return constraints.maxHeight;
}
void insertChildRenderObject(RenderObject child, Element slot) { class _HomogeneousPageViewportElement extends _ViewportBaseElement<HomogeneousPageViewport> {
RenderObject nextSibling = slot?.renderObject; _HomogeneousPageViewportElement(HomogeneousPageViewport widget) : super(widget);
renderObject.add(child, before: nextSibling);
}
void moveChildRenderObject(RenderObject child, Element slot) { void layout(BoxConstraints constraints) {
assert(child.parent == renderObject); // We enter a build scope (meaning that markNeedsBuild() is forbidden)
RenderObject nextSibling = slot?.renderObject; // because we are in the middle of layout and if we allowed people to set
renderObject.move(child, before: nextSibling); // state, they'd expect to have that state reflected immediately, which, if
// we were to try to honour it, would potentially result in assertions
// because you can't normally mutate the render object tree during layout.
// (If there were a way to limit these writes to descendants of this, it'd
// be ok because we are exempt from that assert since we are still actively
// doing our own layout.)
BuildableElement.lockState(() {
double itemExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
double contentExtent = itemExtent * widget.itemCount;
double offset;
if (widget.startOffset <= 0.0 && !widget.itemsWrap) {
_layoutFirstIndex = 0;
offset = -widget.startOffset * itemExtent;
} else {
_layoutFirstIndex = widget.startOffset.floor();
offset = -((widget.startOffset * itemExtent) % itemExtent);
}
if (itemExtent < double.INFINITY) {
_layoutItemCount = ((contentExtent - offset) / contentExtent).ceil();
if (widget.itemCount != null && !widget.itemsWrap)
_layoutItemCount = math.min(_layoutItemCount, widget.itemCount - _layoutFirstIndex);
} else {
assert(() {
'This HomogeneousPageViewport has no specified number of items (meaning it has infinite items), ' +
'and has been placed in an unconstrained environment where all items can be rendered. ' +
'It is most likely that you have placed your HomogeneousPageViewport (which is an internal ' +
'component of several scrollable widgets) inside either another scrolling box, a flexible ' +
'box (Row, Column), or a Stack, without giving it a specific size.';
return widget.itemCount != null;
});
_layoutItemCount = widget.itemCount - _layoutFirstIndex;
}
_layoutItemCount = math.max(0, _layoutItemCount);
_updateChildren();
// Update the renderObject configuration
renderObject.direction = widget.direction == ScrollDirection.vertical ? BlockDirection.vertical : BlockDirection.horizontal;
renderObject.itemExtent = itemExtent;
renderObject.minExtent = itemExtent;
renderObject.startOffset = offset;
renderObject.overlayPainter = widget.overlayPainter;
}, building: true);
} }
void removeChildRenderObject(RenderObject child) { double getTotalExtent(BoxConstraints constraints) {
assert(child.parent == renderObject); double itemExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
renderObject.remove(child); return widget.itemCount != null ? widget.itemCount * itemExtent : double.INFINITY;
} }
} }
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'homogeneous_viewport.dart';
import 'scrollable.dart';
enum ItemsSnapAlignment { item, adjacentItem }
typedef void PageChangedCallback(int newPage);
class PageableList<T> extends Scrollable {
PageableList({
Key key,
initialScrollOffset,
ScrollDirection scrollDirection: ScrollDirection.vertical,
ScrollListener onScrollStart,
ScrollListener onScroll,
ScrollListener onScrollEnd,
SnapOffsetCallback snapOffsetCallback,
double snapAlignmentOffset: 0.0,
this.items,
this.itemBuilder,
this.itemsWrap: false,
this.itemsSnapAlignment: ItemsSnapAlignment.adjacentItem,
this.onPageChanged,
this.scrollableListPainter,
this.duration: const Duration(milliseconds: 200),
this.curve: Curves.ease
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
onScrollStart: onScrollStart,
onScroll: onScroll,
onScrollEnd: onScrollEnd,
snapOffsetCallback: snapOffsetCallback,
snapAlignmentOffset: snapAlignmentOffset
);
final List<T> items;
final ItemBuilder<T> itemBuilder;
final ItemsSnapAlignment itemsSnapAlignment;
final bool itemsWrap;
final PageChangedCallback onPageChanged;
final ScrollableListPainter scrollableListPainter;
final Duration duration;
final Curve curve;
PageableListState<T, PageableList<T>> createState() => new PageableListState<T, PageableList<T>>();
}
class PageableListState<T, Config extends PageableList<T>> extends ScrollableState<Config> {
int get itemCount => config.items?.length ?? 0;
int _previousItemCount;
double pixelToScrollOffset(double value) {
final RenderBox box = context.findRenderObject();
if (box == null || !box.hasSize)
return 0.0;
final double pixelScrollExtent = config.scrollDirection == ScrollDirection.vertical ? box.size.height : box.size.width;
return pixelScrollExtent == 0.0 ? 0.0 : value / pixelScrollExtent;
}
void didUpdateConfig(Config oldConfig) {
super.didUpdateConfig(oldConfig);
bool scrollBehaviorUpdateNeeded = config.scrollDirection != oldConfig.scrollDirection;
if (config.itemsWrap != oldConfig.itemsWrap)
scrollBehaviorUpdateNeeded = true;
if (itemCount != _previousItemCount) {
_previousItemCount = itemCount;
scrollBehaviorUpdateNeeded = true;
}
if (scrollBehaviorUpdateNeeded)
_updateScrollBehavior();
}
void _updateScrollBehavior() {
// if you don't call this from build(), you must call it from setState().
if (config.scrollableListPainter != null)
config.scrollableListPainter.contentExtent = itemCount.toDouble();
scrollTo(scrollBehavior.updateExtents(
contentExtent: itemCount.toDouble(),
containerExtent: 1.0,
scrollOffset: scrollOffset
));
}
void dispatchOnScrollStart() {
super.dispatchOnScrollStart();
config.scrollableListPainter?.scrollStarted();
}
void dispatchOnScroll() {
super.dispatchOnScroll();
if (config.scrollableListPainter != null)
config.scrollableListPainter.scrollOffset = scrollOffset;
}
void dispatchOnScrollEnd() {
super.dispatchOnScrollEnd();
config.scrollableListPainter?.scrollEnded();
}
Widget buildContent(BuildContext context) {
if (itemCount != _previousItemCount) {
_previousItemCount = itemCount;
_updateScrollBehavior();
}
return new HomogeneousPageViewport(
builder: buildItems,
itemsWrap: config.itemsWrap,
itemCount: itemCount,
direction: config.scrollDirection,
startOffset: scrollOffset,
overlayPainter: config.scrollableListPainter
);
}
ScrollBehavior createScrollBehavior() {
return config.itemsWrap ? new UnboundedBehavior() : new OverscrollBehavior();
}
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
bool get snapScrollOffsetChanges => config.itemsSnapAlignment == ItemsSnapAlignment.item;
double snapScrollOffset(double newScrollOffset) {
double previousItemOffset = newScrollOffset.floorToDouble();
double nextItemOffset = newScrollOffset.ceilToDouble();
return (newScrollOffset - previousItemOffset < 0.5 ? previousItemOffset : nextItemOffset)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
}
Future _flingToAdjacentItem(Offset velocity) {
double scrollVelocity = scrollDirectionVelocity(velocity);
double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
.clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5));
return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve)
.then(_notifyPageChanged);
}
Future fling(Offset velocity) {
switch(config.itemsSnapAlignment) {
case ItemsSnapAlignment.adjacentItem:
return _flingToAdjacentItem(velocity);
default:
return super.fling(velocity).then(_notifyPageChanged);
}
}
Future settleScrollOffset() {
return scrollTo(snapScrollOffset(scrollOffset), duration: config.duration, curve: config.curve)
.then(_notifyPageChanged);
}
List<Widget> buildItems(BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>();
int begin = config.itemsWrap ? start : math.max(0, start);
int end = config.itemsWrap ? begin + count : math.min(begin + count, config.items.length);
for (int i = begin; i < end; ++i)
result.add(config.itemBuilder(context, config.items[i % itemCount], i));
assert(result.every((Widget item) => item.key != null));
return result;
}
void _notifyPageChanged(_) {
if (config.onPageChanged != null)
config.onPageChanged(itemCount == 0 ? 0 : scrollOffset.floor() % itemCount);
}
}
...@@ -24,6 +24,11 @@ const double _kMillisecondsPerSecond = 1000.0; ...@@ -24,6 +24,11 @@ const double _kMillisecondsPerSecond = 1000.0;
const double _kMinFlingVelocity = -kMaxFlingVelocity * _kMillisecondsPerSecond; const double _kMinFlingVelocity = -kMaxFlingVelocity * _kMillisecondsPerSecond;
const double _kMaxFlingVelocity = kMaxFlingVelocity * _kMillisecondsPerSecond; const double _kMaxFlingVelocity = kMaxFlingVelocity * _kMillisecondsPerSecond;
final Tolerance kPixelScrollTolerance = new Tolerance(
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
typedef void ScrollListener(double scrollOffset); typedef void ScrollListener(double scrollOffset);
typedef double SnapOffsetCallback(double scrollOffset); typedef double SnapOffsetCallback(double scrollOffset);
...@@ -122,6 +127,18 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -122,6 +127,18 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return new Offset(0.0, scrollOffset); return new Offset(0.0, scrollOffset);
} }
/// Convert a position or velocity measured in terms of pixels to a scrollOffset.
/// Scrollable gesture handlers convert their incoming values with this method.
/// Subclasses that define scrollOffset in units other than pixels must
/// override this method.
double pixelToScrollOffset(double pixelValue) => pixelValue;
double scrollDirectionVelocity(Offset scrollVelocity) {
return config.scrollDirection == ScrollDirection.horizontal
? -scrollVelocity.dx
: -scrollVelocity.dy;
}
ScrollBehavior _scrollBehavior; ScrollBehavior _scrollBehavior;
ScrollBehavior createScrollBehavior(); ScrollBehavior createScrollBehavior();
ScrollBehavior get scrollBehavior { ScrollBehavior get scrollBehavior {
...@@ -180,11 +197,32 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -180,11 +197,32 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
} }
Simulation _createFlingSimulation(double velocity) { Simulation _createFlingSimulation(double velocity) {
return scrollBehavior.createFlingScrollSimulation(scrollOffset, velocity); /*
// Assume that we're rendering at atleast 15 FPS. Stop when we're
// scrolling less than one logical pixel per frame. We're essentially
// normalizing by the devicePixelRatio so that the threshold has the
// same effect independent of the device's pixel density.
double endVelocity = pixelToScrollOffset(15.0 * ui.window.devicePixelRatio);
// Similar to endVelocity. Stop scrolling when we're this close to
// destiniation scroll offset.
double endDistance = pixelToScrollOffset(0.5 * ui.window.devicePixelRatio);
*/
final double endVelocity = pixelToScrollOffset(kPixelScrollTolerance.velocity);
final double endDistance = pixelToScrollOffset(kPixelScrollTolerance.distance);
return scrollBehavior.createFlingScrollSimulation(scrollOffset, velocity)
..tolerance = new Tolerance(velocity: endVelocity.abs(), distance: endDistance);
} }
double snapScrollOffset(double value) {
return config.snapOffsetCallback == null ? value : config.snapOffsetCallback(value);
}
bool get snapScrollOffsetChanges => config.snapOffsetCallback != null;
Simulation _createSnapSimulation(double velocity) { Simulation _createSnapSimulation(double velocity) {
if (velocity == null || config.snapOffsetCallback == null || !_scrollOffsetIsInBounds(scrollOffset)) if (!snapScrollOffsetChanges || velocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
return null; return null;
Simulation simulation = _createFlingSimulation(velocity); Simulation simulation = _createFlingSimulation(velocity);
...@@ -195,14 +233,15 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -195,14 +233,15 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
if (endScrollOffset.isNaN) if (endScrollOffset.isNaN)
return null; return null;
double snappedScrollOffset = config.snapOffsetCallback(endScrollOffset + config.snapAlignmentOffset); double snappedScrollOffset = snapScrollOffset(endScrollOffset + config.snapAlignmentOffset);
double alignedScrollOffset = snappedScrollOffset - config.snapAlignmentOffset; double alignedScrollOffset = snappedScrollOffset - config.snapAlignmentOffset;
if (!_scrollOffsetIsInBounds(alignedScrollOffset)) if (!_scrollOffsetIsInBounds(alignedScrollOffset))
return null; return null;
double snapVelocity = velocity.abs() * (alignedScrollOffset - scrollOffset).sign; double snapVelocity = velocity.abs() * (alignedScrollOffset - scrollOffset).sign;
double endVelocity = pixelToScrollOffset(kPixelScrollTolerance.velocity * velocity.sign);
Simulation toSnapSimulation = Simulation toSnapSimulation =
scrollBehavior.createSnapScrollSimulation(scrollOffset, alignedScrollOffset, snapVelocity); scrollBehavior.createSnapScrollSimulation(scrollOffset, alignedScrollOffset, snapVelocity, endVelocity);
if (toSnapSimulation == null) if (toSnapSimulation == null)
return null; return null;
...@@ -211,10 +250,10 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -211,10 +250,10 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return new ClampedSimulation(toSnapSimulation, xMin: offsetMin, xMax: offsetMax); return new ClampedSimulation(toSnapSimulation, xMin: offsetMin, xMax: offsetMax);
} }
Future _startToEndAnimation({ double velocity }) { Future _startToEndAnimation(Offset scrollVelocity) {
double velocity = scrollDirectionVelocity(scrollVelocity);
_animation.stop(); _animation.stop();
Simulation simulation = Simulation simulation = _createSnapSimulation(velocity) ?? _createFlingSimulation(velocity);
_createSnapSimulation(velocity) ?? _createFlingSimulation(velocity ?? 0.0);
if (simulation == null) if (simulation == null)
return new Future.value(); return new Future.value();
return _animation.animateWith(simulation); return _animation.animateWith(simulation);
...@@ -254,16 +293,16 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -254,16 +293,16 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return scrollTo(newScrollOffset, duration: duration, curve: curve); return scrollTo(newScrollOffset, duration: duration, curve: curve);
} }
Future fling(Offset velocity) { Future fling(Offset scrollVelocity) {
if (velocity != Offset.zero) if (scrollVelocity != Offset.zero)
return _startToEndAnimation(velocity: _scrollVelocity(velocity)); return _startToEndAnimation(scrollVelocity);
if (!_animation.isAnimating) if (!_animation.isAnimating)
return settleScrollOffset(); return settleScrollOffset();
return new Future.value(); return new Future.value();
} }
Future settleScrollOffset() { Future settleScrollOffset() {
return _startToEndAnimation(); return _startToEndAnimation(Offset.zero);
} }
void dispatchOnScrollStart() { void dispatchOnScrollStart() {
...@@ -282,13 +321,6 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -282,13 +321,6 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
config.onScrollEnd(_scrollOffset); config.onScrollEnd(_scrollOffset);
} }
double _scrollVelocity(ui.Offset velocity) {
double scrollVelocity = config.scrollDirection == ScrollDirection.horizontal
? -velocity.dx
: -velocity.dy;
return scrollVelocity.clamp(_kMinFlingVelocity, _kMaxFlingVelocity) / _kMillisecondsPerSecond;
}
void _handlePointerDown(_) { void _handlePointerDown(_) {
_animation.stop(); _animation.stop();
} }
...@@ -300,11 +332,16 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -300,11 +332,16 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
void _handleDragUpdate(double delta) { void _handleDragUpdate(double delta) {
// We negate the delta here because a positive scroll offset moves the // We negate the delta here because a positive scroll offset moves the
// the content up (or to the left) rather than down (or the right). // the content up (or to the left) rather than down (or the right).
scrollBy(-delta); scrollBy(pixelToScrollOffset(-delta));
} }
Future _handleDragEnd(Offset velocity) { double _toScrollVelocity(double velocity) {
return fling(velocity).then((_) { return pixelToScrollOffset(velocity.clamp(_kMinFlingVelocity, _kMaxFlingVelocity) / _kMillisecondsPerSecond);
}
Future _handleDragEnd(Offset pixelScrollVelocity) {
final Offset scrollVelocity = new Offset(_toScrollVelocity(pixelScrollVelocity.dx), _toScrollVelocity(pixelScrollVelocity.dy));
return fling(scrollVelocity).then((_) {
dispatchOnScrollEnd(); dispatchOnScrollEnd();
}); });
} }
...@@ -690,71 +727,6 @@ class ScrollableListState<T, Config extends ScrollableList<T>> extends Scrollabl ...@@ -690,71 +727,6 @@ class ScrollableListState<T, Config extends ScrollableList<T>> extends Scrollabl
} }
} }
typedef void PageChangedCallback(int newPage);
class PageableList<T> extends ScrollableList<T> {
PageableList({
Key key,
int initialPage,
ScrollDirection scrollDirection: ScrollDirection.horizontal,
ScrollListener onScroll,
List<T> items,
ItemBuilder<T> itemBuilder,
bool itemsWrap: false,
double itemExtent,
this.onPageChanged,
EdgeDims padding,
this.duration: const Duration(milliseconds: 200),
this.curve: Curves.ease
}) : super(
key: key,
initialScrollOffset: initialPage == null ? null : initialPage * itemExtent,
scrollDirection: scrollDirection,
onScroll: onScroll,
items: items,
itemBuilder: itemBuilder,
itemsWrap: itemsWrap,
itemExtent: itemExtent,
padding: padding
);
final Duration duration;
final Curve curve;
final PageChangedCallback onPageChanged;
PageableListState<T> createState() => new PageableListState<T>();
}
class PageableListState<T> extends ScrollableListState<T, PageableList<T>> {
double _snapScrollOffset(double newScrollOffset) {
double scaledScrollOffset = newScrollOffset / config.itemExtent;
double previousScrollOffset = scaledScrollOffset.floor() * config.itemExtent;
double nextScrollOffset = scaledScrollOffset.ceil() * config.itemExtent;
double delta = newScrollOffset - previousScrollOffset;
return (delta < config.itemExtent / 2.0 ? previousScrollOffset : nextScrollOffset)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
}
Future fling(ui.Offset velocity) {
double scrollVelocity = _scrollVelocity(velocity);
double newScrollOffset = _snapScrollOffset(scrollOffset + scrollVelocity.sign * config.itemExtent)
.clamp(_snapScrollOffset(scrollOffset - config.itemExtent / 2.0),
_snapScrollOffset(scrollOffset + config.itemExtent / 2.0));
return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve).then(_notifyPageChanged);
}
int get currentPage => (scrollOffset / config.itemExtent).floor() % itemCount;
void _notifyPageChanged(_) {
if (config.onPageChanged != null)
config.onPageChanged(currentPage);
}
Future settleScrollOffset() {
return scrollTo(_snapScrollOffset(scrollOffset), duration: config.duration, curve: config.curve).then(_notifyPageChanged);
}
}
/// A general scrollable list for a large number of children that might not all /// A general scrollable list for a large number of children that might not all
/// have the same height. Prefer [ScrollableWidgetList] when all the children /// have the same height. Prefer [ScrollableWidgetList] when all the children
/// have the same height because it can use that property to be more efficient. /// have the same height because it can use that property to be more efficient.
......
...@@ -28,6 +28,7 @@ export 'src/widgets/notification_listener.dart'; ...@@ -28,6 +28,7 @@ export 'src/widgets/notification_listener.dart';
export 'src/widgets/overlay.dart'; export 'src/widgets/overlay.dart';
export 'src/widgets/page_storage.dart'; export 'src/widgets/page_storage.dart';
export 'src/widgets/pages.dart'; export 'src/widgets/pages.dart';
export 'src/widgets/pageable_list.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
......
...@@ -27,7 +27,6 @@ Widget buildFrame() { ...@@ -27,7 +27,6 @@ Widget buildFrame() {
items: pages, items: pages,
itemBuilder: buildPage, itemBuilder: buildPage,
itemsWrap: itemsWrap, itemsWrap: itemsWrap,
itemExtent: pageSize.width,
scrollDirection: ScrollDirection.horizontal, scrollDirection: ScrollDirection.horizontal,
onPageChanged: (int page) { currentPage = page; } onPageChanged: (int page) { currentPage = page; }
); );
......
...@@ -57,7 +57,7 @@ Future fling(double velocity) { ...@@ -57,7 +57,7 @@ Future fling(double velocity) {
} }
void main() { void main() {
test('ScrollableList snap scrolling, fling(-800)', () { test('ScrollableList snap scrolling, fling(-0.8)', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
tester.pumpWidget(buildFrame()); tester.pumpWidget(buildFrame());
...@@ -67,7 +67,7 @@ void main() { ...@@ -67,7 +67,7 @@ void main() {
Duration dt = const Duration(seconds: 2); Duration dt = const Duration(seconds: 2);
fling(-800.0); fling(-0.8);
tester.pump(); // Start the scheduler at 0.0 tester.pump(); // Start the scheduler at 0.0
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(200.0, 1.0)); expect(scrollOffset, closeTo(200.0, 1.0));
...@@ -76,7 +76,7 @@ void main() { ...@@ -76,7 +76,7 @@ void main() {
tester.pump(); tester.pump();
expect(scrollOffset, 0.0); expect(scrollOffset, 0.0);
fling(-2000.0); fling(-2.0);
tester.pump(); tester.pump();
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(400.0, 1.0)); expect(scrollOffset, closeTo(400.0, 1.0));
...@@ -85,7 +85,7 @@ void main() { ...@@ -85,7 +85,7 @@ void main() {
tester.pump(); tester.pump();
expect(scrollOffset, 400.0); expect(scrollOffset, 400.0);
fling(800.0); fling(0.8);
tester.pump(); tester.pump();
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(0.0, 1.0)); expect(scrollOffset, closeTo(0.0, 1.0));
...@@ -94,7 +94,7 @@ void main() { ...@@ -94,7 +94,7 @@ void main() {
tester.pump(); tester.pump();
expect(scrollOffset, 800.0); expect(scrollOffset, 800.0);
fling(2000.0); fling(2.0);
tester.pump(); tester.pump();
tester.pump(dt); tester.pump(dt);
expect(scrollOffset, closeTo(200.0, 1.0)); expect(scrollOffset, closeTo(200.0, 1.0));
...@@ -104,7 +104,7 @@ void main() { ...@@ -104,7 +104,7 @@ void main() {
expect(scrollOffset, 800.0); expect(scrollOffset, 800.0);
bool completed = false; bool completed = false;
fling(2000.0).then((_) { fling(2.0).then((_) {
completed = true; completed = true;
expect(scrollOffset, closeTo(200.0, 1.0)); expect(scrollOffset, closeTo(200.0, 1.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