Commit b9489678 authored by Hixie's avatar Hixie

fn3: MixedViewport

Also:
- Expose the slot of each Element.
- Minor improvements to HomogeneousViewport.
- Replace TestComponent with FlipComponent in tests.
parent 878775bb
......@@ -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';
......
......@@ -419,6 +419,7 @@ abstract class Element<T extends Widget> 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<T extends Widget> 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<T extends Widget> 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<T extends Widget> extends Element<T> {
}
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<StatefulComponent> {
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<StatefulComponent> {
void unmount() {
super.unmount();
_state.dispose();
_state._element = null;
_state = null;
}
}
......@@ -1016,6 +1019,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Element
void unmount() {
super.unmount();
assert(!renderObject.attached);
widget.didUnmountRenderObject(renderObject);
}
......@@ -1024,10 +1028,10 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> 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() {
......
......@@ -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<HomogeneousViewport
renderObject.add(child, before: nextSibling);
}
void moveChildRenderObject(RenderObject child, dynamic slot) {
void moveChildRenderObject(RenderObject child, Element slot) {
assert(child.parent == renderObject);
RenderObject nextSibling = slot?.renderObject;
renderObject.move(child, before: nextSibling);
}
......
// 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:collection';
import 'package:sky/rendering.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/basic.dart';
typedef Widget IndexedBuilder(BuildContext context, int index); // return null if index is greater than index of last entry
typedef void ExtentsUpdateCallback(double newExtents);
typedef void InvalidatorCallback(Iterable<int> 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<MixedViewport> {
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<double> _childOffsets = <double>[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<int> _invalidIndices = new Set<int>();
/// 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<int> indices) {
assert(indices.length > 0);
_invalidIndices.addAll(indices);
renderObject.markNeedsLayout();
}
/// Forget all the known child offsets.
void _resetCache() {
_didReachLastChild = false;
_childOffsets = <double>[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<int, Element> builtChildren = new Map<int, Element>();
// 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<int> 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);
}
}
......@@ -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<TestComponent> {
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<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index);
result.add(new Container(
key: new ValueKey<int>(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<Widget> result = <Widget>[];
for (int index = start; index < start + count; index += 1) {
callbackTracker.add(index);
result.add(new Container(
key: new ValueKey<int>(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;
}
......
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<int> callbackTracker = <int>[];
// 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<int>(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<int> callbackTracker = <int>[];
// 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<int>(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<int> callbackTracker = <int>[];
// 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<int>(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();
});
}
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