diff --git a/sky/packages/sky/lib/widgets/scrollable.dart b/sky/packages/sky/lib/widgets/scrollable.dart index 8058b50465b..e823bd9166e 100644 --- a/sky/packages/sky/lib/widgets/scrollable.dart +++ b/sky/packages/sky/lib/widgets/scrollable.dart @@ -542,6 +542,8 @@ class ScrollableList extends ScrollableWidgetList { } } +typedef void PageChangedCallback(int newPage); + class PageableList extends ScrollableList { PageableList({ Key key, @@ -551,6 +553,7 @@ class PageableList extends ScrollableList { ItemBuilder itemBuilder, bool itemsWrap: false, double itemExtent, + PageChangedCallback this.pageChanged, EdgeDims padding, this.duration: const Duration(milliseconds: 200), this.curve: ease @@ -567,10 +570,12 @@ class PageableList extends ScrollableList { Duration duration; Curve curve; + PageChangedCallback pageChanged; void syncConstructorArguments(PageableList source) { duration = source.duration; curve = source.curve; + pageChanged = source.pageChanged; super.syncConstructorArguments(source); } @@ -592,12 +597,19 @@ class PageableList extends ScrollableList { double newScrollOffset = _snapScrollOffset(scrollOffset + velocity.sign * itemExtent) .clamp(_snapScrollOffset(scrollOffset - itemExtent / 2.0), _snapScrollOffset(scrollOffset + itemExtent / 2.0)); - scrollTo(newScrollOffset, duration: duration, curve: curve); + scrollTo(newScrollOffset, duration: duration, curve: curve).then(_notifyPageChanged); return EventDisposition.processed; } + int get currentPage => (scrollOffset / itemExtent).floor(); + + void _notifyPageChanged(_) { + if (pageChanged != null) + pageChanged(currentPage); + } + void settleScrollOffset() { - scrollTo(_snapScrollOffset(scrollOffset), duration: duration, curve: curve); + scrollTo(_snapScrollOffset(scrollOffset), duration: duration, curve: curve).then(_notifyPageChanged); } } diff --git a/sky/unit/pubspec.yaml b/sky/unit/pubspec.yaml index 145524f3a9f..d91cb48ef20 100644 --- a/sky/unit/pubspec.yaml +++ b/sky/unit/pubspec.yaml @@ -3,6 +3,7 @@ dependencies: sky: any sky_tools: any test: any + quiver: any dependency_overrides: material_design_icons: path: ../packages/material_design_icons diff --git a/sky/unit/test/widget/pageable_list_test.dart b/sky/unit/test/widget/pageable_list_test.dart new file mode 100644 index 00000000000..915f1f49cbd --- /dev/null +++ b/sky/unit/test/widget/pageable_list_test.dart @@ -0,0 +1,54 @@ +import 'package:sky/widgets.dart'; +import 'package:test/test.dart'; +import 'package:quiver/testing/async.dart'; + +import 'widget_tester.dart'; + +void main() { + test('Scrolling changes page', () { + WidgetTester tester = new WidgetTester(); + + List pages = [0, 1, 2, 3, 4, 5]; + Size pageSize = new Size(200.0, 200.0); + int currentPage; + + Widget buildPage(int page) { + return new Container( + key: new StringKey(page.toString()), + width: pageSize.width, + height: pageSize.height, + child: new Text(page.toString()) + ); + } + + Widget builder() { + return new Container( + height: pageSize.height, + child: new PageableList( + padding: new EdgeDims.symmetric(horizontal: 10.0), + items: pages, + itemBuilder: buildPage, + scrollDirection: ScrollDirection.horizontal, + itemExtent: pageSize.width, + pageChanged: (int page) { + currentPage = page; + } + ) + ); + } + + tester.pumpFrame(builder); + // TODO(abarth): We shouldn't need to pump a second frame here. + tester.pumpFrame(builder); + + expect(currentPage, isNull); + new FakeAsync().run((async) { + tester.scroll(tester.findText('1'), new Offset(300.0, 0.0)); + // One frame to start the animation, a second to complete it. + tester.pumpFrame(builder); + tester.pumpFrame(builder, 5000.0); + async.elapse(new Duration(seconds: 5)); + expect(currentPage, equals(2)); + }); + }); +} diff --git a/sky/unit/test/widget/widget_tester.dart b/sky/unit/test/widget/widget_tester.dart index 67035b82208..4676e04eb7b 100644 --- a/sky/unit/test/widget/widget_tester.dart +++ b/sky/unit/test/widget/widget_tester.dart @@ -1,6 +1,7 @@ import 'dart:sky' as sky; import 'package:sky/rendering.dart'; import 'package:sky/widgets.dart'; +import 'package:sky/base/scheduler.dart' as scheduler; typedef Widget WidgetBuilder(); @@ -21,6 +22,63 @@ class TestApp extends App { } } +class TestPointerEvent extends sky.PointerEvent { + TestPointerEvent({ + this.type, + this.pointer, + this.kind, + this.x, + this.y, + this.dx, + this.dy, + this.velocityX, + this.velocityY, + this.buttons, + this.down, + this.primary, + this.obscured, + this.pressure, + this.pressureMin, + this.pressureMax, + this.distance, + this.distanceMin, + this.distanceMax, + this.radiusMajor, + this.radiusMinor, + this.radiusMin, + this.radiusMax, + this.orientation, + this.tilt + }); + + // These are all of the PointerEvent members, but not all of Event. + String type; + int pointer; + String kind; + double x; + double y; + double dx; + double dy; + double velocityX; + double velocityY; + int buttons; + bool down; + bool primary; + bool obscured; + double pressure; + double pressureMin; + double pressureMax; + double distance; + double distanceMin; + double distanceMax; + double radiusMajor; + double radiusMinor; + double radiusMin; + double radiusMax; + double orientation; + double tilt; +} + class TestGestureEvent extends sky.GestureEvent { TestGestureEvent({ this.type, @@ -33,6 +91,7 @@ class TestGestureEvent extends sky.GestureEvent { this.velocityY }); + // These are all of the GestureEvent members, but not all of Event. String type; int primaryPointer; double x; @@ -105,14 +164,25 @@ class WidgetTester { dispatchEvent(new TestGestureEvent(type: 'gesturetap'), getCenter(widget)); } + void scroll(Widget widget, Offset offset) { + dispatchEvent(new TestGestureEvent(type: 'gesturescrollstart'), getCenter(widget)); + dispatchEvent(new TestGestureEvent( + type: 'gesturescrollupdate', + dx: offset.dx, + dy: offset.dy), getCenter(widget)); + // pointerup to trigger scroll settling in Scrollable + dispatchEvent(new TestPointerEvent( + type: 'pointerup', down: false, primary: true), getCenter(widget)); + } + void dispatchEvent(sky.Event event, Point position) { HitTestResult result = SkyBinding.instance.hitTest(position); SkyBinding.instance.dispatchEvent(event, result); } - void pumpFrame(WidgetBuilder builder) { + void pumpFrame(WidgetBuilder builder, [double frameTimeMs = 0.0]) { _app.builder = builder; - SkyBinding.instance.beginFrame(0.0); + scheduler.beginFrame(frameTimeMs); } }