Commit f683abd7 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Sliver Block (#7618)

parent 2380d854
...@@ -45,6 +45,7 @@ export 'src/rendering/rotated_box.dart'; ...@@ -45,6 +45,7 @@ export 'src/rendering/rotated_box.dart';
export 'src/rendering/semantics.dart'; export 'src/rendering/semantics.dart';
export 'src/rendering/shifted_box.dart'; export 'src/rendering/shifted_box.dart';
export 'src/rendering/sliver.dart'; export 'src/rendering/sliver.dart';
export 'src/rendering/sliver_block.dart';
export 'src/rendering/stack.dart'; export 'src/rendering/stack.dart';
export 'src/rendering/table.dart'; export 'src/rendering/table.dart';
export 'src/rendering/tweens.dart'; export 'src/rendering/tweens.dart';
......
...@@ -1145,22 +1145,26 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl ...@@ -1145,22 +1145,26 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
/// ///
/// If the [center] is not specified, then the first child in the `children` /// If the [center] is not specified, then the first child in the `children`
/// list, if any, is used. /// list, if any, is used.
///
/// The [offset] must be specified. For testing purposes, consider passing a
/// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
RenderViewport2({ RenderViewport2({
AxisDirection axisDirection: AxisDirection.down, AxisDirection axisDirection: AxisDirection.down,
double anchor: 0.0, double anchor: 0.0,
ViewportOffset offset, @required ViewportOffset offset,
List<RenderSliver> children, List<RenderSliver> children,
RenderSliver center, RenderSliver center,
}) : _axisDirection = axisDirection, }) : _axisDirection = axisDirection,
_anchor = anchor, _anchor = anchor,
_offset = offset ?? new ViewportOffset.zero(), _offset = offset,
_center = center { _center = center {
addAll(children); assert(offset != null);
if (center == null && firstChild != null)
_center = firstChild;
assert(axisDirection != null); assert(axisDirection != null);
assert(anchor != null); assert(anchor != null);
assert(anchor >= 0.0 && anchor <= 1.0); assert(anchor >= 0.0 && anchor <= 1.0);
addAll(children);
if (center == null && firstChild != null)
_center = firstChild;
} }
AxisDirection get axisDirection => _axisDirection; AxisDirection get axisDirection => _axisDirection;
......
// Copyright 2016 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 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'binding.dart';
import 'object.dart';
import 'sliver.dart';
class SliverBlockParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderBox> {
int index;
@override
String toString() => 'index=$index; ${super.toString()}';
}
// ///
// ///
// /// The contract for adding and removing children from this render object is
// /// more strict than for normal render objects:
// ///
// /// - Children can be removed except during a layout pass if they have already
// /// been laid out during that layout pass.
// /// - Children cannot be added except during a call to [allowAdditionsFor], and
// /// then only if there is no child correspending to that index (or the child
// /// child corresponding to that index was first removed).
// ///
// /// ## Writing a RenderSliverBlock subclass
// ///
// /// There are three methods to override:
// ///
// /// - [createChild], which must create the child for the given index, and then
// /// insert it in the given location in the child list.
// ///
// /// - [removeChild], which is called when the given child is cleaned up,
// /// and which should remove the given child from the child list.
// ///
// /// - [estimateScrollOffsetExtent], which should return the total extent (e.g.
// /// the height, if this is a vertical block) of all the children.
abstract class RenderSliverBlock extends RenderSliver
with ContainerRenderObjectMixin<RenderBox, SliverBlockParentData>,
RenderSliverHelpers {
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverBlockParentData)
child.parentData = new SliverBlockParentData();
}
/// Called during layout when a new child is needed. The child should be
/// inserted into the child list in the appropriate position, after the
/// `after` child (at the start of the list if `after` is null). Its index and
/// scroll offsets will automatically be set appropriately.
///
/// The `index` argument gives the index of the child to show. The first child
/// that will be requested will have index zero, and this should be the child
/// that is aligned with the zero scroll offset. Subsequent requests will be
/// for indices adjacent to previously requested indices. It is possible for
/// negative indices to be requested. For example: if the user scrolls from
/// child 0 to child 10, and then those children get much smaller, and then
/// the user scrolls back up again, this method will eventually be asked to
/// produce a child for index -1.
///
/// If no child corresponds to `index`, then do nothing.
///
/// Which child is indicated by index zero depends on the [GrowthDirection]
/// specified in the [constraints]. For example if the children are the
/// alphabet, then if [constraints.growthDirection] is
/// [GrowthDirection.forward] then index zero is A, and index 25 is Z. On the
/// other hand if [constraints.growthDirection] is [GrowthDirection.reverse]
/// then index zero is Z, and index 25 is A.
///
/// During a call to [createChild] it is valid to remove other children from
/// the [RenderSliverBlock] object if they were not created during this frame
/// and have not yet been updated during this frame. It is not valid to add
/// any children to this render object.
@protected
void createChild(int index, { @required RenderBox after });
/// Remove the given child from the child list.
///
/// Called by [collectGarbage], which itself is called from [performLayout],
/// after the layout algorithm has finished and the non-visible children are
/// to be removed.
///
/// The default implementation calls [remove], which removes the child from
/// the child list.
///
/// The index of the given child can be obtained using the [indexOf] method,
/// which reads it from the [SliverBlockParentData.index] field of the child's
/// [parentData].
@protected
void removeChild(RenderBox child) {
remove(child);
}
int _currentlyUpdatingChildIndex;
@protected
void allowAdditionsFor(int index, VoidCallback callback) {
assert(_currentlyUpdatingChildIndex == null);
assert(index != null);
_currentlyUpdatingChildIndex = index;
try {
callback();
} finally {
_currentlyUpdatingChildIndex = null;
}
}
@override
void adoptChild(RenderObject child) {
assert(_currentlyUpdatingChildIndex != null);
super.adoptChild(child);
final SliverBlockParentData childParentData = child.parentData;
childParentData.index = _currentlyUpdatingChildIndex;
}
@override
void insert(RenderBox child, { RenderBox after }) {
super.insert(child, after: after);
assert(firstChild != null);
assert(() {
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
assert(indexOf(child) > index);
index = indexOf(child);
child = childAfter(child);
}
return true;
});
}
/// Called to estimate the total scrollable extents of this object.
///
/// Must return the total distance from the start of the child with the
/// earliest possible index to the end of the child with the last possible
/// index.
@protected
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
});
/// Called during layout to create and add the child with index 0 and scroll
/// offset 0.0.
///
/// Calls [createChild] to actually create and add the child.
///
/// Returns false if createChild did not add any child, otherwise returns
/// true.
///
/// Does not layout the new child.
///
/// When this is called, there are no children, so no children can be removed
/// during the call to createChild. No child should be added during that call
/// either, except for the one that is created and returned by [createChild].
@protected
bool addInitialChild() {
assert(_currentlyUpdatingChildIndex == null);
assert(firstChild == null);
bool result;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
allowAdditionsFor(0, () {
createChild(0, after: null);
if (firstChild != null) {
assert(firstChild == lastChild);
assert(indexOf(firstChild) == 0);
final SliverBlockParentData firstChildParentData = firstChild.parentData;
firstChildParentData.scrollOffset = 0.0;
result = true;
} else {
result = false;
}
});
});
return result;
}
/// Called during layout to create, add, and layout the child before
/// [firstChild].
///
/// Calls [createChild] to actually create and add the child.
///
/// Returns the new child.
///
/// The child that was previously the first child, as well as any subsequent
/// children, may be removed by this call if they have not yet been laid out
/// during this layout pass. No child should be added during that call except
/// for the one that is created and returned by [createChild].
@protected
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints) {
assert(_currentlyUpdatingChildIndex == null);
final int index = indexOf(firstChild) - 1;
final double endScrollOffset = offsetOf(firstChild);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
allowAdditionsFor(index, () {
createChild(index, after: null);
});
});
if (indexOf(firstChild) == index) {
firstChild.layout(childConstraints, parentUsesSize: true);
SliverBlockParentData firstChildParentData = firstChild.parentData;
firstChildParentData.scrollOffset = endScrollOffset - paintExtentOf(firstChild);
return firstChild;
}
return null;
}
/// Called during layout to create, add, and layout the child after
/// the given child.
///
/// Calls [createChild] to actually create and add the child.
///
/// Returns the new child. It is the responsibility of the caller to configure
/// the child's scroll offset.
///
/// Children after the `after` child may be removed in the process. Only the
/// new child may be added.
@protected
RenderBox insertAndLayoutChild(BoxConstraints childConstraints, { @required RenderBox after }) {
assert(_currentlyUpdatingChildIndex == null);
assert(after != null);
final int index = indexOf(after) + 1;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
allowAdditionsFor(index, () {
createChild(index, after: after);
});
});
final RenderBox child = childAfter(after);
if (child != null && indexOf(child) == index) {
assert(indexOf(child) == index);
child.layout(childConstraints, parentUsesSize: true);
return child;
}
return null;
}
/// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list.
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_currentlyUpdatingChildIndex == null);
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) {
removeChild(firstChild);
leadingGarbage -= 1;
}
while (trailingGarbage > 0) {
removeChild(lastChild);
trailingGarbage -= 1;
}
});
}
/// Returns the index of the given child, as given by the
/// [SliverBlockParentData.index] field of the child's [parentData].
@protected
int indexOf(RenderBox child) {
assert(child != null);
final SliverBlockParentData childParentData = child.parentData;
assert(childParentData.index != null);
return childParentData.index;
}
/// Returns the scroll offset of the given child, as given by the
/// [SliverBlockParentData.scrollOffset] field of the child's [parentData].
@protected
double offsetOf(RenderBox child) {
assert(child != null);
final SliverBlockParentData childParentData = child.parentData;
assert(childParentData.scrollOffset != null);
return childParentData.scrollOffset;
}
/// Returns the dimension of the given child in the main axis, as given by the
/// child's [RenderBox.size] property. This is only valid after layout.
@protected
double paintExtentOf(RenderBox child) {
assert(child != null);
assert(child.hasSize);
switch (constraints.axis) {
case Axis.horizontal:
return child.size.width;
case Axis.vertical:
return child.size.height;
}
return null;
}
@override
void performLayout() {
assert(_currentlyUpdatingChildIndex == null);
double scrollOffset = constraints.scrollOffset;
assert(scrollOffset >= 0.0);
double remainingPaintExtent = constraints.remainingPaintExtent;
assert(remainingPaintExtent >= 0.0);
double targetEndScrollOffset = scrollOffset + remainingPaintExtent;
BoxConstraints childConstraints = constraints.asBoxConstraints();
int leadingGarbage = 0;
int trailingGarbage = 0;
bool reachedEnd = false;
// This algorithm in principle is straight-forward: find the first child
// that overlaps the given scrollOffset, creating more children at the top
// of the list if necessary, then walk down the list updating and laying out
// each child and adding more at the end if necessary until we have enough
// children to cover the entire viewport.
//
// It is complicated by one minor issue, which is that any time you update
// or create a child, it's possible that the some of the children that
// haven't yet been laid out will be removed, leaving the list in an
// inconsistent state, and requiring that missing nodes be recreated.
//
// To keep this mess tractable, this algorithm starts from what is currently
// the first child, if any, and then walks up and/or down from there, so
// that the nodes that might get removed are always at the edges of what has
// already been laid out.
// Make sure we have at least one child to start from.
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = new SliverGeometry(
scrollExtent: 0.0,
paintExtent: 0.0,
maxPaintExtent: 0.0,
);
return;
}
}
// We have at least one child.
// These variables track the range of children that we have laid out. Within
// this range, the children have consecutive indices. Outside this range,
// it's possible for a child to get removed without notice.
RenderBox leadingChildWithLayout, trailingChildWithLayout;
// Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild;
while (offsetOf(earliestUsefulChild) > scrollOffset) {
// We have to add children before the earliestUsefulChild.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints);
if (earliestUsefulChild == null) {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction.
geometry = new SliverGeometry(
scrollOffsetCorrection: -offsetOf(firstChild),
);
return;
}
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
// At this point, earliestUsefulChild is the first child, and is a child
// whose scrollOffset is at or before the scrollOffset, and
// leadingChildWithLayout and trailingChildWithLayout are either null or
// cover a range of render boxes that we have laid out with the first being
// the same as earliestUsefulChild and the last being either at or after the
// scroll offset.
assert(earliestUsefulChild == firstChild);
assert(offsetOf(earliestUsefulChild) <= scrollOffset);
// Make sure we've laid out at least one child.
if (leadingChildWithLayout == null) {
earliestUsefulChild.layout(childConstraints, parentUsesSize: true);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}
// Here, earliestUsefulChild is still the first child, it's got a
// scrollOffset that is at or before our actual scrollOffset, and it has
// been laid out, and is in fact our leadingChildWithLayout. It's possible
// that some children beyond that one have also been laid out.
bool inLayoutRange = true;
RenderBox child = earliestUsefulChild;
int index = indexOf(child);
double endScrollOffset = offsetOf(child) + paintExtentOf(child);
bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child);
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverBlockParentData childParentData = child.parentData;
childParentData.scrollOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = offsetOf(child) + paintExtentOf(child);
return true;
}
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent = offsetOf(lastChild) + paintExtentOf(lastChild);
geometry = new SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
);
return;
}
}
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
// Finally count up all the remaining children and label them as garbage.
if (child != null) {
child = childAfter(child);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child);
}
}
// At this point everything should be good to go, we just have to clean up
// the garbage and report the geometry.
collectGarbage(leadingGarbage, trailingGarbage);
assert(firstChild != null);
assert(() {
int index = indexOf(firstChild);
RenderBox child = childAfter(firstChild);
while (child != null) {
index += 1;
assert(indexOf(child) == index);
child = childAfter(child);
}
return true;
});
double estimatedTotalExtent;
if (reachedEnd) {
estimatedTotalExtent = endScrollOffset;
} else {
estimatedTotalExtent = estimateScrollOffsetExtent(
firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild),
leadingScrollOffset: offsetOf(firstChild),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedTotalExtent >= endScrollOffset - offsetOf(firstChild));
}
double paintedExtent = calculatePaintOffset(
constraints,
from: offsetOf(firstChild),
to: endScrollOffset,
);
geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent,
);
assert(_currentlyUpdatingChildIndex == null);
}
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
RenderBox child = lastChild;
while (child != null) {
if (child != null) {
if (hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition))
return true;
}
child = childBefore(child);
}
return false;
}
@override
double childPosition(RenderBox child) {
return offsetOf(child);
}
// TODO(ianh): There's a lot of duplicate code in the next two functions,
// but I don't see a good way to avoid it, since both functions are hot.
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
// coordinate system origin here is at the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset unitOffset, originOffset;
bool addExtent;
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
unitOffset = const Offset(0.0, -1.0);
originOffset = new Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
unitOffset = const Offset(1.0, 0.0);
originOffset = Offset.zero;
addExtent = false;
break;
case AxisDirection.down:
unitOffset = const Offset(0.0, 1.0);
originOffset = Offset.zero;
addExtent = false;
break;
case AxisDirection.left:
unitOffset = const Offset(-1.0, 0.0);
originOffset = new Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(unitOffset != null);
assert(addExtent != null);
Offset childOffset = originOffset + unitOffset * (offsetOf(child) - constraints.scrollOffset);
if (addExtent)
childOffset += unitOffset * paintExtentOf(child);
transform.translate(childOffset.dx, childOffset.dy);
}
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
// offset is to the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset unitOffset, originOffset;
bool addExtent;
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
unitOffset = const Offset(0.0, -1.0);
originOffset = offset + new Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
unitOffset = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
unitOffset = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
unitOffset = const Offset(-1.0, 0.0);
originOffset = offset + new Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(unitOffset != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
Offset childOffset = originOffset + unitOffset * (offsetOf(child) - constraints.scrollOffset);
if (addExtent)
childOffset += unitOffset * paintExtentOf(child);
context.paintChild(child, childOffset);
child = childAfter(child);
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (firstChild != null) {
description.add('currently live children: ${indexOf(firstChild)} to ${indexOf(lastChild)}');
} else {
description.add('no children current live');
}
}
}
// Copyright 2016 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' show SplayTreeMap, HashMap;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'framework.dart';
import 'basic.dart';
abstract class SliverBlockDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliverBlockDelegate();
Widget build(BuildContext context, int index);
bool shouldRebuild(@checked SliverBlockDelegate oldDelegate);
int get childCount;
double estimateScrollOffsetExtent(
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
) {
return childCount * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1);
}
}
// ///
// /// In general building all the widgets in advance is not efficient. It is
// /// better to create a delegate that builds them on demand by subclassing
// /// [SliverBlockDelegate] directly.
// ///
// /// This class is provided for the cases where either the list of children is
// /// known well in advance (ideally the children are themselves compile-time
// /// constants, for example), and therefore will not be built each time the
// /// delegate itself is created, or the list is small, such that it's likely
// /// always visible (and thus there is nothing to be gained by building it on
// /// demand). For example, the body of a dialog box might fit both of these
// /// conditions.
class SliverBlockChildListDelegate extends SliverBlockDelegate {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliverBlockChildListDelegate(this.children);
final List<Widget> children;
@override
Widget build(BuildContext context, int index) {
assert(children != null);
if (index < 0 || index >= children.length)
return null;
return children[index];
}
@override
bool shouldRebuild(@checked SliverBlockChildListDelegate oldDelegate) {
return children != oldDelegate.children;
}
@override
int get childCount => children.length;
}
class SliverBlock extends RenderObjectWidget {
SliverBlock({
Key key,
@required this.delegate,
}) : super(key: key) {
assert(delegate != null);
}
final SliverBlockDelegate delegate;
@override
_SliverBlockElement createElement() => new _SliverBlockElement(this);
@override
_RenderSliverBlockForWidgets createRenderObject(BuildContext context) => new _RenderSliverBlockForWidgets();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('delegate: $delegate');
}
}
class _SliverBlockElement extends RenderObjectElement {
_SliverBlockElement(SliverBlock widget) : super(widget);
@override
SliverBlock get widget => super.widget;
@override
_RenderSliverBlockForWidgets get renderObject => super.renderObject;
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
renderObject._element = this;
}
@override
void unmount() {
super.unmount();
renderObject._element = null;
}
@override
void update(SliverBlock newWidget) {
final SliverBlock oldWidget = widget;
super.update(newWidget);
final SliverBlockDelegate newDelegate = newWidget.delegate;
final SliverBlockDelegate oldDelegate = oldWidget.delegate;
if (newDelegate != oldDelegate &&
(newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
performRebuild();
}
Map<int, Element> _childElements = new SplayTreeMap<int, Element>();
Map<int, Widget> _childWidgets = new HashMap<int, Widget>();
RenderBox _currentBeforeChild;
bool _debugOpenToChanges = false;
@override
void performRebuild() {
_childWidgets.clear();
super.performRebuild();
_currentBeforeChild = null;
assert(!_debugOpenToChanges);
assert(() { _debugOpenToChanges = true; return true; });
try {
// The "toList()" below is to get a copy of the array so that we can
// mutate _childElements within the loop. Basically we just update all the
// same indexes as we had before. If any of them mutate the tree, then
// this will also trigger a layout and so forth. (We won't call the
// delegate's build function multiple times, though, because we cache the
// delegate's results until the next time we need to rebuild the whole
// block widget.)
for (int index in _childElements.keys.toList()) {
Element newChild;
renderObject._rebuild(index, () {
newChild = updateChild(_childElements[index], _build(index), index);
});
if (newChild != null) {
_childElements[index] = newChild;
_currentBeforeChild = newChild.renderObject;
} else {
_childElements.remove(index);
}
}
} finally {
assert(() { _debugOpenToChanges = false; return true; });
}
}
Widget _build(int index) {
return _childWidgets.putIfAbsent(index, () {
Widget child = widget.delegate.build(this, index);
if (child == null)
return null;
return new RepaintBoundary.wrap(child, index);
});
}
void _createChild(int index, bool insertFirst) {
assert(!_debugOpenToChanges);
owner.buildScope(this, () {
assert(insertFirst || _childElements[index-1] != null);
assert(() { _debugOpenToChanges = true; return true; });
_currentBeforeChild = insertFirst ? null : _childElements[index-1].renderObject;
Element newChild;
try {
newChild = updateChild(_childElements[index], _build(index), index);
} finally {
assert(() { _debugOpenToChanges = false; return true; });
}
if (newChild != null) {
_childElements[index] = newChild;
} else {
_childElements.remove(index);
}
});
}
@override
void forgetChild(Element child) {
assert(child != null);
assert(child.slot != null);
assert(_childElements.containsKey(child.slot));
_childElements.remove(child.slot);
}
void _removeChild(int index) {
assert(!_debugOpenToChanges);
assert(index >= 0);
owner.buildScope(this, () {
assert(_childElements.containsKey(index));
assert(() { _debugOpenToChanges = true; return true; });
try {
Element result = updateChild(_childElements[index], null, index);
assert(result == null);
} finally {
assert(() { _debugOpenToChanges = false; return true; });
}
_childElements.remove(index);
assert(!_childElements.containsKey(index));
});
}
@override
void insertChildRenderObject(@checked RenderObject child, int slot) {
assert(_debugOpenToChanges);
renderObject.insert(child, after: _currentBeforeChild);
assert(() {
SliverBlockParentData childParentData = child.parentData;
assert(slot == childParentData.index);
return true;
});
}
@override
void moveChildRenderObject(@checked RenderObject child, int slot) {
// TODO(ianh): At some point we should be better about noticing when a
// particular LocalKey changes slot, and handle moving the nodes around.
assert(false);
}
@override
void removeChildRenderObject(@checked RenderObject child) {
assert(_debugOpenToChanges);
renderObject.remove(child);
}
@override
void visitChildren(ElementVisitor visitor) {
// The toList() is to make a copy so that the underlying list can be modified by
// the visitor:
assert(!_childElements.values.any((Element child) => child == null));
_childElements.values.toList().forEach(visitor);
}
}
class _RenderSliverBlockForWidgets extends RenderSliverBlock {
_SliverBlockElement _element;
@override
void createChild(int index, { @required RenderBox after }) {
assert(_element != null);
_element._createChild(index, after == null);
}
@override
void removeChild(RenderBox child) {
assert(_element != null);
_element._removeChild(indexOf(child));
}
void _rebuild(int index, VoidCallback callback) {
allowAdditionsFor(index, callback);
}
@override
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
assert(lastIndex >= firstIndex);
assert(_element != null);
return _element.widget.delegate.estimateScrollOffsetExtent(firstIndex, lastIndex, leadingScrollOffset, trailingScrollOffset);
}
}
...@@ -16,10 +16,11 @@ class Viewport2 extends MultiChildRenderObjectWidget { ...@@ -16,10 +16,11 @@ class Viewport2 extends MultiChildRenderObjectWidget {
Key key, Key key,
this.axisDirection: AxisDirection.down, this.axisDirection: AxisDirection.down,
this.anchor: 0.0, this.anchor: 0.0,
this.offset, @required this.offset,
this.center, this.center,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : super(key: key, children: children) { }) : super(key: key, children: children) {
assert(offset != null);
assert(center == null || children.where((Widget child) => child.key == center).length == 1); assert(center == null || children.where((Widget child) => child.key == center).length == 1);
} }
......
...@@ -13,6 +13,7 @@ export 'src/widgets/app.dart'; ...@@ -13,6 +13,7 @@ export 'src/widgets/app.dart';
export 'src/widgets/banner.dart'; export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart'; export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart'; export 'src/widgets/binding.dart';
export 'src/widgets/block.dart';
export 'src/widgets/clamp_overscrolls.dart'; export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/container.dart'; export 'src/widgets/container.dart';
export 'src/widgets/debug.dart'; export 'src/widgets/debug.dart';
......
// 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 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
import 'rendering_tester.dart';
class RenderSliverBlockTest extends RenderSliverBlock {
RenderSliverBlockTest({
this.children,
});
List<RenderBox> children;
@override
void createChild(int index, { @required RenderBox after }) {
assert(index >= 0);
if (index < 0 || index >= children.length)
return null;
insert(children[index], after: after);
}
@override
double estimateScrollOffsetExtent({
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
}) {
assert(lastIndex >= firstIndex);
return children.length * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1);
}
}
void main() {
test('RenderSliverBlock basic test - down', () {
RenderObject inner;
RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.down,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
inner = new RenderSliverBlockTest(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
),
],
);
layout(root);
expect(root.size.width, equals(800.0));
expect(root.size.height, equals(600.0));
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 400.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
// make sure that layout is stable by laying out again
inner.markNeedsLayout();
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 400.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
// now try various scroll offsets
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -200.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 200.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
root.offset = new ViewportOffset.fixed(600.0);
pumpFrame();
expect(a.attached, false);
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -200.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 200.0));
expect(d.attached, false);
expect(e.attached, false);
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
expect(a.attached, false);
expect(b.attached, false);
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -100.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 300.0));
expect(e.attached, false);
// try going back up
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -200.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 200.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
});
test('RenderSliverBlock basic test - up', () {
RenderObject inner;
RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.up,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
inner = new RenderSliverBlockTest(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
),
],
);
layout(root);
expect(root.size.width, equals(800.0));
expect(root.size.height, equals(600.0));
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 200.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -200.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
// make sure that layout is stable by laying out again
inner.markNeedsLayout();
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 200.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -200.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
// now try various scroll offsets
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 400.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 0.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
root.offset = new ViewportOffset.fixed(600.0);
pumpFrame();
expect(a.attached, false);
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 400.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 0.0));
expect(d.attached, false);
expect(e.attached, false);
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
expect(a.attached, false);
expect(b.attached, false);
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 300.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -100.0));
expect(e.attached, false);
// try going back up
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 400.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 0.0));
expect(c.attached, false);
expect(d.attached, false);
expect(e.attached, false);
});
}
...@@ -25,6 +25,7 @@ void main() { ...@@ -25,6 +25,7 @@ void main() {
RenderBox box; RenderBox box;
RenderObject root = new RenderLayoutWatcher( RenderObject root = new RenderLayoutWatcher(
viewport = new RenderViewport2( viewport = new RenderViewport2(
offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
sliver = new RenderSliverToBoxAdapter(child: box = new RenderSizedBox(const Size(100.0, 400.0))), sliver = new RenderSliverToBoxAdapter(child: box = new RenderSizedBox(const Size(100.0, 400.0))),
], ],
......
...@@ -9,7 +9,9 @@ import 'rendering_tester.dart'; ...@@ -9,7 +9,9 @@ import 'rendering_tester.dart';
void main() { void main() {
test('RenderViewport2 basic test - no children', () { test('RenderViewport2 basic test - no children', () {
RenderViewport2 root = new RenderViewport2(); RenderViewport2 root = new RenderViewport2(
offset: new ViewportOffset.zero(),
);
layout(root); layout(root);
root.offset = new ViewportOffset.fixed(900.0); root.offset = new ViewportOffset.fixed(900.0);
pumpFrame(); pumpFrame();
...@@ -18,6 +20,7 @@ void main() { ...@@ -18,6 +20,7 @@ void main() {
test('RenderViewport2 basic test - down', () { test('RenderViewport2 basic test - down', () {
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(100.0, 400.0))), new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(100.0, 400.0))),
new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(100.0, 400.0))), new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(100.0, 400.0))),
...@@ -66,6 +69,7 @@ void main() { ...@@ -66,6 +69,7 @@ void main() {
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.up, axisDirection: AxisDirection.up,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(100.0, 400.0))), new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(100.0, 400.0))),
new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(100.0, 400.0))), new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(100.0, 400.0))),
...@@ -114,6 +118,7 @@ void main() { ...@@ -114,6 +118,7 @@ void main() {
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.right, axisDirection: AxisDirection.right,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(400.0, 100.0))), new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(400.0, 100.0))),
new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(400.0, 100.0))), new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(400.0, 100.0))),
...@@ -162,6 +167,7 @@ void main() { ...@@ -162,6 +167,7 @@ void main() {
RenderBox a, b, c, d, e; RenderBox a, b, c, d, e;
RenderViewport2 root = new RenderViewport2( RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.left, axisDirection: AxisDirection.left,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[ children: <RenderSliver>[
new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(400.0, 100.0))), new RenderSliverToBoxAdapter(child: a = new RenderSizedBox(const Size(400.0, 100.0))),
new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(400.0, 100.0))), new RenderSliverToBoxAdapter(child: b = new RenderSizedBox(const Size(400.0, 100.0))),
......
// Copyright 2016 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
int globalGeneration = 0;
class GenerationText extends StatefulWidget {
GenerationText(this.value);
final int value;
@override
_GenerationTextState createState() => new _GenerationTextState();
}
class _GenerationTextState extends State<GenerationText> {
_GenerationTextState() : generation = globalGeneration;
final int generation;
@override
Widget build(BuildContext context) => new Text('${config.value}:$generation ');
}
Future<Null> test(WidgetTester tester, double offset, List<int> keys) {
globalGeneration += 1;
return tester.pumpWidget(new Viewport2(
offset: new ViewportOffset.fixed(offset),
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(keys.map((int key) {
return new SizedBox(key: new GlobalObjectKey(key), height: 100.0, child: new GenerationText(key));
}).toList()),
),
],
));
}
void verify(WidgetTester tester, List<Point> answerKey, String text) {
List<Point> testAnswers = tester.renderObjectList/*<RenderBox>*/(find.byType(SizedBox)).map/*<Point>*/(
(RenderBox target) => target.localToGlobal(const Point(0.0, 0.0))
).toList();
expect(testAnswers, equals(answerKey));
final String foundText =
tester.widgetList/*<Text>*/(find.byType(Text))
.map/*<String>*/((Text widget) => widget.data)
.reduce((String value, String element) => value + element);
expect(foundText, equals(text));
}
void main() {
testWidgets('Viewport2+SliverBlock with GlobalKey reparenting', (WidgetTester tester) async {
await test(tester, 0.0, <int>[1,2,3,4,5,6,7,8,9]);
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 100.0),
const Point(0.0, 200.0),
const Point(0.0, 300.0),
const Point(0.0, 400.0),
const Point(0.0, 500.0),
], '1:1 2:1 3:1 4:1 5:1 6:1 ');
// gen 2 - flipping the order:
await test(tester, 0.0, <int>[9,8,7,6,5,4,3,2,1]);
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 100.0),
const Point(0.0, 200.0),
const Point(0.0, 300.0),
const Point(0.0, 400.0),
const Point(0.0, 500.0),
], '9:2 8:2 7:2 6:1 5:1 4:1 ');
// gen 3 - flipping the order back:
await test(tester, 0.0, <int>[1,2,3,4,5,6,7,8,9]);
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 100.0),
const Point(0.0, 200.0),
const Point(0.0, 300.0),
const Point(0.0, 400.0),
const Point(0.0, 500.0),
], '1:3 2:3 3:3 4:1 5:1 6:1 ');
// gen 4 - removal:
await test(tester, 0.0, <int>[1,2,3,5,6,7,8,9]);
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 100.0),
const Point(0.0, 200.0),
const Point(0.0, 300.0),
const Point(0.0, 400.0),
const Point(0.0, 500.0),
], '1:3 2:3 3:3 5:1 6:1 7:4 ');
// gen 5 - insertion:
await test(tester, 0.0, <int>[1,2,3,4,5,6,7,8,9]);
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 100.0),
const Point(0.0, 200.0),
const Point(0.0, 300.0),
const Point(0.0, 400.0),
const Point(0.0, 500.0),
], '1:3 2:3 3:3 4:5 5:1 6:1 ');
// gen 6 - adjacent reordering:
await test(tester, 0.0, <int>[1,2,3,5,4,6,7,8,9]);
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 100.0),
const Point(0.0, 200.0),
const Point(0.0, 300.0),
const Point(0.0, 400.0),
const Point(0.0, 500.0),
], '1:3 2:3 3:3 5:1 4:5 6:1 ');
// gen 7 - scrolling:
await test(tester, 120.0, <int>[1,2,3,5,4,6,7,8,9]);
verify(tester, <Point>[
const Point(0.0, -20.0),
const Point(0.0, 80.0),
const Point(0.0, 180.0),
const Point(0.0, 280.0),
const Point(0.0, 380.0),
const Point(0.0, 480.0),
const Point(0.0, 580.0),
], '2:3 3:3 5:1 4:5 6:1 7:7 8:7 ');
});
}
// Copyright 2016 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
Future<Null> test(WidgetTester tester, double offset) {
return tester.pumpWidget(new Viewport2(
offset: new ViewportOffset.fixed(offset),
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')),
new SizedBox(height: 400.0, child: new Text('b')),
new SizedBox(height: 400.0, child: new Text('c')),
new SizedBox(height: 400.0, child: new Text('d')),
new SizedBox(height: 400.0, child: new Text('e')),
]),
),
],
));
}
void verify(WidgetTester tester, List<Point> answerKey, String text) {
List<Point> testAnswers = tester.renderObjectList/*<RenderBox>*/(find.byType(SizedBox)).map/*<Point>*/(
(RenderBox target) => target.localToGlobal(const Point(0.0, 0.0))
).toList();
expect(testAnswers, equals(answerKey));
final String foundText =
tester.widgetList/*<Text>*/(find.byType(Text))
.map/*<String>*/((Text widget) => widget.data)
.reduce((String value, String element) => value + element);
expect(foundText, equals(text));
}
void main() {
testWidgets('Viewport2+SliverBlock basic test', (WidgetTester tester) async {
await test(tester, 0.0);
expect(tester.renderObject/*<RenderBox>*/(find.byType(Viewport2)).size, equals(const Size(800.0, 600.0)));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 400.0),
], 'ab');
await test(tester, 200.0);
verify(tester, <Point>[
const Point(0.0, -200.0),
const Point(0.0, 200.0),
], 'ab');
await test(tester, 600.0);
verify(tester, <Point>[
const Point(0.0, -200.0),
const Point(0.0, 200.0),
], 'bc');
await test(tester, 900.0);
verify(tester, <Point>[
const Point(0.0, -100.0),
const Point(0.0, 300.0),
], 'cd');
await test(tester, 200.0);
verify(tester, <Point>[
const Point(0.0, -200.0),
const Point(0.0, 200.0),
], 'ab');
});
testWidgets('Viewport2 with GlobalKey reparenting', (WidgetTester tester) async {
Key key1 = new GlobalKey();
ViewportOffset offset = new ViewportOffset.zero();
await tester.pumpWidget(new Viewport2(
offset: offset,
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
]),
),
],
));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 251.0),
const Point(0.0, 503.0),
], 'abc');
await tester.pumpWidget(new Viewport2(
offset: offset,
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')),
]),
),
],
));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 253.0),
const Point(0.0, 504.0),
], 'cab');
await tester.pumpWidget(new Viewport2(
offset: offset,
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 252.0, child: new Text('b')),
]),
),
],
));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 251.0),
const Point(0.0, 504.0),
], 'acb');
await tester.pumpWidget(new Viewport2(
offset: offset,
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')),
]),
),
],
));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 251.0),
], 'ab');
await tester.pumpWidget(new Viewport2(
offset: offset,
children: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 252.0, child: new Text('b')),
]),
),
],
));
verify(tester, <Point>[
const Point(0.0, 0.0),
const Point(0.0, 251.0),
const Point(0.0, 504.0),
], 'acb');
});
}
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