mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
fn3: MixedViewport
Also: - Expose the slot of each Element. - Minor improvements to HomogeneousViewport. - Replace TestComponent with FlipComponent in tests.
This commit is contained in:
parent
aee7a95af0
commit
a498903957
@ -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);
|
||||
}
|
||||
|
||||
545
sky/packages/sky/lib/src/fn3/mixed_viewport.dart
Normal file
545
sky/packages/sky/lib/src/fn3/mixed_viewport.dart
Normal file
@ -0,0 +1,545 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
152
sky/unit/test/fn3/mixed_viewport_test.dart
Normal file
152
sky/unit/test/fn3/mixed_viewport_test.dart
Normal file
@ -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<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();
|
||||
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user