Commit 7782a115 authored by Hans Muller's avatar Hans Muller

Adds PageableList, other scrolling related changes and fixes

- PageableList extends ScrollableList
One fixed width or height item is visible and centered at a
time. Fling and drag gestures scroll to the next/previous item.

- Scrollable.scrollTo(), Scrollable.scrollBy(), ensureWidgetIsVisible() API changed
The named animation parameter for these methods was replaced by
duration and curve. All of the methods now return a Future. The Future
completes when the scroll does.

This change eliminates the need for Scrollable to temporarily take ownership
of a ValueAnimation object (see #645).

- Using Future.then() instead of an AnimationPerformance status listener
In ensure_visible.dart _handleTap() uses ensureWidgetIsVisible() to
center the card roughly as before and then. When the implicit scroll
animation is complete, it changes the centered card's label font. The
change is made when the Future returned by ensureWidgetIsVisible()
completes.

- FixedHeightScrollable's itemHeight parameter is now itemExtent
If scrollDirection is ScrollDirection.vertical (the default) then itemExtent should
be the height of each item; otherwise it should be the width of each item.

Replaced _velocityForFlingGesture() in scrollable.dart with Scrollable._eventVelocity()
The original version clamped pixels/ms against pixels/sec constants. The new version
also deals with scrollDirection.

- Plumbed scrollDirection though FixedHeightScrollable and ScrollableList

Both classes should now support horizontal scrolling.
parent 7786211c
...@@ -177,7 +177,7 @@ class DemoList extends Component { ...@@ -177,7 +177,7 @@ class DemoList extends Component {
Widget build() { Widget build() {
return new ScrollableList<SkyDemo>( return new ScrollableList<SkyDemo>(
items: demos, items: demos,
itemHeight: kCardHeight, itemExtent: kCardHeight,
itemBuilder: buildDemo, itemBuilder: buildDemo,
padding: kListPadding padding: kListPadding
); );
......
...@@ -19,7 +19,7 @@ class FitnessItemList extends Component { ...@@ -19,7 +19,7 @@ class FitnessItemList extends Component {
child: new ScrollableList<FitnessItem>( child: new ScrollableList<FitnessItem>(
padding: const EdgeDims.all(4.0), padding: const EdgeDims.all(4.0),
items: items, items: items,
itemHeight: kFitnessItemHeight, itemExtent: kFitnessItemHeight,
itemBuilder: (item) => item.toRow(onDismissed: onDismissed) itemBuilder: (item) => item.toRow(onDismissed: onDismissed)
) )
); );
......
...@@ -14,7 +14,7 @@ class Stocklist extends Component { ...@@ -14,7 +14,7 @@ class Stocklist extends Component {
type: MaterialType.canvas, type: MaterialType.canvas,
child: new ScrollableList<Stock>( child: new ScrollableList<Stock>(
items: stocks, items: stocks,
itemHeight: StockRow.kHeight, itemExtent: StockRow.kHeight,
itemBuilder: (stock) => new StockRow(stock: stock) itemBuilder: (stock) => new StockRow(stock: stock)
) )
); );
......
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:sky/animation/animated_value.dart';
import 'package:sky/animation/animation_performance.dart';
import 'package:sky/animation/curves.dart';
import 'package:sky/base/lerp.dart'; import 'package:sky/base/lerp.dart';
import 'package:sky/theme/colors.dart' as colors; import 'package:sky/theme/colors.dart' as colors;
import 'package:sky/widgets.dart'; import 'package:sky/widgets.dart';
...@@ -23,10 +20,12 @@ class EnsureVisibleApp extends App { ...@@ -23,10 +20,12 @@ class EnsureVisibleApp extends App {
static const TextStyle cardLabelStyle = static const TextStyle cardLabelStyle =
const TextStyle(color: colors.white, fontSize: 18.0, fontWeight: bold); const TextStyle(color: colors.white, fontSize: 18.0, fontWeight: bold);
static const TextStyle selectedCardLabelStyle =
const TextStyle(color: white, fontSize: 24.0, fontWeight: bold);
List<CardModel> cardModels; List<CardModel> cardModels;
BlockViewportLayoutState layoutState = new BlockViewportLayoutState(); BlockViewportLayoutState layoutState = new BlockViewportLayoutState();
ScrollListener scrollListener; CardModel selectedCardModel;
ValueAnimation<double> scrollAnimation;
void initState() { void initState() {
List<double> cardHeights = <double>[ List<double> cardHeights = <double>[
...@@ -39,15 +38,15 @@ class EnsureVisibleApp extends App { ...@@ -39,15 +38,15 @@ class EnsureVisibleApp extends App {
return new CardModel(i, cardHeights[i], color); return new CardModel(i, cardHeights[i], color);
}); });
scrollAnimation = new ValueAnimation<double>()
..duration = const Duration(milliseconds: 200)
..variable = new AnimatedValue<double>(0.0, curve: ease);
super.initState(); super.initState();
} }
EventDisposition handleTap(Widget target) { EventDisposition handleTap(Widget card, CardModel cardModel) {
ensureWidgetIsVisible(target, animation: scrollAnimation); ensureWidgetIsVisible(card, duration: const Duration(milliseconds: 200))
.then((_) {
setState(() { selectedCardModel = cardModel; });
});
return EventDisposition.processed; return EventDisposition.processed;
} }
...@@ -55,17 +54,18 @@ class EnsureVisibleApp extends App { ...@@ -55,17 +54,18 @@ class EnsureVisibleApp extends App {
if (index >= cardModels.length) if (index >= cardModels.length)
return null; return null;
CardModel cardModel = cardModels[index]; CardModel cardModel = cardModels[index];
TextStyle style = (cardModel == selectedCardModel) ? selectedCardLabelStyle : cardLabelStyle;
Widget card = new Card( Widget card = new Card(
color: cardModel.color, color: cardModel.color,
child: new Container( child: new Container(
height: cardModel.height, height: cardModel.height,
padding: const EdgeDims.all(8.0), padding: const EdgeDims.all(8.0),
child: new Center(child: new Text(cardModel.label, style: cardLabelStyle)) child: new Center(child: new Text(cardModel.label, style: style))
) )
); );
return new Listener( return new Listener(
key: cardModel.key, key: cardModel.key,
onGestureTap: (_) { return handleTap(card); }, onGestureTap: (_) { return handleTap(card, cardModel); },
child: card child: card
); );
} }
......
// 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 'package:sky/base/lerp.dart';
import 'package:sky/painting/text_style.dart';
import 'package:sky/theme/colors.dart';
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/card.dart';
import 'package:sky/widgets/icon.dart';
import 'package:sky/widgets/scrollable.dart';
import 'package:sky/widgets/scaffold.dart';
import 'package:sky/widgets/theme.dart';
import 'package:sky/widgets/tool_bar.dart';
import 'package:sky/widgets/framework.dart';
import 'package:sky/widgets/task_description.dart';
class CardModel {
CardModel(this.value, this.size, this.color);
int value;
Size size;
Color color;
String get label => "Card $value";
Key get key => new Key.fromObjectIdentity(this);
}
class TestApp extends App {
static const TextStyle cardLabelStyle =
const TextStyle(color: white, fontSize: 18.0, fontWeight: bold);
List<CardModel> cardModels;
Size pageSize = new Size(200.0, 200.0);
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]))
.toList();
cardModels = new List.generate(cardSizes.length, (i) {
Color color = lerpColor(Red[300], Blue[900], i / cardSizes.length);
return new CardModel(i, cardSizes[i], color);
});
super.initState();
}
void updatePageSize(Size newSize) {
setState(() {
pageSize = newSize;
});
}
Widget buildCard(CardModel cardModel) {
print("SKY buildCard ${cardModel.label}");
Widget card = new Card(
color: cardModel.color,
child: new Container(
width: cardModel.size.width,
height: cardModel.size.height,
padding: const EdgeDims.all(8.0),
child: new Center(child: new Text(cardModel.label, style: cardLabelStyle))
)
);
return new Container(
key: cardModel.key,
width: pageSize.width,
child: new Center(child: card)
);
}
Widget build() {
Widget list = new PageableList<CardModel>(
items: cardModels,
itemBuilder: buildCard,
scrollDirection: ScrollDirection.horizontal,
itemExtent: pageSize.width
);
return new IconTheme(
data: const IconThemeData(color: IconThemeColor.white),
child: new Theme(
data: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: Blue,
accentColor: RedAccent[200]
),
child: new TaskDescription(
label: 'PageableList',
child: new Scaffold(
toolbar: new ToolBar(center: new Text('PageableList Demo')),
body: new SizeObserver(
callback: updatePageSize,
child: new Container(
child: list,
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50])
)
)
)
)
)
);
}
}
void main() {
runApp(new TestApp());
}
...@@ -767,6 +767,8 @@ abstract class StatefulComponent extends Component { ...@@ -767,6 +767,8 @@ abstract class StatefulComponent extends Component {
return super.syncChild(node, oldNode, slot); return super.syncChild(node, oldNode, slot);
} }
// Calls function fn immediately and then schedules another build
// for this Component.
void setState(void fn()) { void setState(void fn()) {
assert(!_disqualifiedFromEverAppearingAgain); assert(!_disqualifiedFromEverAppearingAgain);
fn(); fn();
......
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:sky' as sky; import 'dart:sky' as sky;
import 'package:newton/newton.dart'; import 'package:newton/newton.dart';
import 'package:sky/animation/animated_simulation.dart'; import 'package:sky/animation/animated_simulation.dart';
import 'package:sky/animation/animation_performance.dart'; import 'package:sky/animation/animation_performance.dart';
import 'package:sky/animation/animated_value.dart';
import 'package:sky/animation/curves.dart';
import 'package:sky/animation/scroll_behavior.dart'; import 'package:sky/animation/scroll_behavior.dart';
import 'package:sky/rendering/box.dart'; import 'package:sky/rendering/box.dart';
import 'package:sky/rendering/viewport.dart'; import 'package:sky/rendering/viewport.dart';
...@@ -19,13 +22,11 @@ import 'package:sky/widgets/framework.dart'; ...@@ -19,13 +22,11 @@ import 'package:sky/widgets/framework.dart';
export 'package:sky/widgets/block_viewport.dart' show BlockViewportLayoutState; export 'package:sky/widgets/block_viewport.dart' show BlockViewportLayoutState;
const double _kMillisecondsPerSecond = 1000.0;
double _velocityForFlingGesture(double eventVelocity) { // The GestureEvent velocity properties are pixels/second, config min,max limits are pixels/ms
// eventVelocity is pixels/second, config min,max limits are pixels/ms const double _kMillisecondsPerSecond = 1000.0;
return eventVelocity.clamp(-config.kMaxFlingVelocity, config.kMaxFlingVelocity) / const double _kMinFlingVelocity = -config.kMaxFlingVelocity * _kMillisecondsPerSecond;
_kMillisecondsPerSecond; const double _kMaxFlingVelocity = config.kMaxFlingVelocity * _kMillisecondsPerSecond;
}
typedef void ScrollListener(); typedef void ScrollListener();
...@@ -48,6 +49,11 @@ abstract class Scrollable extends StatefulComponent { ...@@ -48,6 +49,11 @@ abstract class Scrollable extends StatefulComponent {
void initState() { void initState() {
_toEndAnimation = new AnimatedSimulation(_tickScrollOffset); _toEndAnimation = new AnimatedSimulation(_tickScrollOffset);
_toOffsetAnimation = new ValueAnimation<double>()
..addListener(() {
AnimatedValue<double> offset = _toOffsetAnimation.variable;
scrollTo(offset.value);
});
} }
void syncFields(Scrollable source) { void syncFields(Scrollable source) {
...@@ -86,38 +92,22 @@ abstract class Scrollable extends StatefulComponent { ...@@ -86,38 +92,22 @@ abstract class Scrollable extends StatefulComponent {
); );
} }
void _startToOffsetAnimation(double newScrollOffset, ValueAnimation<double> animation) { Future _startToOffsetAnimation(double newScrollOffset, Duration duration, Curve curve) {
_stopToEndAnimation(); _stopToEndAnimation();
_stopToOffsetAnimation(); _stopToOffsetAnimation();
_toOffsetAnimation
animation.variable ..variable = new AnimatedValue<double>(scrollOffset,
..begin = scrollOffset end: newScrollOffset,
..end = newScrollOffset; curve: curve
)
_toOffsetAnimation = animation
..progress = 0.0 ..progress = 0.0
..addListener(_updateToOffsetAnimation) ..duration = duration;
..addStatusListener(_updateToOffsetAnimationStatus) return _toOffsetAnimation.play();
..play();
}
void _updateToOffsetAnimation() {
scrollTo(_toOffsetAnimation.value);
}
void _updateToOffsetAnimationStatus(AnimationStatus status) {
if (status == AnimationStatus.dismissed || status == AnimationStatus.completed)
_stopToOffsetAnimation();
} }
void _stopToOffsetAnimation() { void _stopToOffsetAnimation() {
if (_toOffsetAnimation != null) { if (_toOffsetAnimation.isAnimating)
_toOffsetAnimation _toOffsetAnimation.stop();
..removeStatusListener(_updateToOffsetAnimationStatus)
..removeListener(_updateToOffsetAnimation)
..stop();
_toOffsetAnimation = null;
}
} }
void _startToEndAnimation({ double velocity: 0.0 }) { void _startToEndAnimation({ double velocity: 0.0 }) {
...@@ -138,27 +128,29 @@ abstract class Scrollable extends StatefulComponent { ...@@ -138,27 +128,29 @@ abstract class Scrollable extends StatefulComponent {
super.didUnmount(); super.didUnmount();
} }
bool scrollTo(double newScrollOffset, { ValueAnimation<double> animation }) { Future scrollTo(double newScrollOffset, { Duration duration, Curve curve: ease }) {
if (newScrollOffset == _scrollOffset) if (newScrollOffset == _scrollOffset)
return false; return new Future.value();
if (animation == null) { Future result;
if (duration == null) {
setState(() { setState(() {
_scrollOffset = newScrollOffset; _scrollOffset = newScrollOffset;
}); });
result = new Future.value();
} else { } else {
_startToOffsetAnimation(newScrollOffset, animation); result = _startToOffsetAnimation(newScrollOffset, duration, curve);
} }
if (_listeners.length > 0) if (_listeners.length > 0)
_notifyListeners(); _notifyListeners();
return true; return result;
} }
bool scrollBy(double scrollDelta) { Future scrollBy(double scrollDelta, { Duration duration, Curve curve }) {
double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta); double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
return scrollTo(newScrollOffset); return scrollTo(newScrollOffset, duration: duration, curve: curve);
} }
void settleScrollOffset() { void settleScrollOffset() {
...@@ -169,6 +161,14 @@ abstract class Scrollable extends StatefulComponent { ...@@ -169,6 +161,14 @@ abstract class Scrollable extends StatefulComponent {
scrollTo(value); scrollTo(value);
} }
// Return the event's velocity in pixels/second.
double _eventVelocity(sky.GestureEvent event) {
double velocity = scrollDirection == ScrollDirection.horizontal
? -event.velocityX
: -event.velocityY;
return velocity.clamp(_kMinFlingVelocity, _kMaxFlingVelocity) / _kMillisecondsPerSecond;
}
EventDisposition _handlePointerDown(_) { EventDisposition _handlePointerDown(_) {
_stopToEndAnimation(); _stopToEndAnimation();
_stopToOffsetAnimation(); _stopToOffsetAnimation();
...@@ -181,10 +181,7 @@ abstract class Scrollable extends StatefulComponent { ...@@ -181,10 +181,7 @@ abstract class Scrollable extends StatefulComponent {
} }
EventDisposition _handleFlingStart(sky.GestureEvent event) { EventDisposition _handleFlingStart(sky.GestureEvent event) {
double eventVelocity = scrollDirection == ScrollDirection.horizontal _startToEndAnimation(velocity: _eventVelocity(event));
? -event.velocityX
: -event.velocityY;
_startToEndAnimation(velocity: _velocityForFlingGesture(eventVelocity));
return EventDisposition.processed; return EventDisposition.processed;
} }
...@@ -232,13 +229,13 @@ Scrollable findScrollableAncestor({ Widget target }) { ...@@ -232,13 +229,13 @@ Scrollable findScrollableAncestor({ Widget target }) {
return ancestor; return ancestor;
} }
bool ensureWidgetIsVisible(Widget target, { ValueAnimation<double> animation }) { Future ensureWidgetIsVisible(Widget target, { Duration duration, Curve curve }) {
assert(target.mounted); assert(target.mounted);
assert(target.renderObject is RenderBox); assert(target.renderObject is RenderBox);
Scrollable scrollable = findScrollableAncestor(target: target); Scrollable scrollable = findScrollableAncestor(target: target);
if (scrollable == null) if (scrollable == null)
return false; return new Future.value();
Size targetSize = (target.renderObject as RenderBox).size; Size targetSize = (target.renderObject as RenderBox).size;
Point targetCenter = target.localToGlobal( Point targetCenter = target.localToGlobal(
...@@ -260,12 +257,10 @@ bool ensureWidgetIsVisible(Widget target, { ValueAnimation<double> animation }) ...@@ -260,12 +257,10 @@ bool ensureWidgetIsVisible(Widget target, { ValueAnimation<double> animation })
double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta) double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
if (scrollOffset != scrollable.scrollOffset) { if (scrollOffset != scrollable.scrollOffset)
scrollable.scrollTo(scrollOffset, animation: animation); return scrollable.scrollTo(scrollOffset, duration: duration, curve: curve);
return true;
}
return false; return new Future.value();
} }
/// A simple scrollable widget that has a single child. Use this component if /// A simple scrollable widget that has a single child. Use this component if
...@@ -346,17 +341,19 @@ class ScrollableBlock extends Component { ...@@ -346,17 +341,19 @@ class ScrollableBlock extends Component {
} }
/// An optimized scrollable widget for a large number of children that are all /// An optimized scrollable widget for a large number of children that are all
/// of the same height. Use this widget when you have a large number of children /// the same size (extent) in the scrollDirection. For example for
/// or when you are concerned about offscreen widgets consuming resources. /// ScrollDirection.vertical itemExtent is the height of each item. Use this
/// widget when you have a large number of children or when you are concerned
// about offscreen widgets consuming resources.
abstract class FixedHeightScrollable extends Scrollable { abstract class FixedHeightScrollable extends Scrollable {
FixedHeightScrollable({ Key key, this.itemHeight, this.padding }) FixedHeightScrollable({ Key key, ScrollDirection scrollDirection, this.itemExtent, this.padding })
: super(key: key) { : super(key: key, scrollDirection: scrollDirection) {
assert(itemHeight != null); assert(itemExtent != null);
} }
EdgeDims padding; EdgeDims padding;
double itemHeight; double itemExtent;
/// Subclasses must implement `get itemCount` to tell FixedHeightScrollable /// Subclasses must implement `get itemCount` to tell FixedHeightScrollable
/// how many items there are in the list. /// how many items there are in the list.
...@@ -364,27 +361,34 @@ abstract class FixedHeightScrollable extends Scrollable { ...@@ -364,27 +361,34 @@ abstract class FixedHeightScrollable extends Scrollable {
int _previousItemCount; int _previousItemCount;
void syncFields(FixedHeightScrollable source) { void syncFields(FixedHeightScrollable source) {
padding = source.padding; if (padding != source.padding || itemExtent != source.itemExtent) {
itemHeight = source.itemHeight; padding = source.padding;
itemExtent = source.itemExtent;
_updateContentsExtent();
}
super.syncFields(source); super.syncFields(source);
} }
ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
OverscrollBehavior get scrollBehavior => super.scrollBehavior; OverscrollBehavior get scrollBehavior => super.scrollBehavior;
double _height; double _containerExtent;
void _handleSizeChanged(Size newSize) { void _handleSizeChanged(Size newSize) {
setState(() { setState(() {
_height = newSize.height; _containerExtent = scrollDirection == ScrollDirection.vertical ? newSize.height : newSize.width;
scrollBehavior.containerSize = _height; scrollBehavior.containerSize = _containerExtent;
}); });
} }
void _updateContentsHeight() { void _updateContentsExtent() {
double contentsHeight = itemHeight * itemCount; double contentsExtent = itemExtent * itemCount;
if (padding != null) if (padding != null) {
contentsHeight += padding.top + padding.bottom; if (scrollDirection == ScrollDirection.vertical)
scrollBehavior.contentsSize = contentsHeight; contentsExtent += padding.top + padding.bottom;
else
contentsExtent += padding.left + padding.right;
}
scrollBehavior.contentsSize = contentsExtent;
} }
void _updateScrollOffset() { void _updateScrollOffset() {
...@@ -392,50 +396,61 @@ abstract class FixedHeightScrollable extends Scrollable { ...@@ -392,50 +396,61 @@ abstract class FixedHeightScrollable extends Scrollable {
settleScrollOffset(); settleScrollOffset();
} }
Offset _toOffset(double value) {
return scrollDirection == ScrollDirection.vertical
? new Offset(0.0, value)
: new Offset(value, 0.0);
}
Widget buildContent() { Widget buildContent() {
if (itemCount != _previousItemCount) { if (itemCount != _previousItemCount) {
_previousItemCount = itemCount; _previousItemCount = itemCount;
_updateContentsHeight(); _updateContentsExtent();
_updateScrollOffset(); _updateScrollOffset();
} }
int itemShowIndex = 0; int itemShowIndex = 0;
int itemShowCount = 0; int itemShowCount = 0;
double offsetY = 0.0; Offset viewportOffset = Offset.zero;
if (_height != null && _height > 0.0) { if (_containerExtent != null && _containerExtent > 0.0) {
if (scrollOffset < 0.0) { if (scrollOffset < 0.0) {
double visibleHeight = _height + scrollOffset; double visibleHeight = _containerExtent + scrollOffset;
itemShowCount = (visibleHeight / itemHeight).round() + 1; itemShowCount = (visibleHeight / itemExtent).round() + 1;
offsetY = scrollOffset; viewportOffset = _toOffset(scrollOffset);
} else { } else {
itemShowCount = (_height / itemHeight).ceil(); itemShowCount = (_containerExtent / itemExtent).ceil();
double alignmentDelta = -scrollOffset % itemHeight; double alignmentDelta = -scrollOffset % itemExtent;
double drawStart; double drawStart;
if (alignmentDelta != 0.0) { if (alignmentDelta != 0.0) {
alignmentDelta -= itemHeight; alignmentDelta -= itemExtent;
itemShowCount += 1; itemShowCount += 1;
drawStart = scrollOffset + alignmentDelta; drawStart = scrollOffset + alignmentDelta;
offsetY = -alignmentDelta; viewportOffset = _toOffset(-alignmentDelta);
} else { } else {
drawStart = scrollOffset; drawStart = scrollOffset;
} }
itemShowIndex = math.max(0, (drawStart / itemHeight).floor()); itemShowIndex = math.max(0, (drawStart / itemExtent).floor());
} }
} }
List<Widget> items = buildItems(itemShowIndex, itemShowCount); List<Widget> items = buildItems(itemShowIndex, itemShowCount);
assert(items.every((item) => item.key != null)); assert(items.every((item) => item.key != null));
BlockDirection blockDirection = scrollDirection == ScrollDirection.vertical
? BlockDirection.vertical
: BlockDirection.horizontal;
// TODO(ianh): Refactor this so that it does the building in the // TODO(ianh): Refactor this so that it does the building in the
// same frame as the size observing, similar to BlockViewport, but // same frame as the size observing, similar to BlockViewport, but
// keeping the fixed-height optimisations. // keeping the fixed-height optimisations.
return new SizeObserver( return new SizeObserver(
callback: _handleSizeChanged, callback: _handleSizeChanged,
child: new Viewport( child: new Viewport(
scrollOffset: new Offset(0.0, offsetY), scrollDirection: scrollDirection,
scrollOffset: viewportOffset,
child: new Container( child: new Container(
padding: padding, padding: padding,
child: new Block(items) child: new Block(items, direction: blockDirection)
) )
) )
); );
...@@ -453,11 +468,12 @@ typedef Widget ItemBuilder<T>(T item); ...@@ -453,11 +468,12 @@ typedef Widget ItemBuilder<T>(T item);
class ScrollableList<T> extends FixedHeightScrollable { class ScrollableList<T> extends FixedHeightScrollable {
ScrollableList({ ScrollableList({
Key key, Key key,
ScrollDirection scrollDirection,
this.items, this.items,
this.itemBuilder, this.itemBuilder,
double itemHeight, double itemExtent,
EdgeDims padding EdgeDims padding
}) : super(key: key, itemHeight: itemHeight, padding: padding); }) : super(key: key, scrollDirection: scrollDirection, itemExtent: itemExtent, padding: padding);
List<T> items; List<T> items;
ItemBuilder<T> itemBuilder; ItemBuilder<T> itemBuilder;
...@@ -479,6 +495,61 @@ class ScrollableList<T> extends FixedHeightScrollable { ...@@ -479,6 +495,61 @@ class ScrollableList<T> extends FixedHeightScrollable {
} }
} }
class PageableList<T> extends ScrollableList<T> {
PageableList({
Key key,
ScrollDirection scrollDirection,
List<T> items,
ItemBuilder<T> itemBuilder,
double itemExtent,
EdgeDims padding,
this.duration: const Duration(milliseconds: 200),
this.curve: ease
}) : super(
key: key,
scrollDirection: scrollDirection,
items: items,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
padding: padding
);
Duration duration;
Curve curve;
void syncFields(PageableList<T> source) {
duration = source.duration;
curve = source.curve;
super.syncFields(source);
}
double _snapScrollOffset(double newScrollOffset) {
double scaledScrollOffset = newScrollOffset / itemExtent;
double previousScrollOffset = scaledScrollOffset.floor() * itemExtent;
double nextScrollOffset = scaledScrollOffset.ceil() * itemExtent;
double delta = newScrollOffset - previousScrollOffset;
return (delta < itemExtent / 2.0 ? previousScrollOffset : nextScrollOffset)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
}
EventDisposition _handlePointerDown(_) {
return EventDisposition.ignored;
}
EventDisposition _handleFlingStart(sky.GestureEvent event) {
double velocity = _eventVelocity(event);
double newScrollOffset = _snapScrollOffset(scrollOffset + velocity.sign * itemExtent)
.clamp(_snapScrollOffset(scrollOffset - itemExtent / 2.0),
_snapScrollOffset(scrollOffset + itemExtent / 2.0));
scrollTo(newScrollOffset, duration: duration, curve: curve);
return EventDisposition.processed;
}
void settleScrollOffset() {
scrollTo(_snapScrollOffset(scrollOffset), duration: duration, curve: curve);
}
}
/// 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 [FixedHeightScrollable] when all the children /// have the same height. Prefer [FixedHeightScrollable] 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.
......
...@@ -408,16 +408,12 @@ class TabBar extends Scrollable { ...@@ -408,16 +408,12 @@ class TabBar extends Scrollable {
Size _tabBarSize; Size _tabBarSize;
List<double> _tabWidths; List<double> _tabWidths;
ValueAnimation<Rect> _indicatorAnimation; ValueAnimation<Rect> _indicatorAnimation;
ValueAnimation<double> _scrollAnimation;
void initState() { void initState() {
super.initState(); super.initState();
_indicatorAnimation = new ValueAnimation<Rect>() _indicatorAnimation = new ValueAnimation<Rect>()
..duration = _kTabBarScroll ..duration = _kTabBarScroll
..variable = new AnimatedRect(null, curve: ease); ..variable = new AnimatedRect(null, curve: ease);
_scrollAnimation = new ValueAnimation<double>()
..duration = _kTabBarScroll
..variable = new AnimatedValue<double>(0.0, curve: ease);
} }
void syncFields(TabBar source) { void syncFields(TabBar source) {
...@@ -473,7 +469,7 @@ class TabBar extends Scrollable { ...@@ -473,7 +469,7 @@ class TabBar extends Scrollable {
if (tabIndex != selectedIndex) { if (tabIndex != selectedIndex) {
if (_tabWidths != null) { if (_tabWidths != null) {
if (isScrollable) if (isScrollable)
scrollTo(_centeredTabScrollOffset(tabIndex), animation: _scrollAnimation); scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll);
_startIndicatorAnimation(selectedIndex, tabIndex); _startIndicatorAnimation(selectedIndex, tabIndex);
} }
if (onChanged != null) if (onChanged != null)
......
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