Commit 3b3d5983 authored by Hans Muller's avatar Hans Muller

Merge pull request #820 from HansMuller/pageable_list

Revised PageableList et al

An itemExtent-computing SizeObserver is no longer needed to use PageableList. The PageableList just uses its own size as the itemExtent.

Added the itemsSnapAlignment property to PageableList which enables snapping scrolls to an adjacent item (the default), or any item boundary no not at all.

PageableList scrollOffsets now vary from 0.0 to itemCount instead of 0.0 to itemExtent * itemCount. Using logical coordinates instead of pixel coordinates means that the scroll position is insensitive to changes in the PageablList's size.

Added HomogenousPageViewport which is used by PageableList. HomogenousPageViewport scrollOffsets are defined as for PageableList.

Factored the (substantial) common parts of HomogenousViewport HomogenousPageViewport into a file private _ViewportBase class.

Removed PageableWidgetList. PageableList now just extends Scrollable. Moved PageableList into its own file.

Removed the pixel dependencies from ScrollBehavior. ScrollBehavior.createFlingSimulation() no longer sets the simulation's tolerance. The caller must do this instead.

Scrollable now uses pixelToScrollOffset() to convert from input gesture positions and velocities to scrollOffsets.

Fixes #710
parents 4f22f3ec c1d42a2f
...@@ -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