Commit c21fcf62 authored by Hans Muller's avatar Hans Muller

Support ScrollableLists that wrap

Adds itemsWrap:bool (default false) to ScrollableList and PageableList. If itemsWrap is true then scrolling past the last item wraps around to the first. Similarly, scrolling before the first item wraps around to the last.

Added abstract ExtentScrollBehavior of ScrollBehavior. Renamed fields called contentsExtents to contentExtent, containerExtents to containerExtent, contentSize to contentExtent, etc.

BoundedBehavior is now a subclass of ExtentScrollBehavior.

Added UnboundedBehavior subclass of ExtentScrollBehvaior; contentExtent and maxScrollOffset are double.INFINITY, minScrollExtent is double.NEGATIVE_INFINITY.
parent d7ed623e
......@@ -26,8 +26,6 @@ class PageableListApp extends App {
void initState() {
List<Size> cardSizes = [
[100.0, 300.0], [300.0, 100.0], [200.0, 400.0], [400.0, 400.0], [300.0, 400.0],
[100.0, 300.0], [300.0, 100.0], [200.0, 400.0], [400.0, 400.0], [300.0, 400.0],
[100.0, 300.0], [300.0, 100.0], [200.0, 400.0], [400.0, 400.0], [300.0, 400.0]
]
.map((args) => new Size(args[0], args[1]))
......@@ -47,15 +45,6 @@ class PageableListApp extends App {
});
}
EventDisposition handleToolbarTap(_) {
setState(() {
scrollDirection = (scrollDirection == ScrollDirection.vertical)
? ScrollDirection.horizontal
: ScrollDirection.vertical;
});
return EventDisposition.processed;
}
Widget buildCard(CardModel cardModel) {
Widget card = new Card(
color: cardModel.color,
......@@ -78,16 +67,88 @@ class PageableListApp extends App {
);
}
Widget build() {
EventDisposition switchScrollDirection() {
setState(() {
scrollDirection = (scrollDirection == ScrollDirection.vertical)
? ScrollDirection.horizontal
: ScrollDirection.vertical;
});
return EventDisposition.processed;
}
bool _drawerShowing = false;
AnimationStatus _drawerStatus = AnimationStatus.dismissed;
void _handleOpenDrawer() {
setState(() {
_drawerShowing = true;
_drawerStatus = AnimationStatus.forward;
});
}
void _handleDrawerDismissed() {
setState(() {
_drawerStatus = AnimationStatus.dismissed;
});
}
Drawer buildDrawer() {
if (_drawerStatus == AnimationStatus.dismissed)
return null;
return new Drawer(
level: 3,
showing: _drawerShowing,
onDismissed: _handleDrawerDismissed,
children: [
new DrawerHeader(child: new Text('Options')),
new DrawerItem(
icon: 'navigation/more_horiz',
selected: scrollDirection == ScrollDirection.horizontal,
child: new Text('Horizontal Layout'),
onPressed: switchScrollDirection
),
new DrawerItem(
icon: 'navigation/more_vert',
selected: scrollDirection == ScrollDirection.vertical,
child: new Text('Vertical Layout'),
onPressed: switchScrollDirection
)
]
);
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(icon: "navigation/menu", onPressed: _handleOpenDrawer),
center: new Text('PageableList'),
right: [
new Text(scrollDirection == ScrollDirection.horizontal ? "horizontal" : "vertical")
]
);
}
Widget buildBody() {
Widget list = new PageableList<CardModel>(
items: cardModels,
itemsWrap: true,
itemBuilder: buildCard,
scrollDirection: scrollDirection,
itemExtent: (scrollDirection == ScrollDirection.vertical)
? pageSize.height
: pageSize.width
);
return new SizeObserver(
callback: updatePageSize,
child: new Container(
child: list,
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50])
)
);
}
Widget build() {
return new IconTheme(
data: const IconThemeData(color: IconThemeColor.white),
child: new Theme(
......@@ -99,17 +160,9 @@ class PageableListApp extends App {
child: new Title(
title: 'PageableList',
child: new Scaffold(
toolbar: new Listener(
onGestureTap: handleToolbarTap,
child: new ToolBar(center: new Text('PageableList: ${scrollDirection}'))
),
body: new SizeObserver(
callback: updatePageSize,
child: new Container(
child: list,
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50])
)
)
drawer: buildDrawer(),
toolbar: buildToolBar(),
body: buildBody()
)
)
)
......
......@@ -7,6 +7,7 @@ import 'dart:math' as math;
import 'package:newton/newton.dart';
const double _kSecondsPerMillisecond = 1000.0;
const double _kScrollDrag = 0.025;
abstract class ScrollBehavior {
Simulation release(double position, double velocity) => null;
......@@ -15,49 +16,74 @@ abstract class ScrollBehavior {
double applyCurve(double scrollOffset, double scrollDelta);
}
class BoundedBehavior extends ScrollBehavior {
BoundedBehavior({ double contentsSize: 0.0, double containerSize: 0.0 })
: _contentsExtents = contentsSize,
_containerExtents = containerSize;
abstract class ExtentScrollBehavior extends ScrollBehavior {
ExtentScrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: _contentExtent = contentExtent, _containerExtent = containerExtent;
double _contentsExtents;
double get contentsExtents => _contentsExtents;
double _contentExtent;
double get contentExtent => _contentExtent;
double _containerExtents;
double get containerExtents => _containerExtents;
double _containerExtent;
double get containerExtent => _containerExtent;
/// Returns the new scrollOffset.
double updateExtents({
double contentsExtents,
double containerExtents,
double contentExtent,
double containerExtent,
double scrollOffset: 0.0
}) {
if (contentsExtents != null)
_contentsExtents = contentsExtents;
if (containerExtents != null)
_containerExtents = containerExtents;
if (contentExtent != null)
_contentExtent = contentExtent;
if (containerExtent != null)
_containerExtent = containerExtent;
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
}
final double minScrollOffset = 0.0;
double get maxScrollOffset => math.max(0.0, _contentsExtents - _containerExtents);
double get minScrollOffset;
double get maxScrollOffset;
}
class BoundedBehavior extends ExtentScrollBehavior {
BoundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
double minScrollOffset = 0.0;
double get maxScrollOffset => math.max(minScrollOffset, minScrollOffset + _contentExtent - _containerExtent);
double applyCurve(double scrollOffset, double scrollDelta) {
return (scrollOffset + scrollDelta).clamp(minScrollOffset, maxScrollOffset);
}
}
class UnboundedBehavior extends ExtentScrollBehavior {
UnboundedBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
Simulation release(double position, double velocity) {
double velocityPerSecond = velocity * 1000.0;
return new BoundedFrictionSimulation(
_kScrollDrag, position, velocityPerSecond, double.NEGATIVE_INFINITY, double.INFINITY
);
}
double get minScrollOffset => double.NEGATIVE_INFINITY;
double get maxScrollOffset => double.INFINITY;
double applyCurve(double scrollOffset, double scrollDelta) {
return scrollOffset + scrollDelta;
}
}
Simulation createDefaultScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
double velocityPerSecond = velocity * _kSecondsPerMillisecond;
SpringDescription spring = new SpringDescription.withDampingRatio(
mass: 1.0, springConstant: 170.0, ratio: 1.1);
double drag = 0.025;
return new ScrollSimulation(position, velocityPerSecond, minScrollOffset, maxScrollOffset, spring, drag);
return new ScrollSimulation(position, velocityPerSecond, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
}
class OverscrollBehavior extends BoundedBehavior {
OverscrollBehavior({ double contentsSize: 0.0, double containerSize: 0.0 })
: super(contentsSize: contentsSize, containerSize: containerSize);
OverscrollBehavior({ double contentExtent: 0.0, double containerExtent: 0.0 })
: super(contentExtent: contentExtent, containerExtent: containerExtent);
Simulation release(double position, double velocity) {
return createDefaultScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
......@@ -81,7 +107,7 @@ class OverscrollBehavior extends BoundedBehavior {
}
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
bool get isScrollable => contentsExtents > containerExtents;
bool get isScrollable => contentExtent > containerExtent;
Simulation release(double position, double velocity) {
if (isScrollable || position < minScrollOffset || position > maxScrollOffset)
......
......@@ -245,7 +245,7 @@ Future ensureWidgetIsVisible(Widget target, { Duration duration, Curve curve })
double scrollOffsetDelta = scrollable.scrollDirection == ScrollDirection.vertical
? targetCenter.y - scrollableCenter.y
: targetCenter.x - scrollableCenter.x;
BoundedBehavior scrollBehavior = scrollable.scrollBehavior;
ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
......@@ -286,8 +286,8 @@ class ScrollableViewport extends Scrollable {
}
void _updateScrollBehaviour() {
scrollTo(scrollBehavior.updateExtents(
contentsExtents: _childSize,
containerExtents: _viewportSize,
contentExtent: _childSize,
containerExtent: _viewportSize,
scrollOffset: scrollOffset));
}
......@@ -338,6 +338,7 @@ class Block extends Component {
/// widget when you have a large number of children or when you are concerned
// about offscreen widgets consuming resources.
abstract class ScrollableWidgetList extends Scrollable {
static const _kEpsilon = .0000001;
ScrollableWidgetList({
Key key,
......@@ -377,7 +378,7 @@ abstract class ScrollableWidgetList extends Scrollable {
}
ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
OverscrollBehavior get scrollBehavior => super.scrollBehavior;
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
double get _containerExtent {
return scrollDirection == ScrollDirection.vertical
......@@ -394,14 +395,14 @@ abstract class ScrollableWidgetList extends Scrollable {
double get _leadingPadding {
if (scrollDirection == ScrollDirection.vertical)
return padding.top;
return padding.left;
return padding != null ? padding.top : 0.0;
return padding != null ? padding.left : -.0;
}
double get _trailingPadding {
if (scrollDirection == ScrollDirection.vertical)
return padding.bottom;
return padding.right;
return padding != null ? padding.bottom : 0.0;
return padding != null ? padding.right : 0.0;
}
EdgeDims get _crossAxisPadding {
......@@ -413,13 +414,13 @@ abstract class ScrollableWidgetList extends Scrollable {
}
void _updateScrollBehavior() {
double contentsExtent = itemExtent * itemCount;
double contentExtent = itemExtent * itemCount;
if (padding != null)
contentsExtent += _leadingPadding + _trailingPadding;
contentExtent += _leadingPadding + _trailingPadding;
scrollTo(scrollBehavior.updateExtents(
contentsExtents: contentsExtent,
containerExtents: _containerExtent,
contentExtent: contentExtent,
containerExtent: _containerExtent,
scrollOffset: scrollOffset));
}
......@@ -435,31 +436,33 @@ abstract class ScrollableWidgetList extends Scrollable {
_updateScrollBehavior();
}
double paddedScrollOffset = scrollOffset;
if (padding != null)
paddedScrollOffset -= _leadingPadding;
double paddedScrollOffset = scrollOffset - _leadingPadding;
int itemShowIndex = 0;
int itemShowCount = 0;
Offset viewportOffset = Offset.zero;
if (_containerExtent != null && _containerExtent > 0.0) {
if (paddedScrollOffset < 0.0) {
double visibleHeight = _containerExtent + paddedScrollOffset;
itemShowCount = (visibleHeight / itemExtent).round() + 1;
if (paddedScrollOffset < scrollBehavior.minScrollOffset) {
// Underscroll
double visibleExtent = _containerExtent + paddedScrollOffset;
itemShowCount = (visibleExtent / itemExtent).round() + 1;
viewportOffset = _toOffset(paddedScrollOffset);
} else {
itemShowCount = (_containerExtent / itemExtent).ceil();
double alignmentDelta = -paddedScrollOffset % itemExtent;
double drawStart;
double alignmentDelta = (-paddedScrollOffset % itemExtent);
double drawStart = paddedScrollOffset;
if (alignmentDelta != 0.0) {
alignmentDelta -= itemExtent;
itemShowCount += 1;
drawStart = paddedScrollOffset + alignmentDelta;
drawStart += alignmentDelta;
viewportOffset = _toOffset(-alignmentDelta);
} else {
drawStart = paddedScrollOffset;
}
itemShowIndex = math.max(0, (drawStart / itemExtent).floor());
if (itemCount > 0) {
// floor(epsilon) = 0, floor(-epsilon) = -1, so:
if (drawStart.abs() < _kEpsilon)
drawStart = 0.0;
itemShowIndex = (drawStart / itemExtent).floor() % itemCount;
}
}
}
......@@ -501,26 +504,34 @@ class ScrollableList<T> extends ScrollableWidgetList {
ScrollDirection scrollDirection: ScrollDirection.vertical,
this.items,
this.itemBuilder,
this.itemsWrap: false,
double itemExtent,
EdgeDims padding
}) : super(key: key, scrollDirection: scrollDirection, itemExtent: itemExtent, padding: padding);
List<T> items;
ItemBuilder<T> itemBuilder;
bool itemsWrap;
void syncConstructorArguments(ScrollableList<T> source) {
items = source.items;
itemBuilder = source.itemBuilder;
itemsWrap = source.itemsWrap;
super.syncConstructorArguments(source);
}
ScrollBehavior createScrollBehavior() {
return itemsWrap ? new UnboundedBehavior() : super.createScrollBehavior();
}
int get itemCount => items.length;
List<Widget> buildItems(int start, int count) {
List<Widget> result = new List<Widget>();
int end = math.min(start + count, items.length);
for (int i = start; i < end; ++i)
result.add(itemBuilder(items[i]));
int begin = itemsWrap ? start : math.max(0, start);
int end = itemsWrap ? begin + count : math.min(begin + count, items.length);
for (int i = begin; i < end; ++i)
result.add(itemBuilder(items[i % itemCount]));
return result;
}
}
......@@ -531,6 +542,7 @@ class PageableList<T> extends ScrollableList<T> {
ScrollDirection scrollDirection: ScrollDirection.horizontal,
List<T> items,
ItemBuilder<T> itemBuilder,
bool itemsWrap: false,
double itemExtent,
EdgeDims padding,
this.duration: const Duration(milliseconds: 200),
......@@ -540,6 +552,7 @@ class PageableList<T> extends ScrollableList<T> {
scrollDirection: scrollDirection,
items: items,
itemBuilder: itemBuilder,
itemsWrap: itemsWrap,
itemExtent: itemExtent,
padding: padding
);
......@@ -600,7 +613,7 @@ class ScrollableMixedWidgetList extends Scrollable {
// changed. Remember as much so that after the new contents
// have been laid out we can adjust the scrollOffset so that
// the last page of content is still visible.
bool _contentsChanged = true;
bool _contentChanged = true;
void initState() {
assert(layoutState != null);
......@@ -620,7 +633,7 @@ class ScrollableMixedWidgetList extends Scrollable {
void syncConstructorArguments(ScrollableMixedWidgetList source) {
builder = source.builder;
if (token != source.token)
_contentsChanged = true;
_contentChanged = true;
token = source.token;
if (layoutState != source.layoutState) {
// Warning: this is unlikely to be what you intended.
......@@ -637,17 +650,17 @@ class ScrollableMixedWidgetList extends Scrollable {
void _handleSizeChanged(Size newSize) {
scrollBy(scrollBehavior.updateExtents(
containerExtents: newSize.height,
containerExtent: newSize.height,
scrollOffset: scrollOffset
));
}
void _handleLayoutChanged() {
double newScrollOffset = scrollBehavior.updateExtents(
contentsExtents: layoutState.didReachLastChild ? layoutState.contentsSize : double.INFINITY,
contentExtent: layoutState.didReachLastChild ? layoutState.contentsSize : double.INFINITY,
scrollOffset: scrollOffset);
if (_contentsChanged) {
_contentsChanged = false;
if (_contentChanged) {
_contentChanged = false;
scrollTo(newScrollOffset);
}
}
......
......@@ -340,13 +340,13 @@ class Tab extends Component {
}
Widget build() {
Widget labelContents;
Widget labelContent;
if (label.icon == null) {
labelContents = _buildLabelText();
labelContent = _buildLabelText();
} else if (label.text == null) {
labelContents = _buildLabelIcon();
labelContent = _buildLabelIcon();
} else {
labelContents = new Flex(
labelContent = new Flex(
<Widget>[
new Container(
child: _buildLabelIcon(),
......@@ -361,7 +361,7 @@ class Tab extends Component {
}
Container centeredLabel = new Container(
child: new Center(child: labelContents),
child: new Center(child: labelContent),
constraints: new BoxConstraints(minWidth: _kMinTabWidth),
padding: _kTabLabelPadding
);
......@@ -371,8 +371,7 @@ class Tab extends Component {
}
class _TabsScrollBehavior extends BoundedBehavior {
_TabsScrollBehavior({ double contentsSize: 0.0, double containerSize: 0.0 })
: super(contentsSize: contentsSize, containerSize: containerSize);
_TabsScrollBehavior();
bool isScrollable = true;
......@@ -460,7 +459,7 @@ class TabBar extends Scrollable {
}
double _centeredTabScrollOffset(int tabIndex) {
double viewportWidth = scrollBehavior.containerExtents;
double viewportWidth = scrollBehavior.containerExtent;
return (_tabRect(tabIndex).left + _tabWidths[tabIndex] / 2.0 - viewportWidth / 2.0)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
}
......@@ -496,8 +495,8 @@ class TabBar extends Scrollable {
_tabBarSize = tabBarSize;
_tabWidths = tabWidths;
scrollBehavior.updateExtents(
containerExtents: _tabBarSize.width,
contentsExtents: _tabWidths.reduce((sum, width) => sum + width));
containerExtent: _tabBarSize.width,
contentExtent: _tabWidths.reduce((sum, width) => sum + width));
});
}
......
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