Commit 0d63d6b7 authored by Adam Barth's avatar Adam Barth

Port most of scrollable.dart to fn3

parent e7bc8f57
......@@ -8,7 +8,7 @@ import 'package:sky/rendering.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/basic.dart';
typedef List<Widget> ListBuilder(int startIndex, int count, BuildContext context);
typedef List<Widget> ListBuilder(BuildContext context, int startIndex, int count);
class HomogeneousViewport extends RenderObjectWidget {
HomogeneousViewport({
......@@ -137,7 +137,7 @@ class HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewport
assert(_layoutItemCount != null);
List<Widget> newWidgets;
if (_layoutItemCount > 0)
newWidgets = widget.builder(_layoutFirstIndex, _layoutItemCount, this);
newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount);
else
newWidgets = <Widget>[];
_children = updateChildren(_children, newWidgets);
......
// 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 'dart:sky' as sky;
import 'package:newton/newton.dart';
import 'package:sky/animation.dart';
import 'package:sky/gestures.dart';
import 'package:sky/src/rendering/box.dart';
import 'package:sky/src/rendering/viewport.dart';
import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/gesture_detector.dart';
import 'package:sky/src/fn3/homogeneous_viewport.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
const double _kMillisecondsPerSecond = 1000.0;
const double _kMinFlingVelocity = -kMaxFlingVelocity * _kMillisecondsPerSecond;
const double _kMaxFlingVelocity = kMaxFlingVelocity * _kMillisecondsPerSecond;
typedef void ScrollListener();
/// A base class for scrollable widgets that reacts to user input and generates
/// a scrollOffset.
abstract class Scrollable extends StatefulComponent {
Scrollable({
Key key,
this.initialScrollOffset,
this.scrollDirection: ScrollDirection.vertical
}) : super(key: key) {
assert(scrollDirection == ScrollDirection.vertical ||
scrollDirection == ScrollDirection.horizontal);
}
final double initialScrollOffset;
final ScrollDirection scrollDirection;
}
abstract class ScrollableState<T extends Scrollable> extends ComponentState<T> {
ScrollableState(T config) : super(config);
AnimatedSimulation _toEndAnimation; // See _startToEndAnimation()
ValueAnimation<double> _toOffsetAnimation; // Started by scrollTo()
void initState(BuildContext context) {
if (config.initialScrollOffset is double)
_scrollOffset = config.initialScrollOffset;
_toEndAnimation = new AnimatedSimulation(_setScrollOffset);
_toOffsetAnimation = new ValueAnimation<double>()
..addListener(() {
AnimatedValue<double> offset = _toOffsetAnimation.variable;
_setScrollOffset(offset.value);
});
}
double _scrollOffset = 0.0;
double get scrollOffset => _scrollOffset;
Offset get scrollOffsetVector {
if (config.scrollDirection == ScrollDirection.horizontal)
return new Offset(scrollOffset, 0.0);
return new Offset(0.0, scrollOffset);
}
ScrollBehavior _scrollBehavior;
ScrollBehavior createScrollBehavior();
ScrollBehavior get scrollBehavior {
if (_scrollBehavior == null)
_scrollBehavior = createScrollBehavior();
return _scrollBehavior;
}
GestureDragUpdateCallback _getDragUpdateHandler(ScrollDirection direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
return _handleDragUpdate;
}
GestureDragEndCallback _getDragEndHandler(ScrollDirection direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
return _handleDragEnd;
}
Widget build(BuildContext context) {
return new GestureDetector(
onVerticalDragUpdate: _getDragUpdateHandler(ScrollDirection.vertical),
onVerticalDragEnd: _getDragEndHandler(ScrollDirection.vertical),
onHorizontalDragUpdate: _getDragUpdateHandler(ScrollDirection.horizontal),
onHorizontalDragEnd: _getDragEndHandler(ScrollDirection.horizontal),
child: new Listener(
child: buildContent(context),
onPointerDown: _handlePointerDown
)
);
}
Widget buildContent(BuildContext context);
Future _startToOffsetAnimation(double newScrollOffset, Duration duration, Curve curve) {
_stopAnimations();
_toOffsetAnimation
..variable = new AnimatedValue<double>(scrollOffset,
end: newScrollOffset,
curve: curve
)
..progress = 0.0
..duration = duration;
return _toOffsetAnimation.play();
}
void _stopAnimations() {
if (_toOffsetAnimation.isAnimating)
_toOffsetAnimation.stop();
if (_toEndAnimation.isAnimating)
_toEndAnimation.stop();
}
void _startToEndAnimation({ double velocity: 0.0 }) {
_stopAnimations();
Simulation simulation = scrollBehavior.release(scrollOffset, velocity);
if (simulation != null)
_toEndAnimation.start(simulation);
}
void dispose() {
_stopAnimations();
}
void _setScrollOffset(double newScrollOffset) {
if (_scrollOffset == newScrollOffset)
return;
setState(() {
_scrollOffset = newScrollOffset;
});
if (_listeners.length > 0)
_notifyListeners();
}
Future scrollTo(double newScrollOffset, { Duration duration, Curve curve: ease }) {
if (newScrollOffset == _scrollOffset)
return new Future.value();
if (duration == null) {
_stopAnimations();
_setScrollOffset(newScrollOffset);
return new Future.value();
}
return _startToOffsetAnimation(newScrollOffset, duration, curve);
}
Future scrollBy(double scrollDelta, { Duration duration, Curve curve }) {
double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
return scrollTo(newScrollOffset, duration: duration, curve: curve);
}
void settleScrollOffset() {
_startToEndAnimation();
}
double _scrollVelocity(sky.Offset velocity) {
double scrollVelocity = config.scrollDirection == ScrollDirection.horizontal
? -velocity.dx
: -velocity.dy;
return scrollVelocity.clamp(_kMinFlingVelocity, _kMaxFlingVelocity) / _kMillisecondsPerSecond;
}
void _handlePointerDown(_) {
_stopAnimations();
}
void _handleDragUpdate(double delta) {
// 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).
scrollBy(-delta);
}
void _handleDragEnd(Offset velocity) {
if (velocity != Offset.zero) {
_startToEndAnimation(velocity: _scrollVelocity(velocity));
} else if (!_toEndAnimation.isAnimating && (_toOffsetAnimation == null || !_toOffsetAnimation.isAnimating)) {
settleScrollOffset();
}
}
final List<ScrollListener> _listeners = new List<ScrollListener>();
void addListener(ScrollListener listener) {
_listeners.add(listener);
}
void removeListener(ScrollListener listener) {
_listeners.remove(listener);
}
void _notifyListeners() {
List<ScrollListener> localListeners = new List<ScrollListener>.from(_listeners);
for (ScrollListener listener in localListeners)
listener();
}
}
// TODO(abarth): findScrollableAncestor
// TODO(abarth): ensureWidgetIsVisible
/// A simple scrollable widget that has a single child. Use this component if
/// you are not worried about offscreen widgets consuming resources.
class ScrollableViewport extends Scrollable {
ScrollableViewport({
Key key,
this.child,
double initialScrollOffset,
ScrollDirection scrollDirection: ScrollDirection.vertical
}) : super(
key: key,
scrollDirection: scrollDirection,
initialScrollOffset: initialScrollOffset
);
final Widget child;
ScrollableViewportState createState() => new ScrollableViewportState(this);
}
class ScrollableViewportState extends ScrollableState<ScrollableViewport> {
ScrollableViewportState(ScrollableViewport config) : super(config);
ScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
OverscrollWhenScrollableBehavior get scrollBehavior => super.scrollBehavior;
double _viewportSize = 0.0;
double _childSize = 0.0;
void _handleViewportSizeChanged(Size newSize) {
_viewportSize = config.scrollDirection == ScrollDirection.vertical ? newSize.height : newSize.width;
setState(() {
_updateScrollBehaviour();
});
}
void _handleChildSizeChanged(Size newSize) {
_childSize = config.scrollDirection == ScrollDirection.vertical ? newSize.height : newSize.width;
setState(() {
_updateScrollBehaviour();
});
}
void _updateScrollBehaviour() {
// if you don't call this from build() or syncConstructorArguments(), you must call it from setState().
scrollTo(scrollBehavior.updateExtents(
contentExtent: _childSize,
containerExtent: _viewportSize,
scrollOffset: scrollOffset
));
}
Widget buildContent(BuildContext context) {
return new SizeObserver(
callback: _handleViewportSizeChanged,
child: new Viewport(
scrollOffset: scrollOffsetVector,
scrollDirection: config.scrollDirection,
child: new SizeObserver(
callback: _handleChildSizeChanged,
child: config.child
)
)
);
}
}
/// A mashup of [ScrollableViewport] and [BlockBody]. Useful when you have a small,
/// fixed number of children that you wish to arrange in a block layout and that
/// might exceed the height of its container (and therefore need to scroll).
class Block extends StatelessComponent {
Block(this.children, {
Key key,
this.initialScrollOffset,
this.scrollDirection: ScrollDirection.vertical
}) : super(key: key);
final List<Widget> children;
final double initialScrollOffset;
final ScrollDirection scrollDirection;
BlockDirection get _direction {
if (scrollDirection == ScrollDirection.vertical)
return BlockDirection.vertical;
return BlockDirection.horizontal;
}
Widget build(BuildContext context) {
return new ScrollableViewport(
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
child: new BlockBody(children, direction: _direction)
);
}
}
/// An optimized scrollable widget for a large number of children that are all
/// the same size (extent) in the scrollDirection. For example for
/// 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 ScrollableWidgetList extends Scrollable {
ScrollableWidgetList({
Key key,
double initialScrollOffset,
ScrollDirection scrollDirection: ScrollDirection.vertical,
this.itemsWrap: false,
this.itemExtent,
this.padding
}) : super(key: key, initialScrollOffset: initialScrollOffset, scrollDirection: scrollDirection) {
assert(itemExtent != null);
}
EdgeDims padding;
bool itemsWrap;
double itemExtent;
Size containerSize = Size.zero;
}
abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends ScrollableState<T> {
ScrollableWidgetListState(T config) : super(config);
/// Subclasses must implement `get itemCount` to tell ScrollableWidgetList
/// how many items there are in the list.
int get itemCount;
int _previousItemCount;
void didUpdateConfig(T oldConfig) {
super.didUpdateConfig(oldConfig);
bool scrollBehaviorUpdateNeeded =
config.padding != oldConfig.padding ||
config.itemExtent != oldConfig.itemExtent ||
config.scrollDirection != oldConfig.scrollDirection;
if (config.itemsWrap != oldConfig.itemsWrap) {
_scrollBehavior = null;
scrollBehaviorUpdateNeeded = true;
}
if (itemCount != _previousItemCount) {
scrollBehaviorUpdateNeeded = true;
_previousItemCount = itemCount;
}
if (scrollBehaviorUpdateNeeded)
_updateScrollBehavior();
}
ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
double get _containerExtent {
return config.scrollDirection == ScrollDirection.vertical
? config.containerSize.height
: config.containerSize.width;
}
void _handleSizeChanged(Size newSize) {
setState(() {
config.containerSize = newSize;
_updateScrollBehavior();
});
}
double get _leadingPadding {
EdgeDims padding = config.padding;
if (config.scrollDirection == ScrollDirection.vertical)
return padding != null ? padding.top : 0.0;
return padding != null ? padding.left : -.0;
}
double get _trailingPadding {
EdgeDims padding = config.padding;
if (config.scrollDirection == ScrollDirection.vertical)
return padding != null ? padding.bottom : 0.0;
return padding != null ? padding.right : 0.0;
}
EdgeDims get _crossAxisPadding {
EdgeDims padding = config.padding;
if (padding == null)
return null;
if (config.scrollDirection == ScrollDirection.vertical)
return new EdgeDims.only(left: padding.left, right: padding.right);
return new EdgeDims.only(top: padding.top, bottom: padding.bottom);
}
void _updateScrollBehavior() {
// if you don't call this from build() or syncConstructorArguments(), you must call it from setState().
double contentExtent = config.itemExtent * itemCount;
if (config.padding != null)
contentExtent += _leadingPadding + _trailingPadding;
scrollTo(scrollBehavior.updateExtents(
contentExtent: contentExtent,
containerExtent: _containerExtent,
scrollOffset: scrollOffset
));
}
Widget buildContent(BuildContext context) {
if (itemCount != _previousItemCount) {
_previousItemCount = itemCount;
_updateScrollBehavior();
}
return new SizeObserver(
callback: _handleSizeChanged,
child: new Container(
padding: _crossAxisPadding,
child: new HomogeneousViewport(
builder: _buildItems,
itemsWrap: config.itemsWrap,
itemExtent: config.itemExtent,
itemCount: itemCount,
direction: config.scrollDirection,
startOffset: scrollOffset - _leadingPadding
)
)
);
}
List<Widget> _buildItems(BuildContext context, int start, int count) {
List<Widget> result = buildItems(context, start, count);
assert(result.every((item) => item.key != null));
return result;
}
List<Widget> buildItems(BuildContext context, int start, int count);
}
typedef Widget ItemBuilder<T>(BuildContext context, T item);
/// A wrapper around [ScrollableWidgetList] that helps you translate a list of
/// model objects into a scrollable list of widgets. Assumes all the widgets
/// have the same height.
class ScrollableList<T> extends ScrollableWidgetList {
ScrollableList({
Key key,
double initialScrollOffset,
ScrollDirection scrollDirection: ScrollDirection.vertical,
this.items,
this.itemBuilder,
itemsWrap: false,
double itemExtent,
EdgeDims padding
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
itemsWrap: itemsWrap,
itemExtent: itemExtent,
padding: padding);
final List<T> items;
final ItemBuilder<T> itemBuilder;
ScrollableListState<T, ScrollableList<T>> createState() => new ScrollableListState<T, ScrollableList<T>>(this);
}
class ScrollableListState<T, Config extends ScrollableList<T>> extends ScrollableWidgetListState<Config> {
ScrollableListState(Config config) : super(config);
ScrollBehavior createScrollBehavior() {
return config.itemsWrap ? new UnboundedBehavior() : super.createScrollBehavior();
}
int get itemCount => config.items.length;
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]));
return result;
}
}
typedef void PageChangedCallback(int newPage);
class PageableList<T> extends ScrollableList<T> {
PageableList({
Key key,
double initialScrollOffset,
ScrollDirection scrollDirection: ScrollDirection.horizontal,
List<T> items,
ItemBuilder<T> itemBuilder,
bool itemsWrap: false,
double itemExtent,
PageChangedCallback this.pageChanged,
EdgeDims padding,
this.duration: const Duration(milliseconds: 200),
this.curve: ease
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
items: items,
itemBuilder: itemBuilder,
itemsWrap: itemsWrap,
itemExtent: itemExtent,
padding: padding
);
Duration duration;
Curve curve;
PageChangedCallback pageChanged;
}
class PageableListState<T> extends ScrollableListState<T, PageableList<T>> {
PageableListState(PageableList<T> config) : super(config);
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);
}
void _handleDragEnd(sky.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));
scrollTo(newScrollOffset, duration: config.duration, curve: config.curve).then(_notifyPageChanged);
}
int get currentPage => (scrollOffset / config.itemExtent).floor() % itemCount;
void _notifyPageChanged(_) {
if (config.pageChanged != null)
config.pageChanged(currentPage);
}
void settleScrollOffset() {
scrollTo(_snapScrollOffset(scrollOffset), duration: config.duration, curve: config.curve).then(_notifyPageChanged);
}
}
// TODO(abarth): ScrollableMixedWidgetList
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3.dart';
import 'package:test/test.dart';
......@@ -9,7 +10,6 @@ void main() {
Key keyA = new GlobalKey();
Key keyB = new GlobalKey();
Key keyC = new GlobalKey();
tester.pumpFrame(
new Stack([
......@@ -34,13 +34,13 @@ void main() {
])
);
expect(tester.findElementByKey(keyA).renderObject.localToGlobal(const Point(0.0, 0.0)),
expect((tester.findElementByKey(keyA).renderObject as RenderBox).localToGlobal(const Point(0.0, 0.0)),
equals(const Point(100.0, 100.0)));
expect(tester.findElementByKey(keyB).renderObject.localToGlobal(const Point(0.0, 0.0)),
expect((tester.findElementByKey(keyB).renderObject as RenderBox).localToGlobal(const Point(0.0, 0.0)),
equals(const Point(100.0, 200.0)));
expect(tester.findElementByKey(keyB).renderObject.globalToLocal(const Point(110.0, 205.0)),
expect((tester.findElementByKey(keyB).renderObject as RenderBox).globalToLocal(const Point(110.0, 205.0)),
equals(const Point(10.0, 5.0)));
});
}
......@@ -33,7 +33,7 @@ void main() {
Widget builder() {
return new TestComponent(new HomogeneousViewport(
builder: (int start, int count, BuildContext context) {
builder: (BuildContext context, int start, int count) {
List<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index);
......@@ -52,18 +52,19 @@ void main() {
tester.pumpFrame(builder());
TestComponentState testComponent = tester.findElement((element) => element.widget is TestComponent).state;
StatefulComponentElement testComponent = tester.findElement((element) => element.widget is TestComponent);
TestComponentState testComponentState = testComponent.state;
expect(callbackTracker, equals([0, 1, 2, 3, 4, 5]));
callbackTracker.clear();
testComponent.go(false);
testComponentState.go(false);
tester.pumpFrameWithoutChange();
expect(callbackTracker, equals([]));
callbackTracker.clear();
testComponent.go(true);
testComponentState.go(true);
tester.pumpFrameWithoutChange();
expect(callbackTracker, equals([0, 1, 2, 3, 4, 5]));
......@@ -80,7 +81,7 @@ void main() {
double offset = 300.0;
ListBuilder itemBuilder = (int start, int count, BuildContext context) {
ListBuilder itemBuilder = (BuildContext context, int start, int count) {
List<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index);
......@@ -130,7 +131,7 @@ void main() {
double offset = 300.0;
ListBuilder itemBuilder = (int start, int count, BuildContext context) {
ListBuilder itemBuilder = (BuildContext context, int start, int count) {
List<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index);
......
import 'package:quiver/testing/async.dart';
import 'package:sky/widgets.dart';
import 'package:test/test.dart';
......
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