From c01bb264c7cff35cb00232642d0e75ffe62aaac9 Mon Sep 17 00:00:00 2001 From: Hixie Date: Wed, 26 Aug 2015 16:56:50 -0700 Subject: [PATCH] Remove the inner SizeObserver from ScrollableWidgetList. Adds a HomogeneousViewport class that works like MixedViewport but handles only children that have all the same height. Converts ScrollableWidgetList to use that, so that we don't waste a frame looking at the size of the contents each time we change size. This allows a number of seemingly pointless double-pumps in the tests to be removed. Other changes that were necessary to support the above: - RenderBlock now supports minExtent (think 'min-height' in CSS) - RenderBlock now supports itemExtent (forces the height of each child to be the same, so that the itemExtent passed to the fixed- height scrollables are all authoritative instead of a source of bugs when they don't match) - RenderBlockViewport now supports horizontal scrolling - improved the style of the isInfinite assert in box.dart - fixed the position of a comment in mixed_viewport.dart - added a test - made the logic for how many items to show be more precise --- sky/packages/sky/lib/rendering/block.dart | 70 +++++-- sky/packages/sky/lib/rendering/box.dart | 5 +- sky/packages/sky/lib/widgets.dart | 1 + .../sky/lib/widgets/homogeneous_viewport.dart | 179 ++++++++++++++++++ .../sky/lib/widgets/mixed_viewport.dart | 4 +- sky/packages/sky/lib/widgets/scrollable.dart | 60 ++---- sky/unit/test/widget/date_picker_test.dart | 6 - .../widget/homogeneous_viewport_test.dart | 170 +++++++++++++++++ sky/unit/test/widget/pageable_list_test.dart | 2 - 9 files changed, 422 insertions(+), 75 deletions(-) create mode 100644 sky/packages/sky/lib/widgets/homogeneous_viewport.dart create mode 100644 sky/unit/test/widget/homogeneous_viewport_test.dart diff --git a/sky/packages/sky/lib/rendering/block.dart b/sky/packages/sky/lib/rendering/block.dart index 1279fb26347..bb113f6d0ae 100644 --- a/sky/packages/sky/lib/rendering/block.dart +++ b/sky/packages/sky/lib/rendering/block.dart @@ -23,8 +23,10 @@ abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin RenderBlockBase({ List children, - BlockDirection direction: BlockDirection.vertical - }) : _direction = direction { + BlockDirection direction: BlockDirection.vertical, + double itemExtent, + double minExtent: 0.0 + }) : _direction = direction, _itemExtent = itemExtent, _minExtent = minExtent { addAll(children); } @@ -42,22 +44,42 @@ abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin } } + double _itemExtent; + double get itemExtent => _itemExtent; + void set itemExtent(double value) { + if (value != _itemExtent) { + _itemExtent = value; + markNeedsLayout(); + } + } + + double _minExtent; + double get minExtent => _minExtent; + void set minExtent(double value) { + if (value != _minExtent) { + _minExtent = value; + markNeedsLayout(); + } + } + bool get isVertical => _direction == BlockDirection.vertical; BoxConstraints _getInnerConstraints(BoxConstraints constraints) { if (isVertical) - return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth)); - return new BoxConstraints.tightFor(height: constraints.constrainHeight(constraints.maxHeight)); + return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth), + height: itemExtent); + return new BoxConstraints.tightFor(height: constraints.constrainHeight(constraints.maxHeight), + width: itemExtent); } double get _mainAxisExtent { RenderBox child = lastChild; if (child == null) - return 0.0; + return minExtent; BoxParentData parentData = child.parentData; return isVertical ? - parentData.position.y + child.size.height : - parentData.position.x + child.size.width; + math.max(minExtent, parentData.position.y + child.size.height) : + math.max(minExtent, parentData.position.x + child.size.width); } void performLayout() { @@ -84,8 +106,10 @@ class RenderBlock extends RenderBlockBase { RenderBlock({ List children, - BlockDirection direction: BlockDirection.vertical - }) : super(children: children, direction: direction); + BlockDirection direction: BlockDirection.vertical, + double itemExtent, + double minExtent: 0.0 + }) : super(children: children, direction: direction, itemExtent: itemExtent, minExtent: minExtent); double _getIntrinsicCrossAxis(BoxConstraints constraints, _ChildSizingFunction childSize) { double extent = 0.0; @@ -116,7 +140,7 @@ class RenderBlock extends RenderBlockBase { assert(child.parentData is BlockParentData); child = child.parentData.nextSibling; } - return extent; + return math.max(extent, minExtent); } double getMinIntrinsicWidth(BoxConstraints constraints) { @@ -185,15 +209,17 @@ class RenderBlockViewport extends RenderBlockBase { DimensionCallback totalExtentCallback, DimensionCallback maxCrossAxisDimensionCallback, DimensionCallback minCrossAxisDimensionCallback, + BlockDirection direction: BlockDirection.vertical, + double itemExtent, + double minExtent: 0.0, double startOffset: 0.0, - List children, - BlockDirection direction: BlockDirection.vertical + List children }) : _callback = callback, _totalExtentCallback = totalExtentCallback, _maxCrossAxisDimensionCallback = maxCrossAxisDimensionCallback, _minCrossAxisDimensionCallback = minCrossAxisDimensionCallback, _startOffset = startOffset, - super(children: children, direction: direction); + super(children: children, direction: direction, itemExtent: itemExtent, minExtent: minExtent); bool _inCallback = false; @@ -250,11 +276,10 @@ class RenderBlockViewport extends RenderBlockBase { double _startOffset; double get startOffset => _startOffset; void set startOffset(double value) { - if (value == _startOffset) - return; - _startOffset = value; - if (!_inCallback) + if (value != _startOffset) { + _startOffset = value; markNeedsPaint(); + } } double _getIntrinsicDimension(BoxConstraints constraints, DimensionCallback intrinsicCallback, _Constrainer constrainer) { @@ -283,13 +308,13 @@ class RenderBlockViewport extends RenderBlockBase { double getMinIntrinsicWidth(BoxConstraints constraints) { if (isVertical) return _getIntrinsicDimension(constraints, minCrossAxisDimensionCallback, constraints.constrainWidth); - return constraints.constrainWidth(0.0); + return constraints.constrainWidth(minExtent); } double getMaxIntrinsicWidth(BoxConstraints constraints) { if (isVertical) return _getIntrinsicDimension(constraints, maxCrossAxisDimensionCallback, constraints.constrainWidth); - return _getIntrinsicDimension(constraints, totalExtentCallback, constraints.constrainWidth); + return _getIntrinsicDimension(constraints, totalExtentCallback, new BoxConstraints(minWidth: minExtent).apply(constraints).constrainWidth); } double getMinIntrinsicHeight(BoxConstraints constraints) { @@ -301,7 +326,7 @@ class RenderBlockViewport extends RenderBlockBase { double getMaxIntrinsicHeight(BoxConstraints constraints) { if (!isVertical) return _getIntrinsicDimension(constraints, maxCrossAxisDimensionCallback, constraints.constrainHeight); - return _getIntrinsicDimension(constraints, totalExtentCallback, constraints.constrainHeight); + return _getIntrinsicDimension(constraints, totalExtentCallback, new BoxConstraints(minHeight: minExtent).apply(constraints).constrainHeight); } // We don't override computeDistanceToActualBaseline(), because we @@ -325,7 +350,10 @@ class RenderBlockViewport extends RenderBlockBase { void paint(PaintingContext context, Offset offset) { context.canvas.save(); context.canvas.clipRect(offset & size); - defaultPaint(context, offset.translate(0.0, startOffset)); + if (isVertical) + defaultPaint(context, offset.translate(0.0, startOffset)); + else + defaultPaint(context, offset.translate(startOffset, 0.0)); context.canvas.restore(); } diff --git a/sky/packages/sky/lib/rendering/box.dart b/sky/packages/sky/lib/rendering/box.dart index de18eb53ffd..713dd7f81a4 100644 --- a/sky/packages/sky/lib/rendering/box.dart +++ b/sky/packages/sky/lib/rendering/box.dart @@ -319,7 +319,10 @@ abstract class RenderBox extends RenderObject { bool debugDoesMeetConstraints() { assert(constraints != null); assert(_size != null); - assert(!_size.isInfinite && 'See https://github.com/domokit/sky_engine/blob/master/sky/packages/sky/lib/widgets/sizing.md#user-content-unbounded-constraints' is String); + assert(() { + 'See https://github.com/domokit/sky_engine/blob/master/sky/packages/sky/lib/widgets/sizing.md#user-content-unbounded-constraints'; + return !_size.isInfinite; + }); bool result = constraints.contains(_size); if (!result) print("${this.runtimeType} does not meet its constraints. Constraints: $constraints, size: $_size"); diff --git a/sky/packages/sky/lib/widgets.dart b/sky/packages/sky/lib/widgets.dart index 5f3d2d9a152..07067349f91 100644 --- a/sky/packages/sky/lib/widgets.dart +++ b/sky/packages/sky/lib/widgets.dart @@ -24,6 +24,7 @@ export 'package:sky/widgets/floating_action_button.dart'; export 'package:sky/widgets/focus.dart'; export 'package:sky/widgets/framework.dart'; export 'package:sky/widgets/gesture_detector.dart'; +export 'package:sky/widgets/homogeneous_viewport.dart'; export 'package:sky/widgets/icon.dart'; export 'package:sky/widgets/icon_button.dart'; export 'package:sky/widgets/ink_well.dart'; diff --git a/sky/packages/sky/lib/widgets/homogeneous_viewport.dart b/sky/packages/sky/lib/widgets/homogeneous_viewport.dart new file mode 100644 index 00000000000..5001568cea5 --- /dev/null +++ b/sky/packages/sky/lib/widgets/homogeneous_viewport.dart @@ -0,0 +1,179 @@ +// 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:math' as math; + +import 'package:sky/rendering/block.dart'; +import 'package:sky/rendering/box.dart'; +import 'package:sky/rendering/object.dart'; +import 'package:sky/widgets/framework.dart'; +import 'package:sky/widgets/basic.dart'; + +typedef List ListBuilder(int startIndex, int count); + +class HomogeneousViewport extends RenderObjectWrapper { + HomogeneousViewport({ + Key key, + this.builder, + this.itemExtent, // required + 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.startOffset: 0.0 + }) : super(key: key) { + assert(itemExtent != null); + } + + ListBuilder builder; + double itemExtent; + int itemCount; + ScrollDirection direction; + double startOffset; + + bool _layoutDirty = true; + List _children; + + RenderBlockViewport get renderObject => super.renderObject; + + RenderBlockViewport createNode() { + // we don't pass constructor arguments to the RenderBlockViewport() because until + // we know our children, the constructor arguments we could give have no effect + RenderBlockViewport result = new RenderBlockViewport(); + result.callback = layout; + result.totalExtentCallback = getTotalExtent; + result.minCrossAxisDimensionCallback = getMinCrossAxisDimension; + result.maxCrossAxisDimensionCallback = getMaxCrossAxisDimension; + return result; + } + + void remove() { + renderObject.callback = null; + renderObject.totalExtentCallback = null; + renderObject.minCrossAxisDimensionCallback = null; + renderObject.maxCrossAxisDimensionCallback = null; + super.remove(); + _children.clear(); + _layoutDirty = true; + } + + void walkChildren(WidgetTreeWalker walker) { + if (_children == null) return; + for (Widget child in _children) + walker(child); + } + + void insertChildRenderObject(RenderObjectWrapper child, Widget slot) { + RenderObject nextSibling = slot != null ? slot.renderObject : null; + renderObject.add(child.renderObject, before: nextSibling); + } + + void detachChildRenderObject(RenderObjectWrapper child) { + renderObject.remove(child.renderObject); + } + + bool retainStatefulNodeIfPossible(HomogeneousViewport newNode) { + retainStatefulRenderObjectWrapper(newNode); + if (startOffset != newNode.startOffset) { + _layoutDirty = true; + startOffset = newNode.startOffset; + } + if (itemCount != newNode.itemCount) { + _layoutDirty = true; + itemCount = newNode.itemCount; + } + if (itemExtent != newNode.itemExtent) { + _layoutDirty = true; + itemExtent = newNode.itemExtent; + } + if (direction != newNode.direction) { + _layoutDirty = true; + direction = newNode.direction; + } + if (builder != newNode.builder) { + _layoutDirty = true; + builder = newNode.builder; + } + return true; + } + + // This is called during the regular component build + void syncRenderObject(HomogeneousViewport old) { + super.syncRenderObject(old); + if (_layoutDirty) { + renderObject.markNeedsLayout(); + } else { + assert(old != null); // if old was null, we'd be new, and therefore _layoutDirty would be true + _updateChildren(); + } + } + + int _layoutFirstIndex; + int _layoutItemCount; + + void layout(BoxConstraints constraints) { + LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder(); + try { + double mainAxisExtent = direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth; + double offset; + if (startOffset <= 0.0) { + _layoutFirstIndex = 0; + offset = -startOffset; + } else { + _layoutFirstIndex = startOffset ~/ itemExtent; + offset = -(startOffset % itemExtent); + } + if (mainAxisExtent < double.INFINITY) { + _layoutItemCount = ((mainAxisExtent - offset) / itemExtent).ceil(); + if (itemCount != null) + _layoutItemCount = math.min(_layoutItemCount, itemCount - _layoutFirstIndex); + } else { + assert(() { + 'This HomogeneousViewport 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 HomogeneousViewport (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 itemCount != null; + }); + _layoutItemCount = itemCount - _layoutFirstIndex; + } + _layoutItemCount = math.max(0, _layoutItemCount); + _updateChildren(); + // Update the renderObject configuration + renderObject.direction = direction == ScrollDirection.vertical ? BlockDirection.vertical : BlockDirection.horizontal; + renderObject.itemExtent = itemExtent; + renderObject.minExtent = getTotalExtent(null); + renderObject.startOffset = offset; + } finally { + exitLayoutCallbackBuilder(handle); + } + } + + void _updateChildren() { + assert(_layoutFirstIndex != null); + assert(_layoutItemCount != null); + List newChildren; + if (_layoutItemCount > 0) + newChildren = builder(_layoutFirstIndex, _layoutItemCount); + else + newChildren = []; + syncChildren(newChildren, _children == null ? [] : _children); + _children = newChildren; + } + + double getTotalExtent(BoxConstraints constraints) { + // constraints is null when called by layout() above + return itemCount != null ? itemCount * itemExtent : double.INFINITY; + } + + double getMinCrossAxisDimension(BoxConstraints constraints) { + return 0.0; + } + + double getMaxCrossAxisDimension(BoxConstraints constraints) { + if (direction == ScrollDirection.vertical) + return constraints.maxWidth; + return constraints.maxHeight; + } + +} diff --git a/sky/packages/sky/lib/widgets/mixed_viewport.dart b/sky/packages/sky/lib/widgets/mixed_viewport.dart index bcd836987f2..5bd9dcf7205 100644 --- a/sky/packages/sky/lib/widgets/mixed_viewport.dart +++ b/sky/packages/sky/lib/widgets/mixed_viewport.dart @@ -91,11 +91,11 @@ class MixedViewport extends RenderObjectWrapper { Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>(); - // we don't pass the direction or offset to the render object when we create it, because - // the render object is empty so it will not matter RenderBlockViewport get renderObject => super.renderObject; RenderBlockViewport createNode() { + // we don't pass the direction or offset to the render object when we + // create it, because the render object is empty so it will not matter RenderBlockViewport result = new RenderBlockViewport(); result.callback = layout; result.totalExtentCallback = _noIntrinsicDimensions; diff --git a/sky/packages/sky/lib/widgets/scrollable.dart b/sky/packages/sky/lib/widgets/scrollable.dart index 124b22923f5..c83ba12e279 100644 --- a/sky/packages/sky/lib/widgets/scrollable.dart +++ b/sky/packages/sky/lib/widgets/scrollable.dart @@ -18,6 +18,7 @@ import 'package:sky/rendering/viewport.dart'; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/framework.dart'; import 'package:sky/widgets/gesture_detector.dart'; +import 'package:sky/widgets/homogeneous_viewport.dart'; import 'package:sky/widgets/mixed_viewport.dart'; export 'package:sky/widgets/mixed_viewport.dart' show MixedViewportLayoutState; @@ -421,17 +422,11 @@ abstract class ScrollableWidgetList extends Scrollable { double contentExtent = itemExtent * itemCount; if (padding != null) contentExtent += _leadingPadding + _trailingPadding; - scrollTo(scrollBehavior.updateExtents( contentExtent: contentExtent, containerExtent: _containerExtent, - scrollOffset: scrollOffset)); - } - - Offset _toOffset(double value) { - return scrollDirection == ScrollDirection.vertical - ? new Offset(0.0, value) - : new Offset(value, 0.0); + scrollOffset: scrollOffset + )); } Widget buildContent() { @@ -440,48 +435,27 @@ abstract class ScrollableWidgetList extends Scrollable { _updateScrollBehavior(); } - double paddedScrollOffset = scrollOffset - _leadingPadding; - int itemShowIndex = 0; - int itemShowCount = 0; - Offset viewportOffset = Offset.zero; - - if (_containerExtent != null && _containerExtent > 0.0 && itemCount > 0) { - if (paddedScrollOffset < scrollBehavior.minScrollOffset) { - // Underscroll - double visibleExtent = _containerExtent + paddedScrollOffset; - itemShowCount = (visibleExtent / itemExtent).ceil(); - viewportOffset = _toOffset(paddedScrollOffset); - } else { - itemShowCount = (_containerExtent / itemExtent).ceil() + 1; - itemShowIndex = (paddedScrollOffset / itemExtent).floor(); - viewportOffset = _toOffset(paddedScrollOffset - itemShowIndex * itemExtent); - itemShowIndex %= itemCount; // Wrap index for when itemWrap is true. - } - } - - List items = buildItems(itemShowIndex, itemShowCount); - 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 - // same frame as the size observing, similar to MixedViewport, but - // keeping the fixed-height optimisations. return new SizeObserver( callback: _handleSizeChanged, - child: new Viewport( - scrollDirection: scrollDirection, - scrollOffset: viewportOffset, - child: new Container( - padding: _crossAxisPadding, - child: new BlockBody(items, direction: blockDirection) + child: new Container( + padding: _crossAxisPadding, + child: new HomogeneousViewport( + builder: _buildItems, + itemExtent: itemExtent, + itemCount: itemCount, + direction: scrollDirection, + startOffset: scrollOffset - _leadingPadding ) ) ); } + List _buildItems(int start, int count) { + List result = buildItems(start, count); + assert(result.every((item) => item.key != null)); + return result; + } + List buildItems(int start, int count); } diff --git a/sky/unit/test/widget/date_picker_test.dart b/sky/unit/test/widget/date_picker_test.dart index cb064cf0acd..868f0cfae77 100644 --- a/sky/unit/test/widget/date_picker_test.dart +++ b/sky/unit/test/widget/date_picker_test.dart @@ -22,19 +22,13 @@ void main() { ]); } - tester.pumpFrame(builder); - // TODO(abarth): We shouldn't need to pump a second frame here. tester.pumpFrame(builder); expect(currentValue, isNull); tester.tap(tester.findText('2015')); tester.pumpFrame(builder); - // TODO(jackson): We shouldn't need to pump a second frame here. - tester.pumpFrame(builder); tester.tap(tester.findText('2014')); tester.pumpFrame(builder); - // TODO(jackson): We shouldn't need to pump a second frame here. - tester.pumpFrame(builder); expect(currentValue, equals(new DateTime(2014, 6, 9))); tester.tap(tester.findText('30')); expect(currentValue, equals(new DateTime(2013, 1, 30))); diff --git a/sky/unit/test/widget/homogeneous_viewport_test.dart b/sky/unit/test/widget/homogeneous_viewport_test.dart new file mode 100644 index 00000000000..9db43d17f69 --- /dev/null +++ b/sky/unit/test/widget/homogeneous_viewport_test.dart @@ -0,0 +1,170 @@ +import 'package:sky/widgets.dart'; +import 'package:test/test.dart'; + +import 'widget_tester.dart'; + +class TestComponent extends StatefulComponent { + TestComponent(this.viewport); + HomogeneousViewport viewport; + void syncConstructorArguments(TestComponent source) { + viewport = source.viewport; + } + bool _flag = true; + void go(bool flag) { + setState(() { + _flag = flag; + }); + } + Widget build() { + return _flag ? viewport : new Text('Not Today'); + } +} + +void main() { + test('HomogeneousViewport mount/dismount smoke test', () { + WidgetTester tester = new WidgetTester(); + + List callbackTracker = []; + + // the root view is 800x600 in the test environment + // so if our widget is 100 pixels tall, it should fit exactly 6 times. + + TestComponent testComponent; + Widget builder() { + testComponent = new TestComponent(new HomogeneousViewport( + builder: (int start, int count) { + List result = []; + for (int index = start; index < start + count; index += 1) { + callbackTracker.add(index); + result.add(new Container( + key: new ValueKey(index), + height: 100.0, + child: new Text("$index") + )); + } + return result; + }, + startOffset: 0.0, + itemExtent: 100.0 + )); + return testComponent; + } + + tester.pumpFrame(builder); + + expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); + + callbackTracker.clear(); + testComponent.go(false); + tester.pumpFrameWithoutChange(); + + expect(callbackTracker, equals([])); + + callbackTracker.clear(); + testComponent.go(true); + tester.pumpFrameWithoutChange(); + + expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); + }); + + test('HomogeneousViewport vertical', () { + WidgetTester tester = new WidgetTester(); + + List callbackTracker = []; + + // the root view is 800x600 in the test environment + // so if our widget is 200 pixels tall, it should fit exactly 3 times. + // but if we are offset by 300 pixels, there will be 4, numbered 1-4. + + double offset = 300.0; + + ListBuilder itemBuilder = (int start, int count) { + List result = []; + for (int index = start; index < start + count; index += 1) { + callbackTracker.add(index); + result.add(new Container( + key: new ValueKey(index), + width: 500.0, // this should be ignored + height: 400.0, // should be overridden by itemExtent + child: new Text("$index") + )); + } + return result; + }; + + TestComponent testComponent; + Widget builder() { + testComponent = new TestComponent(new HomogeneousViewport( + builder: itemBuilder, + startOffset: offset, + itemExtent: 200.0 + )); + return testComponent; + } + + tester.pumpFrame(builder); + + expect(callbackTracker, equals([1, 2, 3, 4])); + + callbackTracker.clear(); + + offset = 400.0; // now only 3 should fit, numbered 2-4. + + tester.pumpFrame(builder); + + expect(callbackTracker, equals([2, 3, 4])); + + callbackTracker.clear(); + }); + + test('HomogeneousViewport horizontal', () { + WidgetTester tester = new WidgetTester(); + + List callbackTracker = []; + + // the root view is 800x600 in the test environment + // so if our widget is 200 pixels wide, it should fit exactly 4 times. + // but if we are offset by 300 pixels, there will be 5, numbered 1-5. + + double offset = 300.0; + + ListBuilder itemBuilder = (int start, int count) { + List result = []; + for (int index = start; index < start + count; index += 1) { + callbackTracker.add(index); + result.add(new Container( + key: new ValueKey(index), + width: 400.0, // this should be overridden by itemExtent + height: 500.0, // this should be ignored + child: new Text("$index") + )); + } + return result; + }; + + TestComponent testComponent; + Widget builder() { + testComponent = new TestComponent(new HomogeneousViewport( + builder: itemBuilder, + startOffset: offset, + itemExtent: 200.0, + direction: ScrollDirection.horizontal + )); + return testComponent; + } + + tester.pumpFrame(builder); + + expect(callbackTracker, equals([1, 2, 3, 4, 5])); + + callbackTracker.clear(); + + offset = 400.0; // now only 4 should fit, numbered 2-5. + + tester.pumpFrame(builder); + + expect(callbackTracker, equals([2, 3, 4, 5])); + + callbackTracker.clear(); + }); +} diff --git a/sky/unit/test/widget/pageable_list_test.dart b/sky/unit/test/widget/pageable_list_test.dart index 5b03254f860..c3803d8b26b 100644 --- a/sky/unit/test/widget/pageable_list_test.dart +++ b/sky/unit/test/widget/pageable_list_test.dart @@ -37,8 +37,6 @@ void main() { ); } - tester.pumpFrame(builder); - // TODO(abarth): We shouldn't need to pump a second frame here. tester.pumpFrame(builder); expect(currentPage, isNull);