diff --git a/sky/packages/sky/lib/src/fn3.dart b/sky/packages/sky/lib/src/fn3.dart index f5688e5cc43..d6a14431be6 100644 --- a/sky/packages/sky/lib/src/fn3.dart +++ b/sky/packages/sky/lib/src/fn3.dart @@ -24,11 +24,12 @@ export 'fn3/focus.dart'; export 'fn3/framework.dart'; export 'fn3/gesture_detector.dart'; export 'fn3/homogeneous_viewport.dart'; -export 'fn3/icon_button.dart'; export 'fn3/icon.dart'; +export 'fn3/icon_button.dart'; export 'fn3/ink_well.dart'; -export 'fn3/material_button.dart'; export 'fn3/material.dart'; +export 'fn3/material_button.dart'; +export 'fn3/mixed_viewport.dart'; export 'fn3/navigator.dart'; export 'fn3/popup_menu.dart'; export 'fn3/popup_menu_item.dart'; diff --git a/sky/packages/sky/lib/src/fn3/framework.dart b/sky/packages/sky/lib/src/fn3/framework.dart index aaa56f32d98..9d785e6608c 100644 --- a/sky/packages/sky/lib/src/fn3/framework.dart +++ b/sky/packages/sky/lib/src/fn3/framework.dart @@ -419,6 +419,7 @@ abstract class Element implements BuildContext { /// /// Subclasses of Element that only have one child should use null for /// the slot for that child. + dynamic get slot => _slot; dynamic _slot; /// An integer that is guaranteed to be greater than the parent's, if any. @@ -488,12 +489,12 @@ abstract class Element implements BuildContext { } if (child != null) { if (child.widget == newWidget) { - if (child._slot != newSlot) + if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } if (_canUpdate(child.widget, newWidget)) { - if (child._slot != newSlot) + if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); assert(child.widget == newWidget); @@ -517,7 +518,7 @@ abstract class Element implements BuildContext { assert(widget != null); assert(_parent == null); assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.mounted); - assert(_slot == null); + assert(slot == null); assert(depth == null); _parent = parent; _slot = newSlot; @@ -654,12 +655,12 @@ abstract class BuildableElement extends Element { } try { - _child = updateChild(_child, built, _slot); + _child = updateChild(_child, built, slot); assert(_child != null); } catch (e, stack) { _debugReportException('building $this', e, stack); built = new ErrorWidget(); - _child = updateChild(null, built, _slot); + _child = updateChild(null, built, slot); } } @@ -735,6 +736,7 @@ class StatefulComponentElement extends BuildableElement { StatefulComponentElement(StatefulComponent widget) : _state = widget.createState(), super(widget) { assert(_state._config == widget); + assert(_state._element == null); _state._element = this; _builder = _state.build; } @@ -755,6 +757,7 @@ class StatefulComponentElement extends BuildableElement { void unmount() { super.unmount(); _state.dispose(); + _state._element = null; _state = null; } } @@ -1016,6 +1019,7 @@ abstract class RenderObjectElement extends Element void unmount() { super.unmount(); + assert(!renderObject.attached); widget.didUnmountRenderObject(renderObject); } @@ -1024,10 +1028,10 @@ abstract class RenderObjectElement extends Element } void _updateSlot(dynamic newSlot) { - assert(_slot != newSlot); + assert(slot != newSlot); super._updateSlot(newSlot); - assert(_slot == newSlot); - _ancestorRenderObjectElement.moveChildRenderObject(renderObject, _slot); + assert(slot == newSlot); + _ancestorRenderObjectElement.moveChildRenderObject(renderObject, slot); } void detachRenderObject() { diff --git a/sky/packages/sky/lib/src/fn3/homogeneous_viewport.dart b/sky/packages/sky/lib/src/fn3/homogeneous_viewport.dart index a174ed0083a..f661e34ae09 100644 --- a/sky/packages/sky/lib/src/fn3/homogeneous_viewport.dart +++ b/sky/packages/sky/lib/src/fn3/homogeneous_viewport.dart @@ -30,11 +30,11 @@ class HomogeneousViewport extends RenderObjectWidget { final ScrollDirection direction; final double startOffset; - RenderObjectElement createElement() => new HomogeneousViewportElement(this); + HomogeneousViewportElement createElement() => new HomogeneousViewportElement(this); // we don't pass constructor arguments to the RenderBlockViewport() because until // we know our children, the constructor arguments we could give have no effect - RenderObject createRenderObject() => new RenderBlockViewport(); + RenderBlockViewport createRenderObject() => new RenderBlockViewport(); bool isLayoutDifferentThan(HomogeneousViewport oldWidget) { return itemsWrap != oldWidget.itemsWrap || @@ -163,7 +163,8 @@ class HomogeneousViewportElement extends RenderObjectElement indices); +typedef void InvalidatorAvailableCallback(InvalidatorCallback invalidator); + +enum _ChangeDescription { none, scrolled, resized } + +class MixedViewport extends RenderObjectWidget { + MixedViewport({ + Key key, + this.startOffset, + this.direction: ScrollDirection.vertical, + this.builder, + this.token, + this.onExtentsUpdate, + this.onInvalidatorAvailable + }): super(key: key); + + final double startOffset; + final ScrollDirection direction; + final IndexedBuilder builder; + final Object token; + final ExtentsUpdateCallback onExtentsUpdate; + final InvalidatorAvailableCallback onInvalidatorAvailable; + + MixedViewportElement createElement() => new MixedViewportElement(this); + + // 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 createRenderObject() => new RenderBlockViewport(); + + _ChangeDescription evaluateChangesFrom(MixedViewport oldWidget) { + if (direction != oldWidget.direction || + builder != oldWidget.builder || + token != oldWidget.token) + return _ChangeDescription.resized; + if (startOffset != oldWidget.startOffset) + return _ChangeDescription.scrolled; + return _ChangeDescription.none; + } + + // all the actual work is done in the element +} + +class _ChildKey { + const _ChildKey(this.type, this.key); + factory _ChildKey.fromWidget(Widget widget) => new _ChildKey(widget.runtimeType, widget.key); + final Type type; + final Key key; + bool operator ==(other) => other is _ChildKey && other.type == type && other.key == key; + int get hashCode => 373 * 37 * type.hashCode + key.hashCode; + String toString() => "_ChildKey(type: $type, key: $key)"; +} + +class MixedViewportElement extends RenderObjectElement { + MixedViewportElement(MixedViewport config) : super(config) { + if (config.onInvalidatorAvailable != null) + config.onInvalidatorAvailable(invalidate); + } + + /// _childOffsets contains the offsets of each child from the top of the list + /// up to the last one we've ever created, and the offset of the end of the + /// last one. If there are no children, then the only offset is 0.0. The + /// offset of the end of the last child created (the actual last child, if + /// didReachLastChild is true), is also the distance from the top (left) of + /// the first child to the bottom (right) of the last child created. + List _childOffsets = [0.0]; + + /// Whether childOffsets includes the offset of the last child. + bool _didReachLastChild = false; + + /// The index of the first child whose bottom edge is below the top of the + /// viewport. + int _firstVisibleChildIndex; + + /// The currently visibly children. + Map<_ChildKey, Element> _childrenByKey = new Map<_ChildKey, Element>(); + + /// The child offsets that we've been told are invalid. + final Set _invalidIndices = new Set(); + + /// Returns false if any of the previously-cached offsets have been marked as + /// invalid and need to be updated. + bool get isValid => _invalidIndices.length == 0; + + /// The constraints for which the current offsets are valid. + BoxConstraints _lastLayoutConstraints; + + /// The last value that was sent to onExtentsUpdate. + double _lastReportedExtents; + + RenderBlockViewport get renderObject => super.renderObject; + + /// Notify the BlockViewport that the children at indices have, or might have, + /// changed size. Call this whenever the dimensions of a particular child + /// change, so that the rendering will be updated accordingly. A pointer to + /// this method is provided via the onInvalidatorAvailable callback. + void invalidate(Iterable indices) { + assert(indices.length > 0); + _invalidIndices.addAll(indices); + renderObject.markNeedsLayout(); + } + + /// Forget all the known child offsets. + void _resetCache() { + _didReachLastChild = false; + _childOffsets = [0.0]; + _invalidIndices.clear(); + } + + void visitChildren(ElementVisitor visitor) { + for (Element child in _childrenByKey.values) + visitor(child); + } + + void mount(Element parent, dynamic newSlot) { + super.mount(parent, newSlot); + renderObject.callback = layout; + renderObject.totalExtentCallback = _noIntrinsicExtent; + renderObject.maxCrossAxisExtentCallback = _noIntrinsicExtent; + renderObject.minCrossAxisExtentCallback = _noIntrinsicExtent; + } + + void unmount() { + renderObject.callback = null; + renderObject.totalExtentCallback = null; + renderObject.minCrossAxisExtentCallback = null; + renderObject.maxCrossAxisExtentCallback = null; + super.unmount(); + } + + double _noIntrinsicExtent(BoxConstraints constraints) { + assert(() { + 'MixedViewport does not support returning intrinsic dimensions. ' + + 'Calculating the intrinsic dimensions would require walking the entire child list, ' + + 'which defeats the entire point of having a lazily-built list of children.'; + return false; + }); + return null; + } + + static const Object _omit = const Object(); // used as a slot when it's not yet time to attach the child + + void update(MixedViewport newWidget) { + _ChangeDescription changes = newWidget.evaluateChangesFrom(widget); + super.update(newWidget); + if (changes == _ChangeDescription.resized) + _resetCache(); + if (changes != _ChangeDescription.none || !isValid) { + renderObject.markNeedsLayout(); + } else { + // we just need to redraw our existing widgets as-is + if (_childrenByKey.length > 0) { + assert(_firstVisibleChildIndex >= 0); + assert(renderObject != null); + final int startIndex = _firstVisibleChildIndex; + int lastIndex = startIndex + _childrenByKey.length - 1; + for (int index = startIndex; index <= lastIndex; index += 1) { + final Widget newWidget = _buildWidgetAt(index); + final _ChildKey key = new _ChildKey.fromWidget(newWidget); + final Element oldElement = _childrenByKey[key]; + assert(oldElement != null); + final Element newElement = updateChild(oldElement, newWidget, renderObject.childAfter(oldElement.renderObject)); + assert(newElement != null); + _childrenByKey[key] = newElement; + } + } + } + } + + void layout(BoxConstraints constraints) { + if (constraints != _lastLayoutConstraints) { + _resetCache(); + _lastLayoutConstraints = constraints; + } + BuildableElement.lockState(() { + _doLayout(constraints); + }); + if (widget.onExtentsUpdate != null) { + final double newExtents = _didReachLastChild ? _childOffsets.last : null; + if (newExtents != _lastReportedExtents) { + _lastReportedExtents = newExtents; + widget.onExtentsUpdate(_lastReportedExtents); + } + } + } + + /// Binary search to find the index of the child responsible for rendering a given pixel + int _findIndexForOffsetBeforeOrAt(double offset) { + int left = 0; + int right = _childOffsets.length - 1; + while (right >= left) { + int middle = left + ((right - left) ~/ 2); + if (_childOffsets[middle] < offset) { + left = middle + 1; + } else if (_childOffsets[middle] > offset) { + right = middle - 1; + } else { + return middle; + } + } + return right; + } + + /// Calls the builder. This is for the case where you don't know if you have a child at this index. + Widget _maybeBuildWidgetAt(int index) { + if (widget.builder == null) + return null; + final Widget newWidget = widget.builder(this, index); + assert(newWidget == null || newWidget.key != null); // every widget in a list must have a list-unique key + return newWidget; + } + + /// Calls the builder. This is for the case where you know that you should have a child there. + Widget _buildWidgetAt(int index) { + final Widget newWidget = widget.builder(this, index); + assert(newWidget != null); + assert(newWidget.key != null); // every widget in a list must have a list-unique key + return newWidget; + } + + /// Given an element configuration, inflates the element, updating the existing one if there was one. + /// Returns the resulting element. + Element _inflateOrUpdateWidget(Widget newWidget) { + final _ChildKey key = new _ChildKey.fromWidget(newWidget); + final Element oldElement = _childrenByKey[key]; + final Element newElement = updateChild(oldElement, newWidget, _omit); + assert(newElement != null); + return newElement; + } + + // Build the widget at index. + Element _getElement(int index, BoxConstraints innerConstraints) { + assert(index <= _childOffsets.length - 1); + final Widget newWidget = _buildWidgetAt(index); + final Element newElement = _inflateOrUpdateWidget(newWidget); + return newElement; + } + + // Build the widget at index, handling the case where there is no such widget. + // Update the offset for that widget. + Element _getElementAtLastKnownOffset(int index, BoxConstraints innerConstraints) { + + // Inflate the new widget; if there isn't one, abort early. + assert(index == _childOffsets.length - 1); + final Widget newWidget = _maybeBuildWidgetAt(index); + if (newWidget == null) + return null; + final Element newElement = _inflateOrUpdateWidget(newWidget); + + // Update the offsets based on the newElement's dimensions. + final double newOffset = _getOffset(newElement, innerConstraints); + _childOffsets.add(_childOffsets[index] + newOffset); + + return newElement; + } + + /// Returns the intrinsic size of the given element in the scroll direction + double _getOffset(Element element, BoxConstraints innerConstraints) { + final RenderBox childRenderObject = element.renderObject; + switch (widget.direction) { + case ScrollDirection.vertical: return childRenderObject.getMaxIntrinsicHeight(innerConstraints); + case ScrollDirection.horizontal: return childRenderObject.getMaxIntrinsicWidth(innerConstraints); + case ScrollDirection.both: assert(false); // we don't support ScrollDirection.both, see issue 888 + } + } + + /// This is the core lazy-build algorithm. It builds widgets incrementally + /// from index 0 until it has built enough widgets to cover itself, and + /// discards any widgets that are not displayed. + void _doLayout(BoxConstraints constraints) { + Map<_ChildKey, Element> newChildren = new Map<_ChildKey, Element>(); + Map builtChildren = new Map(); + + // Establish the start and end offsets based on our current constraints. + double extent; + switch (widget.direction) { + case ScrollDirection.vertical: + extent = constraints.maxHeight; + assert(extent < double.INFINITY && + 'There is no point putting a lazily-built vertical MixedViewport inside a box with infinite internal ' + + 'height (e.g. inside something else that scrolls vertically), because it would then just eagerly build ' + + 'all the children. You probably want to put the MixedViewport inside a Container with a fixed height.' is String); + break; + case ScrollDirection.horizontal: + extent = constraints.maxWidth; + assert(extent < double.INFINITY && + 'There is no point putting a lazily-built horizontal MixedViewport inside a box with infinite internal ' + + 'width (e.g. inside something else that scrolls horizontally), because it would then just eagerly build ' + + 'all the children. You probably want to put the MixedViewport inside a Container with a fixed width.' is String); + break; + case ScrollDirection.both: assert(false); // we don't support ScrollDirection.both, see issue 888 + } + final double endOffset = widget.startOffset + extent; + + // Create the constraints that we will use to measure the children. + BoxConstraints innerConstraints; + switch (widget.direction) { + case ScrollDirection.vertical: + innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth()); + break; + case ScrollDirection.horizontal: + innerConstraints = new BoxConstraints.tightFor(height: constraints.constrainHeight()); + break; + case ScrollDirection.both: assert(false); // we don't support ScrollDirection.both, see issue 888 + } + + // Before doing the actual layout, fix the offsets for the widgets whose + // size or type has changed. + if (!isValid) { + assert(_childOffsets.length > 0); + List invalidIndices = _invalidIndices.toList(); + invalidIndices.sort(); + for (int i = 0; i < invalidIndices.length - 1; i += 1) { + + // Determine the indices for this pass. + final int widgetIndex = invalidIndices[i]; + if (widgetIndex >= _childOffsets.length-1) + break; // we don't have that child, so there's nothing to invalidate + int endIndex; + if (i == invalidIndices.length - 1) { + // This is the last invalid index. Update all the remaining entries in _childOffsets. + endIndex = _childOffsets.length - 1; + } else { + endIndex = invalidIndices[i + 1]; + if (endIndex > _childOffsets.length - 1) + endIndex = _childOffsets.length - 1; // no point updating beyond the last offset we know of + } + assert(widgetIndex >= 0); + assert(endIndex < _childOffsets.length); + assert(widgetIndex < endIndex); + + // Inflate the widget or update the existing element, as necessary. + final Element newElement = _getElement(widgetIndex, innerConstraints); + + // Update the offsets based on the newElement's dimensions. + final double newOffset = _getOffset(newElement, innerConstraints); + final double oldOffset = _childOffsets[widgetIndex + 1] - _childOffsets[widgetIndex]; + final double offsetDelta = newOffset - oldOffset; + for (int j = widgetIndex + 1; j <= endIndex; j++) + _childOffsets[j] += offsetDelta; + + // Decide if it's visible. + final _ChildKey key = new _ChildKey.fromWidget(newElement.widget); + final bool isVisible = _childOffsets[widgetIndex] < endOffset && _childOffsets[widgetIndex + 1] >= widget.startOffset; + if (isVisible) { + // Keep it. + newChildren[key] = newElement; + builtChildren[widgetIndex] = newElement; + } else { + // Drop it. + _childrenByKey.remove(key); + updateChild(newElement, null, null); + } + + } + _invalidIndices.clear(); + } + + // Decide what the first child to render should be (startIndex), if any (haveChildren). + int startIndex; + bool haveChildren; + if (endOffset < 0.0) { + // We're so far scrolled up that nothing is visible. + haveChildren = false; + } else if (widget.startOffset <= 0.0) { + startIndex = 0; + // If we're scrolled up past the top, then our first visible widget, if + // any, is the first widget. + if (_childOffsets.length > 1) { + haveChildren = true; + } else { + final Element element = _getElementAtLastKnownOffset(startIndex, innerConstraints); + if (element != null) { + newChildren[new _ChildKey.fromWidget(element.widget)] = element; + builtChildren[startIndex] = element; + haveChildren = true; + } else { + haveChildren = false; + _didReachLastChild = true; + } + } + } else { + // We're at some sane (not higher than the top) scroll offset. + // See if we can already find the offset in our cache. + startIndex = _findIndexForOffsetBeforeOrAt(widget.startOffset); + if (startIndex < _childOffsets.length - 1) { + // We already know of a child that would be visible at this offset. + haveChildren = true; + } else { + // We don't have an offset on the list that is beyond the start offset. + assert(_childOffsets.last <= widget.startOffset); + // Fill the list until this isn't true or until we know that the + // list is complete (and thus we are overscrolled). + while (true) { + // Get the next element and cache its offset. + final Element element = _getElementAtLastKnownOffset(startIndex, innerConstraints); + if (element == null) { + // Reached the end of the list. We are so far overscrolled, there's nothing to show. + _didReachLastChild = true; + haveChildren = false; + break; + } + final _ChildKey key = new _ChildKey.fromWidget(element.widget); + if (_childOffsets.last > widget.startOffset) { + // This element is visible! It must thus be our first visible child. + newChildren[key] = element; + builtChildren[startIndex] = element; + haveChildren = true; + break; + } + // This element is not visible. Drop the inflated element. + // (We've already cached its offset for later use.) + _childrenByKey.remove(key); + updateChild(element, null, null); + startIndex += 1; + assert(startIndex == _childOffsets.length - 1); + } + assert(haveChildren == _childOffsets.last > widget.startOffset); + assert(() { + if (haveChildren) { + // We found a child to render. It's the last one for which we have an + // offset in _childOffsets. + // If we're here, we have at least one child, so our list has + // at least two offsets, the top of the child and the bottom + // of the child. + assert(_childOffsets.length >= 2); + assert(startIndex == _childOffsets.length - 2); + } + return true; + }); + } + } + assert(haveChildren != null); + assert(haveChildren || _didReachLastChild || endOffset < 0.0); + assert(startIndex >= 0); + assert(startIndex < _childOffsets.length); + + // Build the other widgets that are visible. + int index = startIndex; + if (haveChildren) { + // Update the renderObject configuration + switch (widget.direction) { + case ScrollDirection.vertical: + renderObject.direction = BlockDirection.vertical; + break; + case ScrollDirection.horizontal: + renderObject.direction = BlockDirection.horizontal; + break; + case ScrollDirection.both: assert(false); // we don't support ScrollDirection.both, see issue 888 + } + renderObject.startOffset = _childOffsets[index] - widget.startOffset; + // Build all the widgets we still need. + while (_childOffsets[index] < endOffset) { + if (!builtChildren.containsKey(index)) { + Element element = _getElement(index, innerConstraints); + if (element == null) { + _didReachLastChild = true; + break; + } + if (index == _childOffsets.length-1) { + // Remember this element's offset. + final double newOffset = _getOffset(element, innerConstraints); + _childOffsets.add(_childOffsets[index] + newOffset); + } else { + // Verify that it hasn't changed size. + assert(_childOffsets[index] - _childOffsets[index-1] == _getOffset(element, innerConstraints)); + } + // Remember the element for when we place the children. + final _ChildKey key = new _ChildKey.fromWidget(element.widget); + newChildren[key] = element; + builtChildren[index] = element; + } + assert(builtChildren[index] != null); + index += 1; + } + } + + // Remove any old children. + for (_ChildKey oldChildKey in _childrenByKey.keys) { + if (!newChildren.containsKey(oldChildKey)) + updateChild(_childrenByKey[oldChildKey], null, null); + } + + if (haveChildren) { + // Place all our children in our RenderObject. + // All the children we are placing are in builtChildren and newChildren. + // We will walk them backwards so we can set the slots at the same time. + Element nextSibling = null; + while (index > startIndex) { + index -= 1; + final Element element = builtChildren[index]; + if (element.slot != nextSibling) + updateSlotForChild(element, nextSibling); + nextSibling = element; + } + } + + // Update our internal state. + _childrenByKey = newChildren; + _firstVisibleChildIndex = startIndex; + } + + void updateSlotForChild(Element element, dynamic newSlot) { + assert(newSlot == null || newSlot == _omit || newSlot is Element); + super.updateSlotForChild(element, newSlot); + } + + void insertChildRenderObject(RenderObject child, dynamic slot) { + if (slot == _omit) + return; + assert(slot == null || slot is Element); + RenderObject nextSibling = slot?.renderObject; + renderObject.add(child, before: nextSibling); + } + + void moveChildRenderObject(RenderObject child, dynamic slot) { + if (slot == _omit) + return; + assert(slot == null || slot is Element); + RenderObject nextSibling = slot?.renderObject; + assert(nextSibling == null || nextSibling.parent == renderObject); + if (child.parent == renderObject) + renderObject.move(child, before: nextSibling); + else + renderObject.add(child, before: nextSibling); + } + + void removeChildRenderObject(RenderObject child) { + if (child.parent != renderObject) + return; // probably had slot == _omit when inserted + renderObject.remove(child); + } + +} diff --git a/sky/unit/test/fn3/homogeneous_viewport_test.dart b/sky/unit/test/fn3/homogeneous_viewport_test.dart index 85488c604d1..c2fa3b1507b 100644 --- a/sky/unit/test/fn3/homogeneous_viewport_test.dart +++ b/sky/unit/test/fn3/homogeneous_viewport_test.dart @@ -2,25 +2,7 @@ import 'package:sky/src/fn3.dart'; import 'package:test/test.dart'; import 'widget_tester.dart'; - -class TestComponent extends StatefulComponent { - TestComponent(this.viewport); - final HomogeneousViewport viewport; - TestComponentState createState() => new TestComponentState(this); -} - -class TestComponentState extends ComponentState { - TestComponentState(TestComponent config): super(config); - bool _flag = true; - void go(bool flag) { - setState(() { - _flag = flag; - }); - } - Widget build(BuildContext context) { - return _flag ? config.viewport : new Text('Not Today'); - } -} +import 'test_widgets.dart'; void main() { test('HomogeneousViewport mount/dismount smoke test', () { @@ -32,39 +14,42 @@ void main() { // so if our widget is 100 pixels tall, it should fit exactly 6 times. Widget builder() { - return new TestComponent(new HomogeneousViewport( - builder: (BuildContext context, 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 new FlipComponent( + left: new HomogeneousViewport( + builder: (BuildContext context, 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 + ), + right: new Text('Not Today') + ); } tester.pumpFrame(builder()); - StatefulComponentElement testComponent = tester.findElement((element) => element.widget is TestComponent); - TestComponentState testComponentState = testComponent.state; + StatefulComponentElement element = tester.findElement((element) => element.widget is FlipComponent); + FlipComponentState testComponent = element.state; expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); callbackTracker.clear(); - testComponentState.go(false); + testComponent.flip(); tester.pumpFrameWithoutChange(); expect(callbackTracker, equals([])); callbackTracker.clear(); - testComponentState.go(true); + testComponent.flip(); tester.pumpFrameWithoutChange(); expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); @@ -95,13 +80,16 @@ void main() { return result; }; - TestComponent testComponent; + FlipComponent testComponent; Widget builder() { - testComponent = new TestComponent(new HomogeneousViewport( - builder: itemBuilder, - startOffset: offset, - itemExtent: 200.0 - )); + testComponent = new FlipComponent( + left: new HomogeneousViewport( + builder: itemBuilder, + startOffset: offset, + itemExtent: 200.0 + ), + right: new Text('Not Today') + ); return testComponent; } @@ -145,14 +133,17 @@ void main() { return result; }; - TestComponent testComponent; + FlipComponent testComponent; Widget builder() { - testComponent = new TestComponent(new HomogeneousViewport( - builder: itemBuilder, - startOffset: offset, - itemExtent: 200.0, - direction: ScrollDirection.horizontal - )); + testComponent = new FlipComponent( + left: new HomogeneousViewport( + builder: itemBuilder, + startOffset: offset, + itemExtent: 200.0, + direction: ScrollDirection.horizontal + ), + right: new Text('Not Today') + ); return testComponent; } diff --git a/sky/unit/test/fn3/mixed_viewport_test.dart b/sky/unit/test/fn3/mixed_viewport_test.dart new file mode 100644 index 00000000000..466b37f3cd2 --- /dev/null +++ b/sky/unit/test/fn3/mixed_viewport_test.dart @@ -0,0 +1,152 @@ +import 'package:sky/src/fn3.dart'; +import 'package:test/test.dart'; + +import 'widget_tester.dart'; +import 'test_widgets.dart'; + +void main() { + test('MixedViewport 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. + + Widget builder() { + return new FlipComponent( + left: new MixedViewport( + builder: (BuildContext context, int i) { + callbackTracker.add(i); + return new Container( + key: new ValueKey(i), + height: 100.0, + child: new Text("$i") + ); + }, + startOffset: 0.0 + ), + right: new Text('Not Today') + ); + } + + tester.pumpFrame(builder()); + + StatefulComponentElement element = tester.findElement((element) => element.widget is FlipComponent); + FlipComponentState testComponent = element.state; + + expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); + + callbackTracker.clear(); + testComponent.flip(); + tester.pumpFrameWithoutChange(); + + expect(callbackTracker, equals([])); + + callbackTracker.clear(); + testComponent.flip(); + tester.pumpFrameWithoutChange(); + + expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); + + }); + + test('MixedViewport 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; + + IndexedBuilder itemBuilder = (BuildContext context, int i) { + callbackTracker.add(i); + return new Container( + key: new ValueKey(i), + width: 500.0, // this should be ignored + height: 200.0, + child: new Text("$i") + ); + }; + + Widget builder() { + return new FlipComponent( + left: new MixedViewport( + builder: itemBuilder, + startOffset: offset + ), + right: new Text('Not Today') + ); + } + + tester.pumpFrame(builder()); + + // 0 is built to find its width + expect(callbackTracker, equals([0, 1, 2, 3, 4])); + + callbackTracker.clear(); + + offset = 400.0; // now only 3 should fit, numbered 2-4. + + tester.pumpFrame(builder()); + + // 0 and 1 aren't built, we know their size and nothing else changed + expect(callbackTracker, equals([2, 3, 4])); + + callbackTracker.clear(); + + }); + + test('MixedViewport 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; + + IndexedBuilder itemBuilder = (BuildContext context, int i) { + callbackTracker.add(i); + return new Container( + key: new ValueKey(i), + height: 500.0, // this should be ignored + width: 200.0, + child: new Text("$i") + ); + }; + + Widget builder() { + return new FlipComponent( + left: new MixedViewport( + builder: itemBuilder, + startOffset: offset, + direction: ScrollDirection.horizontal + ), + right: new Text('Not Today') + ); + } + + tester.pumpFrame(builder()); + + // 0 is built to find its width + expect(callbackTracker, equals([0, 1, 2, 3, 4, 5])); + + callbackTracker.clear(); + + offset = 400.0; // now only 4 should fit, numbered 2-5. + + tester.pumpFrame(builder()); + + // 0 and 1 aren't built, we know their size and nothing else changed + expect(callbackTracker, equals([2, 3, 4, 5])); + + callbackTracker.clear(); + + }); +}