Commit 5928d221 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

ShrinkWrap Viewport (#7790)

parent 2bbc0283
......@@ -40,7 +40,8 @@ class ListDemoState extends State<ListDemo> {
decoration: new BoxDecoration(
border: new Border(top: new BorderSide(color: Colors.black26))
),
child: new Block(
child: new ScrollView(
shrinkWrap: true,
children: <Widget>[
new ListItem(
dense: true,
......
......@@ -285,8 +285,8 @@ class AboutDialog extends StatelessWidget {
if (children != null)
body.addAll(children);
return new AlertDialog(
content: new Block(
children: body
content: new SingleChildScrollView(
child: new BlockBody(children: body),
),
actions: <Widget>[
new FlatButton(
......
......@@ -290,9 +290,9 @@ class SimpleDialog extends StatelessWidget {
if (children != null) {
body.add(new Flexible(
child: new Block(
child: new SingleChildScrollView(
padding: contentPadding ?? const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0),
children: children
child: new BlockBody(children: children),
)
));
}
......
......@@ -292,15 +292,15 @@ class _PopupMenu<T> extends StatelessWidget {
Widget child = new ConstrainedBox(
constraints: const BoxConstraints(
minWidth: _kMenuMinWidth,
maxWidth: _kMenuMaxWidth
maxWidth: _kMenuMaxWidth,
),
child: new IntrinsicWidth(
stepWidth: _kMenuWidthStep,
child: new Block(
children: children,
child: new SingleChildScrollView(
padding: const EdgeInsets.symmetric(
vertical: _kMenuVerticalPadding
)
),
child: new BlockBody(children: children),
)
)
);
......@@ -317,9 +317,9 @@ class _PopupMenu<T> extends StatelessWidget {
alignment: FractionalOffset.topRight,
widthFactor: width.evaluate(route.animation),
heightFactor: height.evaluate(route.animation),
child: child
)
)
child: child,
),
),
);
},
child: child
......
......@@ -542,8 +542,9 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
);
}
return new Block(
children: children
return new ScrollView(
shrinkWrap: true,
children: children,
);
}
......
......@@ -263,7 +263,6 @@ class TwoLevelList extends StatelessWidget {
/// The [type] argument must not be null.
TwoLevelList({
Key key,
this.scrollableKey,
this.children: const <Widget>[],
this.type: MaterialListType.twoLine,
this.padding
......@@ -279,18 +278,15 @@ class TwoLevelList extends StatelessWidget {
/// The kind of [ListItem] contained in this list.
final MaterialListType type;
/// The key to use for the underlying scrollable widget.
final Key scrollableKey;
/// The amount of space by which to inset the children inside the viewport.
final EdgeInsets padding;
@override
Widget build(BuildContext context) {
return new Block(
return new ScrollView(
padding: padding,
shrinkWrap: true,
children: KeyedSubtree.ensureUniqueKeysForList(children),
scrollableKey: scrollableKey
);
}
}
......@@ -198,10 +198,7 @@ class BoxConstraints extends Constraints {
return height.clamp(minHeight, maxHeight);
}
/// Returns the size that both satisfies the constraints and is as close as
/// possible to the given size.
Size constrain(Size size) {
Size result = new Size(constrainWidth(size.width), constrainHeight(size.height));
Size _debugPropagateDebugSize(Size size, Size result) {
assert(() {
if (size is _DebugSize)
result = new _DebugSize(result, size._owner, size._canBeUsedByParent);
......@@ -210,6 +207,26 @@ class BoxConstraints extends Constraints {
return result;
}
/// Returns the size that both satisfies the constraints and is as close as
/// possible to the given size.
///
/// See also [constrainDimensions], which applies the same algorithm to
/// separately provided widths and heights.
Size constrain(Size size) {
Size result = new Size(constrainWidth(size.width), constrainHeight(size.height));
assert(() { result = _debugPropagateDebugSize(size, result); return true; });
return result;
}
/// Returns the size that both satisfies the constraints and is as close as
/// possible to the given width and height.
///
/// When you already have a [Size], prefer [constrain], which applies the same
/// algorithm to a [Size] directly.
Size constrainDimensions(double width, double height) {
return new Size(constrainWidth(width), constrainHeight(height));
}
/// Returns a size that attempts to meet the following conditions, in order:
///
/// - The size must satisfy these constraints.
......@@ -218,8 +235,11 @@ class BoxConstraints extends Constraints {
/// - The returned size as big as possible while still being equal to or
/// smaller than the given size.
Size constrainSizeAndAttemptToPreserveAspectRatio(Size size) {
if (isTight)
return smallest;
if (isTight) {
Size result = smallest;
assert(() { result = _debugPropagateDebugSize(size, result); return true; });
return result;
}
double width = size.width;
double height = size.height;
......@@ -247,7 +267,9 @@ class BoxConstraints extends Constraints {
width = height * aspectRatio;
}
return new Size(constrainWidth(width), constrainHeight(height));
Size result = new Size(constrainWidth(width), constrainHeight(height));
assert(() { result = _debugPropagateDebugSize(size, result); return true; });
return result;
}
/// The biggest size that satisifes the constraints.
......
......@@ -257,6 +257,12 @@ class SliverConstraints extends Constraints {
///
/// The actual number of pixels provided should be specified in the
/// [RenderSliver.geometry] as [SliverGeometry.paintExtent].
///
/// This value may be infinite, for example if the viewport is an
/// unconstrained [RenderShrinkWrappingViewport].
///
/// This value may be 0.0, for example if the sliver is scrolled off the
/// bottom of a downwards vertical viewport.
final double remainingPaintExtent;
/// The number of pixels in the cross-axis. For a vertical list, this is the
......@@ -576,6 +582,8 @@ class SliverLogicalParentData extends ParentData {
String toString() => 'scrollOffset=${scrollOffset.toStringAsFixed(1)}';
}
class SliverLogicalContainerParentData extends SliverLogicalParentData with ContainerParentDataMixin<RenderSliver> { }
/// Parent data structure used by parents of slivers that position their
/// children using absolute coordinates. For example, used by [RenderViewport2].
///
......@@ -740,10 +748,11 @@ abstract class RenderSliver extends RenderObject {
///
/// ## Coordinates for RenderSliver objects
///
/// The `mainAxisPosition` is the distance in the [AxisDirection] from the
/// edge of the sliver's painted area. This can be an unusual direction, for
/// example in the [AxisDirection.up] case this is a distance from the
/// _bottom_ of the sliver's painted area.
/// The `mainAxisPosition` is the distance in the [AxisDirection] (after
/// applying the [GrowthDirection]) from the edge of the sliver's painted
/// area. This can be an unusual direction, for example in the
/// [AxisDirection.up] case this is a distance from the _bottom_ of the
/// sliver's painted area.
///
/// The `crossAxisPosition` is the distance in the other axis. If the cross
/// axis is horizontal (i.e. the [SliverConstraints.axisDirection] is either
......@@ -1129,7 +1138,11 @@ abstract class ViewportOffset extends ChangeNotifier {
///
/// If applying the content dimensions changes the scroll offset, return
/// false. Otherwise, return true. If you return false, the [RenderViewport2]
/// will be laid out again with the new scroll offset. This is expensive.
/// will be laid out again with the new scroll offset. This is expensive. (The
/// return value is answering the question "did you accept these content
/// dimensions unconditionally?"; if the new dimensions change the
/// [ViewportOffset]'s actual [pixels] value, then the viewport will need to
/// be laid out again.)
///
/// This is called at least once each time the [RenderViewport2] is laid out,
/// even if the values have not changed. It may be called many times if the
......@@ -1199,31 +1212,14 @@ class _FixedViewportOffset extends ViewportOffset {
// /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// /// placed inside a [RenderSliver] (the opposite of this class).
class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSliver, SliverPhysicalContainerParentData> {
/// Creates a viewport for [RenderSliver] objects.
///
/// If the [center] is not specified, then the first child in the `children`
/// list, if any, is used.
///
/// The [offset] must be specified. For testing purposes, consider passing a
/// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
RenderViewport2({
abstract class RenderViewportBase2<ParentDataClass extends ContainerParentDataMixin<RenderSliver>> extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass> {
RenderViewportBase2({
AxisDirection axisDirection: AxisDirection.down,
double anchor: 0.0,
@required ViewportOffset offset,
List<RenderSliver> children,
RenderSliver center,
}) : _axisDirection = axisDirection,
_anchor = anchor,
_offset = offset,
_center = center {
assert(offset != null);
_offset = offset {
assert(axisDirection != null);
assert(anchor != null);
assert(anchor >= 0.0 && anchor <= 1.0);
addAll(children);
if (center == null && firstChild != null)
_center = firstChild;
assert(offset != null);
}
AxisDirection get axisDirection => _axisDirection;
......@@ -1238,20 +1234,6 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
Axis get axis => axisDirectionToAxis(axisDirection);
// TODO(ianh): Extract the shrink-wrap logic into a separate viewport class.
bool _shrinkWrap = false;
double get anchor => _anchor;
double _anchor;
set anchor(double value) {
assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (value == _anchor)
return;
_anchor = value;
markNeedsLayout();
}
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
......@@ -1266,47 +1248,14 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
if (attached)
_offset.addListener(markNeedsLayout);
if (hasSize) {
assert(_minScrollExtent != null);
assert(_maxScrollExtent != null);
assert(anchor != null);
// If we already have a size, then we should re-report the dimensions
// to the new ViewportOffset. If we don't then we'll report them when
// we establish the dimensions later, so don't worry about it now.
double effectiveExtent;
switch (axis) {
case Axis.vertical:
effectiveExtent = size.height;
break;
case Axis.horizontal:
effectiveExtent = size.width;
break;
}
assert(effectiveExtent != null);
offset.applyViewportDimension(effectiveExtent);
if (offset.applyContentDimensions(
// when updating this, also update similar code in performLayout()
math.min(0.0, _minScrollExtent + effectiveExtent * anchor),
math.max(0.0, _maxScrollExtent - effectiveExtent * (1.0 - anchor)),
))
markNeedsLayout();
if (!rereportDimensions())
markNeedsLayout();
}
}
RenderSliver get center => _center;
RenderSliver _center;
set center(RenderSliver value) {
if (value == _center)
return;
_center = value;
markNeedsLayout();
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData)
child.parentData = new SliverPhysicalContainerParentData();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
......@@ -1322,181 +1271,8 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
@override
bool get isRepaintBoundary => true;
@override
bool get sizedByParent => !_shrinkWrap;
@override
void performResize() {
assert(!_shrinkWrap);
assert(constraints.hasBoundedHeight && constraints.hasBoundedWidth);
size = constraints.biggest;
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
// Out-of-band data computed during layout.
double _minScrollExtent;
double _maxScrollExtent;
double _shrinkWrapExtent;
bool _hasVisualOverflow = false;
@override
void performLayout() {
assert(!_shrinkWrap || anchor == 0.0);
if (center == null) {
assert(firstChild == null);
if (_shrinkWrap) {
switch (axis) {
case Axis.vertical:
size = new Size(constraints.maxWidth, 0.0);
break;
case Axis.horizontal:
size = new Size(0.0, constraints.maxHeight);
break;
}
offset.applyViewportDimension(0.0);
}
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0);
return;
}
assert(center.parent == this);
double extent;
double crossExtent;
if (_shrinkWrap) {
switch (axis) {
case Axis.vertical:
assert(constraints.hasBoundedWidth);
extent = constraints.maxHeight;
crossExtent = constraints.maxWidth;
break;
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
extent = constraints.maxWidth;
crossExtent = constraints.maxHeight;
break;
}
} else {
assert(constraints.hasBoundedHeight && constraints.hasBoundedWidth);
switch (axis) {
case Axis.vertical:
extent = size.height;
crossExtent = size.width;
break;
case Axis.horizontal:
extent = size.width;
crossExtent = size.height;
break;
}
}
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
double correction;
double effectiveExtent;
do {
assert(offset.pixels != null);
correction = _attemptLayout(extent, crossExtent, offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
if (_shrinkWrap) {
switch (axis) {
case Axis.vertical:
effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
break;
case Axis.horizontal:
effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
break;
}
offset.applyViewportDimension(effectiveExtent);
} else {
switch (axis) {
case Axis.vertical:
effectiveExtent = size.height;
break;
case Axis.horizontal:
effectiveExtent = size.width;
break;
}
}
// when updating this, also update similar code in offset setter
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + effectiveExtent * anchor),
math.max(0.0, _maxScrollExtent - effectiveExtent * (1.0 - anchor))
))
break;
}
} while (true);
assert(_shrinkWrap != sizedByParent);
if (_shrinkWrap) {
switch (axis) {
case Axis.vertical:
size = new Size(crossExtent, effectiveExtent);
break;
case Axis.horizontal:
size = new Size(effectiveExtent, crossExtent);
break;
}
}
}
double _attemptLayout(double extent, double crossExtent, double correctedOffset) {
assert(!extent.isNaN);
assert(extent >= 0.0);
assert(crossExtent.isFinite);
assert(crossExtent >= 0.0);
assert(correctedOffset.isFinite);
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
// centerOffset is the offset from the leading edge of the RenderViewport2
// to the zero scroll offset (the line between the forward slivers and the
// reverse slivers). The other two are that, but clamped to the visible
// region of the viewport.
final double centerOffset = extent * anchor - correctedOffset;
final double clampedForwardCenter = math.max(0.0, math.min(extent, centerOffset));
final double clampedReverseCenter = math.max(0.0, math.min(extent, extent - centerOffset));
// negative scroll offsets
double result = _layoutOneSide(
childBefore(center),
math.max(extent, extent * anchor - correctedOffset) - extent,
clampedReverseCenter,
clampedForwardCenter,
crossExtent,
GrowthDirection.reverse,
childBefore,
);
if (result != 0.0)
return -result;
// positive scroll offsets
return _layoutOneSide(
center,
math.max(0.0, correctedOffset - extent * anchor),
clampedForwardCenter,
clampedReverseCenter,
crossExtent,
GrowthDirection.forward,
childAfter,
);
}
double _layoutOneSide(
@protected
double layoutOneSide(
RenderSliver child,
double scrollOffset,
double layoutOffset,
......@@ -1540,10 +1316,8 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
crossAxisExtent: crossAxisExtent,
), parentUsesSize: true);
// collect the child's objects
final SliverGeometry childLayoutGeometry = child.geometry;
final SliverPhysicalParentData childParentData = child.parentData;
assert(childLayoutGeometry.debugAssertIsValid);
......@@ -1553,7 +1327,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
return childLayoutGeometry.scrollOffsetCorrection;
// geometry
childParentData.paintOffset = _computeAbsolutePaintOffset(child, layoutOffset, growthDirection);
updateChildPaintOffset(child, layoutOffset, growthDirection);
maxPaintOffset = math.max(layoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
scrollOffset -= childLayoutGeometry.scrollExtent;
layoutOffset += childLayoutGeometry.layoutExtent;
......@@ -1561,22 +1335,9 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
if (scrollOffset <= 0.0)
scrollOffset = 0.0;
// out-of-band data mutation
switch (growthDirection) {
case GrowthDirection.forward:
_maxScrollExtent += childLayoutGeometry.scrollExtent;
break;
case GrowthDirection.reverse:
_minScrollExtent -= childLayoutGeometry.scrollExtent;
break;
}
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
if (childLayoutGeometry.hasVisualOverflow)
_hasVisualOverflow = true;
updateOutOfBoundsData(growthDirection, childLayoutGeometry);
// move on to the next child
assert(child.parentData == childParentData);
child = advance(child);
}
......@@ -1584,65 +1345,22 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
return 0.0;
}
Offset _computeAbsolutePaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection) {
assert(axisDirection != null);
assert(growthDirection != null);
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up:
return new Offset(0.0, size.height - (paintOffset + child.geometry.paintExtent));
case AxisDirection.right:
return new Offset(paintOffset, 0.0);
case AxisDirection.down:
return new Offset(0.0, paintOffset);
case AxisDirection.left:
return new Offset(size.width - (paintOffset + child.geometry.paintExtent), 0.0);
}
return null;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
assert(child.parent == this);
final SliverPhysicalParentData childParentData = child.parentData;
childParentData.applyPaintTransform(transform);
}
void _paintContents(PaintingContext context, Offset offset) {
assert(center.parent == this);
assert(firstChild != null);
RenderSliver child = firstChild;
while (child != center) {
if (child.geometry.visible) {
final SliverPhysicalParentData childParentData = child.parentData;
context.paintChild(child, offset + childParentData.paintOffset);
}
child = childAfter(child);
}
child = lastChild;
while (true) {
if (child.geometry.visible) {
final SliverPhysicalParentData childParentData = child.parentData;
context.paintChild(child, offset + childParentData.paintOffset);
}
if (child == center)
break;
child = childBefore(child);
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (center == null) {
assert(firstChild == null);
if (firstChild == null)
return;
if (hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
} else {
paintContents(context, offset);
}
}
if (_hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
} else {
_paintContents(context, offset);
@protected
void paintContents(PaintingContext context, Offset offset) {
for (RenderSliver child in childrenInPaintOrder) {
if (child.geometry.visible)
context.paintChild(child, offset + paintOffsetOf(child));
}
}
......@@ -1657,7 +1375,6 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
final Canvas canvas = context.canvas;
RenderSliver child = firstChild;
while (child != null) {
final SliverPhysicalParentData childParentData = child.parentData;
Size size;
switch (axis) {
case Axis.vertical:
......@@ -1668,7 +1385,7 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
break;
}
assert(size != null);
canvas.drawRect(((offset + childParentData.paintOffset) & size).deflate(0.5), paint);
canvas.drawRect(((offset + paintOffsetOf(child)) & size).deflate(0.5), paint);
child = childAfter(child);
}
return true;
......@@ -1677,63 +1394,49 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
@override
bool hitTestChildren(HitTestResult result, { Point position }) {
if (center == null) {
assert(firstChild == null);
return false;
}
assert(center.parent == this);
assert(firstChild != null);
double crossAxisPosition, mainAxisPosition;
double mainAxisPosition, crossAxisPosition;
switch (axis) {
case Axis.vertical:
crossAxisPosition = position.x;
mainAxisPosition = position.y;
crossAxisPosition = position.x;
break;
case Axis.horizontal:
crossAxisPosition = position.y;
mainAxisPosition = position.x;
crossAxisPosition = position.y;
break;
}
assert(mainAxisPosition != null);
assert(crossAxisPosition != null);
RenderSliver child;
child = center;
while (child != null) {
if (child.geometry.visible && child.hitTest(
result,
mainAxisPosition: _computeChildMainAxisPosition(child, mainAxisPosition),
crossAxisPosition: crossAxisPosition
)) {
return true;
}
child = childAfter(child);
}
child = childBefore(center);
while (child != null) {
for (RenderSliver child in childrenInHitTestOrder) {
if (child.geometry.visible && child.hitTest(
result,
mainAxisPosition: _computeChildMainAxisPosition(child, mainAxisPosition),
mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
crossAxisPosition: crossAxisPosition
)) {
return true;
}
child = childBefore(child);
}
return false;
}
double _computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
final SliverPhysicalParentData childParentData = child.parentData;
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
@protected
Offset computeAbsolutePaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection) {
assert(hasSize); // this is only usable once we have a size
assert(axisDirection != null);
assert(growthDirection != null);
assert(child != null);
assert(child.geometry != null);
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
case AxisDirection.up:
return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dy);
return new Offset(0.0, size.height - (paintOffset + child.geometry.paintExtent));
case AxisDirection.right:
return parentMainAxisPosition - childParentData.paintOffset.dx;
return new Offset(paintOffset, 0.0);
case AxisDirection.down:
return parentMainAxisPosition - childParentData.paintOffset.dy;
return new Offset(0.0, paintOffset);
case AxisDirection.left:
return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dx);
return new Offset(size.width - (paintOffset + child.geometry.paintExtent), 0.0);
}
return 0.0;
return null;
}
// TODO(ianh): semantics - shouldn't walk the invisible children
......@@ -1742,46 +1445,646 @@ class RenderViewport2 extends RenderBox with ContainerRenderObjectMixin<RenderSl
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
if (_shrinkWrap)
description.add('shrink-wrap enabled');
description.add('anchor: $anchor');
description.add('offset: $offset');
}
@override
String debugDescribeChildren(String prefix) {
if (firstChild == null)
return '$prefix\n';
int count = indexOfFirstChild();
String result = '$prefix \u2502\n';
RenderSliver child = firstChild;
while (child != lastChild) {
result += '${child.toStringDeep("$prefix \u251C\u2500${labelForChild(count)}: ", "$prefix \u2502")}';
count += 1;
child = childAfter(child);
}
assert(child == lastChild);
result += '${child.toStringDeep("$prefix \u2514\u2500${labelForChild(count)}: ", "$prefix ")}';
return result;
}
// API TO BE IMPLEMENTED BY SUBCLASSES
// setupParentData
// performLayout (and optionally sizedByParent and performResize)
@protected
bool get hasVisualOverflow;
@protected
void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry);
@protected
void updateChildPaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection);
@protected
Offset paintOffsetOf(RenderSliver child);
// applyPaintTransform
/// Converts the `parentMainAxisPosition` into the child's coordinate system.
///
/// The `parentMainAxisPosition` is a distance from the top edge (for vertical
/// viewports) or left edge (for horizontal viewports) of the viewport bounds.
/// This describes a line, perpendicular to the viewport's main axis, heretofor
/// known as the target line.
///
/// The child's coordinate system's origin in the main axis is at the leading
/// edge of the given child, as given by the child's
/// [SliverConstraints.axisDirection] and [SliverConstraints.growthDirection].
///
/// This method returns the distance from the leading edge of the given child to
/// the target line described above.
///
/// (The `parentMainAxisPosition` is not from the leading edge of the
/// viewport, it's always the top or left edge.)
@protected
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition);
/// Notifies the [offset] of the render object's new dimensions.
///
/// Implementations must call [ViewportOffset.applyViewportDimension] and
/// [ViewportOffset.applyContentDimensions], in that order.
///
/// Return the value returned by [ViewportOffset.applyContentDimensions]:
/// false if the [ViewportOffset] did not accept the new dimensions
/// unconditionally, but instead wants to relayout the render object (e.g.
/// because it changed its mind about the actual offset that the viewport
/// should be laid out at), and true if the [ViewportOffset] was happy with
/// the given dimensions and does not think that anything need change. If this
/// function returns false, the caller will call [markNeedsLayout].
@protected
bool rereportDimensions();
@protected
int indexOfFirstChild();
@protected
String labelForChild(int index);
/// Provides an iterable that walks the children of the viewport, in the order
/// that they should be painted.
///
/// This should be the reverse order of [childrenInHitTestOrder].
@protected
Iterable<RenderSliver> get childrenInPaintOrder;
/// Provides an iterable that walks the children of the viewport, in the order
/// that hit-testing should use.
///
/// This should be the reverse order of [childrenInPaintOrder].
@protected
Iterable<RenderSliver> get childrenInHitTestOrder;
}
// ///
// /// See also:
// ///
// /// - [RenderSliver], which explains more about the Sliver protocol.
// /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// /// placed inside a [RenderSliver] (the opposite of this class).
// /// - [RenderShrinkWrappingViewport], a variant of [RenderViewport2] that
// /// shrink-wraps its contents along the main axis.
class RenderViewport2 extends RenderViewportBase2<SliverPhysicalContainerParentData> {
/// Creates a viewport for [RenderSliver] objects.
///
/// If the [center] is not specified, then the first child in the `children`
/// list, if any, is used.
///
/// The [offset] must be specified. For testing purposes, consider passing a
/// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
RenderViewport2({
AxisDirection axisDirection: AxisDirection.down,
@required ViewportOffset offset,
double anchor: 0.0,
List<RenderSliver> children,
RenderSliver center,
}) : _anchor = anchor,
_center = center,
super(axisDirection: axisDirection, offset: offset) {
assert(anchor != null);
assert(anchor >= 0.0 && anchor <= 1.0);
addAll(children);
if (center == null && firstChild != null)
_center = firstChild;
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalContainerParentData)
child.parentData = new SliverPhysicalContainerParentData();
}
double get anchor => _anchor;
double _anchor;
set anchor(double value) {
assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (value == _anchor)
return;
_anchor = value;
markNeedsLayout();
}
RenderSliver get center => _center;
RenderSliver _center;
set center(RenderSliver value) {
if (value == _center)
return;
_center = value;
markNeedsLayout();
}
@override
bool get sizedByParent => true;
@override
void performResize() {
assert(constraints.hasBoundedHeight && constraints.hasBoundedWidth);
size = constraints.biggest;
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
static const int _kMaxLayoutCycles = 10;
// Out-of-band data computed during layout.
double _minScrollExtent;
double _maxScrollExtent;
bool _hasVisualOverflow = false;
@override
void performLayout() {
if (center == null) {
assert(firstChild == null);
return result;
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0);
return;
}
assert(center.parent == this);
assert(firstChild != null);
double mainAxisExtent;
double crossAxisExtent;
switch (axis) {
case Axis.vertical:
mainAxisExtent = size.height;
crossAxisExtent = size.width;
break;
case Axis.horizontal:
mainAxisExtent = size.width;
crossAxisExtent = size.height;
break;
}
final double centerOffsetAdjustment = center.centerOffsetAdjustment;
double correction;
int count = 0;
do {
assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
// when updating this, also update rereportDimensions
if (offset.applyContentDimensions(
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
))
break;
}
count += 1;
} while (count < _kMaxLayoutCycles);
assert(() {
if (count >= _kMaxLayoutCycles) {
assert(count != 1);
throw new FlutterError(
'A RenderViewport2 exceeded its maximum number of layout cycles.\n'
'RenderViewport2 render objects, during layout, can retry if either their '
'slivers or their ViewportOffset decide that the offset should be corrected '
'to take into account information collected during that layout.\n'
'In the case of this RenderViewport2 object, however, this happened $count '
'times and still there was no consensus on the scroll offset. This usually '
'indicates a bug. Specifically, it means that one of the following three '
'problems is being experienced by the RenderViewport2 object:\n'
' * One of the RenderSliver children or the ViewportOffset have a bug such'
' that they always think that they need to correct the offset regardless.\n'
' * Some combination of the RenderSliver children and the ViewportOffset'
' have a bad interaction such that one applies a correction then another'
' applies a reverse correction, leading to an infinite loop of corrections.\n'
' * There is a pathological case that would eventually resolve, but it is'
' so complicated that it cannot be resolved in any reasonable number of'
' layout passes.'
);
}
return true;
});
}
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
assert(!mainAxisExtent.isNaN);
assert(mainAxisExtent >= 0.0);
assert(crossAxisExtent.isFinite);
assert(crossAxisExtent >= 0.0);
assert(correctedOffset.isFinite);
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
// centerOffset is the offset from the leading edge of the RenderViewport2
// to the zero scroll offset (the line between the forward slivers and the
// reverse slivers). The other two are that, but clamped to the visible
// region of the viewport.
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
final double clampedForwardCenter = math.max(0.0, math.min(mainAxisExtent, centerOffset));
final double clampedReverseCenter = math.max(0.0, math.min(mainAxisExtent, mainAxisExtent - centerOffset));
// negative scroll offsets
double result = layoutOneSide(
childBefore(center),
math.max(mainAxisExtent, mainAxisExtent * anchor - correctedOffset) - mainAxisExtent,
clampedReverseCenter,
clampedForwardCenter,
crossAxisExtent,
GrowthDirection.reverse,
childBefore,
);
if (result != 0.0)
return -result;
// positive scroll offsets
return layoutOneSide(
center,
math.max(0.0, correctedOffset - mainAxisExtent * anchor),
clampedForwardCenter,
clampedReverseCenter,
crossAxisExtent,
GrowthDirection.forward,
childAfter,
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
switch (growthDirection) {
case GrowthDirection.forward:
_maxScrollExtent += childLayoutGeometry.scrollExtent;
break;
case GrowthDirection.reverse:
_minScrollExtent -= childLayoutGeometry.scrollExtent;
break;
}
if (childLayoutGeometry.hasVisualOverflow)
_hasVisualOverflow = true;
}
@override
void updateChildPaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection) {
final SliverPhysicalParentData childParentData = child.parentData;
childParentData.paintOffset = computeAbsolutePaintOffset(child, paintOffset, growthDirection);
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverPhysicalParentData childParentData = child.parentData;
return childParentData.paintOffset;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
final SliverPhysicalParentData childParentData = child.parentData;
childParentData.applyPaintTransform(transform);
}
@override
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
assert(child != null);
assert(child.constraints != null);
final SliverPhysicalParentData childParentData = child.parentData;
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
case AxisDirection.down:
return parentMainAxisPosition - childParentData.paintOffset.dy;
case AxisDirection.right:
return parentMainAxisPosition - childParentData.paintOffset.dx;
case AxisDirection.up:
return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dy);
case AxisDirection.left:
return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dx);
}
return 0.0;
}
@override
bool rereportDimensions() {
assert(_minScrollExtent != null);
assert(_maxScrollExtent != null);
assert(anchor != null);
double effectiveExtent;
switch (axis) {
case Axis.vertical:
effectiveExtent = size.height;
break;
case Axis.horizontal:
effectiveExtent = size.width;
break;
}
assert(effectiveExtent != null);
offset.applyViewportDimension(effectiveExtent);
return offset.applyContentDimensions(
// when updating this, also update similar code in performLayout()
math.min(0.0, _minScrollExtent + effectiveExtent * anchor),
math.max(0.0, _maxScrollExtent - effectiveExtent * (1.0 - anchor)),
);
}
@override
int indexOfFirstChild() {
assert(center != null);
assert(center.parent == this);
assert(firstChild != null);
int count = 0;
RenderSliver child = center;
while (child != firstChild) {
count -= 1;
child = childBefore(child);
}
return count;
}
child = firstChild;
while (child != lastChild) {
result += '${child.toStringDeep("$prefix \u251C\u2500${_labelChild(count)}: ", "$prefix \u2502")}';
count += 1;
@override
String labelForChild(int index) {
if (index == 0)
return 'center child';
return 'child $index';
}
@override
Iterable<RenderSliver> get childrenInPaintOrder sync* {
if (firstChild == null)
return;
RenderSliver child = firstChild;
while (child != center) {
yield child;
child = childAfter(child);
}
child = lastChild;
while (true) {
yield child;
if (child == center)
return;
child = childBefore(child);
}
}
@override
Iterable<RenderSliver> get childrenInHitTestOrder sync* {
if (firstChild == null)
return;
RenderSliver child = center;
while (child != null) {
yield child;
child = childAfter(child);
}
if (child != null) {
assert(child == lastChild);
result += '${child.toStringDeep("$prefix \u2514\u2500${_labelChild(count)}: ", "$prefix ")}';
child = childBefore(center);
while (child != null) {
yield child;
child = childBefore(child);
}
return result;
}
static String _labelChild(int count) {
if (count == 0)
return 'center child';
return 'child $count';
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('anchor: $anchor');
}
}
// ///
// /// See also:
// ///
// /// - [RenderViewport2], a viewport that does not shrink-wrap its contents
// /// - [RenderSliver], which explains more about the Sliver protocol.
// /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// /// placed inside a [RenderSliver] (the opposite of this class).
class RenderShrinkWrappingViewport extends RenderViewportBase2<SliverLogicalContainerParentData> {
/// Creates a viewport (for [RenderSliver] objects) that shrink-wraps its
/// contents.
///
/// The [offset] must be specified. For testing purposes, consider passing a
/// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
RenderShrinkWrappingViewport({
AxisDirection axisDirection: AxisDirection.down,
@required ViewportOffset offset,
List<RenderSliver> children,
}) : super(axisDirection: axisDirection, offset: offset) {
addAll(children);
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverLogicalContainerParentData)
child.parentData = new SliverLogicalContainerParentData();
}
// Out-of-band data computed during layout.
double _maxScrollExtent;
double _shrinkWrapExtent;
bool _hasVisualOverflow = false;
@override
void performLayout() {
if (firstChild == null) {
switch (axis) {
case Axis.vertical:
assert(constraints.hasBoundedWidth);
size = new Size(constraints.maxWidth, constraints.minHeight);
break;
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
size = new Size(constraints.minWidth, constraints.maxHeight);
break;
}
offset.applyViewportDimension(0.0);
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0.0, 0.0);
return;
}
double mainAxisExtent;
double crossAxisExtent;
switch (axis) {
case Axis.vertical:
assert(constraints.hasBoundedWidth);
mainAxisExtent = constraints.maxHeight;
crossAxisExtent = constraints.maxWidth;
break;
case Axis.horizontal:
assert(constraints.hasBoundedHeight);
mainAxisExtent = constraints.maxWidth;
crossAxisExtent = constraints.maxHeight;
break;
}
double correction;
double effectiveExtent;
do {
assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
switch (axis) {
case Axis.vertical:
effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
break;
case Axis.horizontal:
effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
break;
}
offset.applyViewportDimension(effectiveExtent);
// when updating this, also update similar code in rereportDimensions
if (offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent)))
break;
}
} while (true);
switch (axis) {
case Axis.vertical:
size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
break;
case Axis.horizontal:
size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
break;
}
}
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
assert(!mainAxisExtent.isNaN);
assert(mainAxisExtent >= 0.0);
assert(crossAxisExtent.isFinite);
assert(crossAxisExtent >= 0.0);
assert(correctedOffset.isFinite);
_maxScrollExtent = 0.0;
_shrinkWrapExtent = 0.0;
_hasVisualOverflow = false;
return layoutOneSide(
firstChild,
math.max(0.0, correctedOffset),
0.0,
mainAxisExtent,
crossAxisExtent,
GrowthDirection.forward,
childAfter,
);
}
@override
bool get hasVisualOverflow => _hasVisualOverflow;
@override
void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
assert(growthDirection == GrowthDirection.forward);
_maxScrollExtent += childLayoutGeometry.scrollExtent;
if (childLayoutGeometry.hasVisualOverflow)
_hasVisualOverflow = true;
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
}
@override
void updateChildPaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection) {
assert(growthDirection == GrowthDirection.forward);
final SliverLogicalParentData childParentData = child.parentData;
childParentData.scrollOffset = paintOffset;
}
@override
Offset paintOffsetOf(RenderSliver child) {
final SliverLogicalParentData childParentData = child.parentData;
return computeAbsolutePaintOffset(child, childParentData.scrollOffset, GrowthDirection.forward);
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
final Offset offset = paintOffsetOf(child);
transform.translate(offset.dx, offset.dy);
}
@override
double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
assert(child != null);
assert(child.constraints != null);
assert(hasSize);
final SliverLogicalParentData childParentData = child.parentData;
switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
case AxisDirection.down:
case AxisDirection.right:
return parentMainAxisPosition - childParentData.scrollOffset;
case AxisDirection.up:
return (size.height - parentMainAxisPosition) - childParentData.scrollOffset;
case AxisDirection.left:
return (size.width - parentMainAxisPosition) - childParentData.scrollOffset;
}
return 0.0;
}
@override
bool rereportDimensions() {
assert(_maxScrollExtent != null);
double effectiveExtent;
switch (axis) {
case Axis.vertical:
effectiveExtent = size.height;
break;
case Axis.horizontal:
effectiveExtent = size.width;
break;
}
assert(effectiveExtent != null);
offset.applyViewportDimension(effectiveExtent);
return offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent));
}
@override
int indexOfFirstChild() => 0;
@override
String labelForChild(int index) => 'child $index';
@override
Iterable<RenderSliver> get childrenInPaintOrder sync* {
RenderSliver child = firstChild;
while (child != null) {
yield child;
child = childAfter(child);
}
}
@override
Iterable<RenderSliver> get childrenInHitTestOrder sync* {
RenderSliver child = lastChild;
while (child != null) {
yield child;
child = childBefore(child);
}
}
}
......
......@@ -9,136 +9,160 @@ import 'framework.dart';
import 'basic.dart';
import 'scrollable.dart';
import 'sliver.dart';
import 'viewport.dart';
AxisDirection _getDirection(BuildContext context, Axis scrollDirection) {
// TODO(abarth): Consider reading direction.
switch (scrollDirection) {
case Axis.horizontal:
return AxisDirection.right;
case Axis.vertical:
return AxisDirection.down;
}
return null;
}
/// A convenience widget that combines common scrolling-related widgets.
class ScrollView extends StatelessWidget {
ScrollView({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.reverse: false,
this.padding,
this.initialScrollOffset: 0.0,
this.itemExtent,
this.shrinkWrap: false,
this.children: const <Widget>[],
}) : super(key: key);
final EdgeInsets padding;
}) : super(key: key) {
assert(reverse != null);
assert(initialScrollOffset != null);
assert(shrinkWrap != null);
}
final Axis scrollDirection;
final bool reverse;
final EdgeInsets padding;
final double initialScrollOffset;
final double itemExtent;
final bool shrinkWrap;
final List<Widget> children;
Widget _buildChildLayout() {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
SliverChildListDelegate get childrenDelegate => new SliverChildListDelegate(children);
@protected
AxisDirection getDirection(BuildContext context) {
// TODO(abarth): Consider reading direction.
switch (scrollDirection) {
case Axis.horizontal:
return reverse ? AxisDirection.left : AxisDirection.right;
case Axis.vertical:
return reverse ? AxisDirection.up : AxisDirection.down;
}
return null;
}
@protected
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return new SliverList(
delegate: delegate,
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return new SliverBlock(delegate: delegate);
return new SliverBlock(delegate: childrenDelegate);
}
@override
Widget build(BuildContext context) {
Widget sliver = _buildChildLayout();
Widget sliver = buildChildLayout(context);
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
return new ScrollableViewport2(
axisDirection: _getDirection(context, scrollDirection),
AxisDirection axisDirection = getDirection(context);
return new Scrollable2(
axisDirection: axisDirection,
initialScrollOffset: initialScrollOffset,
slivers: <Widget>[ sliver ],
viewportBuilder: (BuildContext context, ViewportOffset offset) {
if (shrinkWrap) {
return new ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: <Widget>[ sliver ],
);
} else {
return new Viewport2(
axisDirection: axisDirection,
offset: offset,
slivers: <Widget>[ sliver ],
);
}
}
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$scrollDirection');
if (padding != null)
description.add('padding: $padding');
if (initialScrollOffset != 0.0)
description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}');
if (itemExtent != null)
description.add('itemExtent: $itemExtent');
if (shrinkWrap)
description.add('shrink-wrapping');
}
}
class ScrollGrid extends StatelessWidget {
class ScrollGrid extends ScrollView {
ScrollGrid({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.initialScrollOffset: 0.0,
Axis scrollDirection: Axis.vertical,
EdgeInsets padding,
double initialScrollOffset: 0.0,
bool shrinkWrap: false,
this.gridDelegate,
this.children: const <Widget>[],
}) : super(key: key);
List<Widget> children: const <Widget>[],
}) : super(key: key, scrollDirection: scrollDirection, padding: padding, shrinkWrap: shrinkWrap, children: children);
ScrollGrid.count({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.initialScrollOffset: 0.0,
Axis scrollDirection: Axis.vertical,
EdgeInsets padding,
double initialScrollOffset: 0.0,
bool shrinkWrap: false,
@required int crossAxisCount,
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
this.children: const <Widget>[],
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
), super(key: key);
), super(key: key, scrollDirection: scrollDirection, padding: padding, shrinkWrap: shrinkWrap, children: children);
ScrollGrid.extent({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.initialScrollOffset: 0.0,
Axis scrollDirection: Axis.vertical,
EdgeInsets padding,
double initialScrollOffset: 0.0,
bool shrinkWrap: false,
@required double maxCrossAxisExtent,
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
this.children: const <Widget>[],
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
), super(key: key);
final EdgeInsets padding;
final Axis scrollDirection;
final double initialScrollOffset;
), super(key: key, scrollDirection: scrollDirection, padding: padding, shrinkWrap: shrinkWrap, children: children);
final SliverGridDelegate gridDelegate;
final List<Widget> children;
@override
Widget build(BuildContext context) {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
Widget sliver = new SliverGrid(
delegate: delegate,
Widget buildChildLayout(BuildContext context) {
return new SliverGrid(
delegate: childrenDelegate,
gridDelegate: gridDelegate,
);
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
return new ScrollableViewport2(
axisDirection: _getDirection(context, scrollDirection),
initialScrollOffset: initialScrollOffset,
slivers: <Widget>[ sliver ],
);
}
}
......@@ -396,64 +396,6 @@ class ScrollConfiguration2 extends InheritedWidget {
}
}
class ScrollableViewport2 extends StatelessWidget {
ScrollableViewport2({
Key key,
this.initialScrollOffset: 0.0,
this.axisDirection: AxisDirection.down,
this.anchor: 0.0,
this.center,
this.scrollBehavior,
this.slivers: const <Widget>[],
}) {
assert(slivers != null);
}
final double initialScrollOffset;
final AxisDirection axisDirection;
final double anchor;
final Key center;
final ScrollBehavior2 scrollBehavior;
final List<Widget> slivers;
Axis get axis => axisDirectionToAxis(axisDirection);
@override
Widget build(BuildContext context) {
return new Scrollable2(
initialScrollOffset: initialScrollOffset,
axisDirection: axisDirection,
scrollBehavior: scrollBehavior,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport2(
axisDirection: axisDirection,
anchor: anchor,
offset: offset,
center: center,
slivers: slivers,
);
}
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
if (anchor != 0.0)
description.add('anchor: ${anchor.toStringAsFixed(1)}');
if (initialScrollOffset != 0.0)
description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}');
if (center != null)
description.add('center: $center');
}
}
typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position);
class Scrollable2 extends StatefulWidget {
......
......@@ -10,10 +10,18 @@ import 'basic.dart';
import 'framework.dart';
import 'scrollable.dart';
// ///
// /// The viewport will shrink-wrap the child in both axes.
// ///
// /// See also:
// /// * [ScrollView], which handles multiple children in a scrolling list.
// /// * [ScrollGrid], which handles multiple children in a scrolling grid.
// /// * [Scrollable2], which handles arbitrary scrolling effects.
class SingleChildScrollView extends StatelessWidget {
SingleChildScrollView({
Key key,
this.scrollDirection: Axis.vertical,
this.padding,
this.initialScrollOffset: 0.0,
this.child,
}) : super(key: key) {
......@@ -23,6 +31,8 @@ class SingleChildScrollView extends StatelessWidget {
final Axis scrollDirection;
final EdgeInsets padding;
final double initialScrollOffset;
final Widget child;
......@@ -41,6 +51,9 @@ class SingleChildScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
Widget contents = child;
if (padding != null)
contents = new Padding(padding: padding, child: contents);
return new Scrollable2(
axisDirection: axisDirection,
initialScrollOffset: initialScrollOffset,
......@@ -49,7 +62,7 @@ class SingleChildScrollView extends StatelessWidget {
key: key,
axisDirection: axisDirection,
offset: offset,
child: child,
child: contents,
);
},
);
......
......@@ -98,3 +98,39 @@ class Viewport2Element extends MultiChildRenderObjectElement {
}
}
}
class ShrinkWrappingViewport extends MultiChildRenderObjectWidget {
ShrinkWrappingViewport({
Key key,
this.axisDirection: AxisDirection.down,
@required this.offset,
List<Widget> slivers: const <Widget>[],
}) : super(key: key, children: slivers) {
assert(offset != null);
}
final AxisDirection axisDirection;
final ViewportOffset offset;
@override
RenderShrinkWrappingViewport createRenderObject(BuildContext context) {
return new RenderShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
);
}
@override
void updateRenderObject(BuildContext context, RenderShrinkWrappingViewport renderObject) {
renderObject
..axisDirection = axisDirection
..offset = offset;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('offset: $offset');
}
}
......@@ -34,7 +34,6 @@ void main() {
)
)
);
await tester.tap(find.text('Step 2'));
expect(index, 1);
});
......@@ -277,8 +276,8 @@ void main() {
)
);
ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
expect(scrollableState.scrollOffset, 0.0);
Scrollable2State scrollableState = tester.firstState(find.byType(Scrollable2));
expect(scrollableState.position.pixels, 0.0);
await tester.tap(find.text('Step 3'));
await tester.pumpWidget(
......@@ -313,8 +312,10 @@ void main() {
);
await tester.pump(const Duration(milliseconds: 100));
expect(scrollableState.scrollOffset, greaterThan(0.0));
});
expect(scrollableState.position.pixels, greaterThan(0.0));
}, skip: Scrollable == Scrollable &&
ScrollableViewport == ScrollableViewport &&
Block == Block); // TODO(abarth): re-enable when ensureVisible is implemented
testWidgets('Stepper index test', (WidgetTester tester) async {
await tester.pumpWidget(
......
......@@ -134,4 +134,7 @@ class RenderSizedBox extends RenderBox {
@override
void performLayout() { }
@override
bool hitTestSelf(Point position) => true;
}
......@@ -40,6 +40,12 @@ void main() {
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(a.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 400.0));
expect(b.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 800.0));
expect(c.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 1000.0));
expect(d.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 1000.0));
expect(e.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 1000.0));
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -200.0));
......@@ -63,6 +69,10 @@ void main() {
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(130.0, 150.0));
expect(result.path.first.target, equals(c));
});
test('RenderViewport2 basic test - up', () {
......@@ -112,6 +122,10 @@ void main() {
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(150.0, 350.0));
expect(result.path.first.target, equals(c));
});
test('RenderViewport2 basic test - right', () {
......@@ -161,6 +175,10 @@ void main() {
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(-100.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(300.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(700.0, 0.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(150.0, 450.0));
expect(result.path.first.target, equals(c));
});
test('RenderViewport2 basic test - left', () {
......@@ -210,12 +228,278 @@ void main() {
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(500.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(100.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-300.0, 0.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(550.0, 150.0));
expect(result.path.first.target, equals(c));
});
// TODO(ianh): test anchor
// TODO(ianh): test offset
// TODO(ianh): test center
// TODO(ianh): test hit testing
// TODO(ianh): test semantics
test('RenderShrinkWrappingViewport basic test - no children', () {
RenderShrinkWrappingViewport root = new RenderShrinkWrappingViewport(
offset: new ViewportOffset.zero(),
);
layout(root);
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
});
test('RenderShrinkWrappingViewport basic test - down', () {
RenderBox a, b, c, d, e;
RenderShrinkWrappingViewport root = new RenderShrinkWrappingViewport(
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
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: c = new RenderSizedBox(const Size(100.0, 400.0))),
new RenderSliverToBoxAdapter(child: d = new RenderSizedBox(const Size(100.0, 400.0))),
new RenderSliverToBoxAdapter(child: 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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(a.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 400.0));
expect(b.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 800.0));
expect(c.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 1000.0));
expect(d.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 1000.0));
expect(e.localToGlobal(const Point(800.0, 400.0)), const Point(800.0, 1000.0));
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
root.offset = new ViewportOffset.fixed(600.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -600.0));
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -900.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -500.0));
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 600.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(130.0, 150.0));
expect(result.path.first.target, equals(c));
});
test('RenderShrinkWrappingViewport basic test - up', () {
RenderBox a, b, c, d, e;
RenderShrinkWrappingViewport root = new RenderShrinkWrappingViewport(
axisDirection: AxisDirection.up,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
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: c = new RenderSizedBox(const Size(100.0, 400.0))),
new RenderSliverToBoxAdapter(child: d = new RenderSizedBox(const Size(100.0, 400.0))),
new RenderSliverToBoxAdapter(child: 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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
root.offset = new ViewportOffset.fixed(600.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 800.0));
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 1100.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 700.0));
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.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, -400.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(150.0, 350.0));
expect(result.path.first.target, equals(c));
});
test('RenderShrinkWrappingViewport basic test - right', () {
RenderBox a, b, c, d, e;
RenderShrinkWrappingViewport root = new RenderShrinkWrappingViewport(
axisDirection: AxisDirection.right,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
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: c = new RenderSizedBox(const Size(400.0, 100.0))),
new RenderSliverToBoxAdapter(child: d = new RenderSizedBox(const Size(400.0, 100.0))),
new RenderSliverToBoxAdapter(child: e = new RenderSizedBox(const Size(400.0, 100.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(400.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(800.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(800.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(800.0, 0.0));
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(-200.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(200.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(600.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(800.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(800.0, 0.0));
root.offset = new ViewportOffset.fixed(600.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(-600.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(-200.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(200.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(600.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(800.0, 0.0));
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(-900.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(-500.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(-100.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(300.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(700.0, 0.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(150.0, 450.0));
expect(result.path.first.target, equals(c));
});
test('RenderShrinkWrappingViewport basic test - left', () {
RenderBox a, b, c, d, e;
RenderShrinkWrappingViewport root = new RenderShrinkWrappingViewport(
axisDirection: AxisDirection.left,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
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: c = new RenderSizedBox(const Size(400.0, 100.0))),
new RenderSliverToBoxAdapter(child: d = new RenderSizedBox(const Size(400.0, 100.0))),
new RenderSliverToBoxAdapter(child: e = new RenderSizedBox(const Size(400.0, 100.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(400.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(0.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(-400.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(-400.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-400.0, 0.0));
root.offset = new ViewportOffset.fixed(200.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(600.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(200.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(-200.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(-400.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-400.0, 0.0));
root.offset = new ViewportOffset.fixed(600.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(1000.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(600.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(200.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(-200.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-400.0, 0.0));
root.offset = new ViewportOffset.fixed(900.0);
pumpFrame();
expect(a.localToGlobal(const Point(0.0, 0.0)), const Point(1300.0, 0.0));
expect(b.localToGlobal(const Point(0.0, 0.0)), const Point(900.0, 0.0));
expect(c.localToGlobal(const Point(0.0, 0.0)), const Point(500.0, 0.0));
expect(d.localToGlobal(const Point(0.0, 0.0)), const Point(100.0, 0.0));
expect(e.localToGlobal(const Point(0.0, 0.0)), const Point(-300.0, 0.0));
HitTestResult result = new HitTestResult();
root.hitTest(result, position: const Point(550.0, 150.0));
expect(result.path.first.target, equals(c));
});
test('RenderShrinkWrappingViewport shrinkwrap test - 1 child', () {
RenderBox child;
RenderBox root = new RenderPositionedBox(
child: child = new RenderShrinkWrappingViewport(
axisDirection: AxisDirection.left,
offset: new ViewportOffset.fixed(200.0),
children: <RenderSliver>[
new RenderSliverToBoxAdapter(child: new RenderSizedBox(const Size(400.0, 100.0))),
],
),
);
layout(root);
expect(root.size.width, equals(800.0));
expect(root.size.height, equals(600.0));
expect(child.size.width, equals(400.0));
expect(child.size.height, equals(600.0));
});
test('RenderShrinkWrappingViewport shrinkwrap test - 2 children', () {
RenderBox child;
RenderBox root = new RenderPositionedBox(
child: child = new RenderShrinkWrappingViewport(
axisDirection: AxisDirection.right,
offset: new ViewportOffset.fixed(200.0),
children: <RenderSliver>[
new RenderSliverToBoxAdapter(child: new RenderSizedBox(const Size(300.0, 100.0))),
new RenderSliverToBoxAdapter(child: new RenderSizedBox(const Size(150.0, 100.0))),
],
),
);
layout(root);
expect(root.size.width, equals(800.0));
expect(root.size.height, equals(600.0));
expect(child.size.width, equals(450.0));
expect(child.size.height, equals(600.0));
});
}
......@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'test_widgets.dart';
final Key blockKey = new Key('test');
void main() {
......@@ -130,7 +132,7 @@ void main() {
new Container(),
]);
await tester.pumpWidget(new ScrollableViewport2(
await tester.pumpWidget(new TestScrollable(
slivers: <Widget>[
new SliverBlock(
delegate: delegate,
......
......@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import 'test_widgets.dart';
final Matcher doesNotOverscroll = isNot(paints..circle());
......@@ -24,7 +25,7 @@ Future<Null> slowDrag(WidgetTester tester, Point start, Offset offset) async {
void main() {
testWidgets('Overscroll indicator color', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
......@@ -57,7 +58,7 @@ void main() {
testWidgets('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
......@@ -92,7 +93,7 @@ void main() {
testWidgets('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
......@@ -125,7 +126,7 @@ void main() {
group('Flipping direction of scrollable doesn\'t change overscroll behavior', () {
testWidgets('down', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
......@@ -142,7 +143,7 @@ void main() {
testWidgets('up', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.up,
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
......@@ -160,7 +161,7 @@ void main() {
testWidgets('Overscroll in both directions', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
......@@ -180,7 +181,7 @@ void main() {
testWidgets('Overscroll horizontally', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.right,
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
......@@ -203,7 +204,7 @@ void main() {
RenderObject painter;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.left,
scrollBehavior: new TestScrollBehavior1(),
slivers: <Widget>[
......@@ -218,7 +219,7 @@ void main() {
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.right,
scrollBehavior: new TestScrollBehavior2(),
slivers: <Widget>[
......
......@@ -303,4 +303,7 @@ void main() {
expect(tester.getSize(find.text('3')), equals(const Size(400.0, 400.0)));
expect(find.text('4'), findsNothing);
});
// TODO(ianh): can you tap a grid cell that is slightly off the bottom of the screen?
// (try it with the flutter_gallery Grid demo)
}
......@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'test_widgets.dart';
class TestScrollPosition extends ScrollPosition {
TestScrollPosition(
this.extentMultiplier,
......@@ -88,7 +90,7 @@ class TestScrollBehavior extends ScrollBehavior2 {
void main() {
testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async {
await tester.pumpWidget(new ScrollableViewport2(
await tester.pumpWidget(new TestScrollable(
scrollBehavior: new TestScrollBehavior(1.0),
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
......@@ -97,7 +99,7 @@ void main() {
Scrollable2State state = tester.state(find.byType(Scrollable2));
expect(state.position.getMetrics().extentInside, 1.0);
await tester.pumpWidget(new ScrollableViewport2(
await tester.pumpWidget(new TestScrollable(
scrollBehavior: new TestScrollBehavior(2.0),
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
......
......@@ -6,12 +6,14 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'test_widgets.dart';
Future<Null> pumpTest(WidgetTester tester, TargetPlatform platform) async {
await tester.pumpWidget(new MaterialApp(
theme: new ThemeData(
platform: platform,
),
home: new ScrollableViewport2(
home: new TestScrollable(
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
......
......@@ -5,6 +5,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'test_widgets.dart';
void main() {
testWidgets('SliverFillRemaining control test', (WidgetTester tester) async {
List<Widget> children = new List<Widget>.generate(20, (int i) {
......@@ -12,7 +14,7 @@ void main() {
});
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
slivers: <Widget>[
new SliverFill(
delegate: new SliverChildListDelegate(children),
......@@ -28,7 +30,7 @@ void main() {
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsNothing);
await tester.scroll(find.byType(ScrollableViewport2), const Offset(0.0, -700.0));
await tester.scroll(find.byType(Scrollable2), const Offset(0.0, -700.0));
await tester.pump();
expect(find.text('0'), findsNothing);
......@@ -37,7 +39,7 @@ void main() {
expect(find.text('3'), findsNothing);
expect(find.text('4'), findsNothing);
await tester.scroll(find.byType(ScrollableViewport2), const Offset(0.0, 200.0));
await tester.scroll(find.byType(Scrollable2), const Offset(0.0, 200.0));
await tester.pump();
expect(find.text('0'), findsOneWidget);
......
......@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'test_widgets.dart';
void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
RenderSliver target = key.currentContext.findRenderObject();
expect(target.parent, new isInstanceOf<RenderViewport2>());
......@@ -26,7 +28,7 @@ void main() {
testWidgets('Sliver appbars - floating - scroll offset doesn\'t change', (WidgetTester tester) async {
const double bigHeight = 1000.0;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(height: bigHeight),
......@@ -54,7 +56,7 @@ void main() {
const double bigHeight = 1000.0;
GlobalKey key1, key2, key3;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
......@@ -124,7 +126,7 @@ void main() {
const double bigHeight = 1000.0;
GlobalKey key1, key2, key3;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
......@@ -157,7 +159,7 @@ void main() {
const double bigHeight = 1000.0;
GlobalKey key1, key2, key3;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
......
......@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'test_widgets.dart';
void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
RenderSliver target = key.currentContext.findRenderObject();
expect(target.parent, new isInstanceOf<RenderViewport2>());
......@@ -17,7 +19,7 @@ void verifyPaintPosition(GlobalKey key, Offset ideal, bool visible) {
}
void verifyActualBoxPosition(WidgetTester tester, Finder finder, int index, Rect ideal) {
RenderBox box = tester.renderObjectList/*<RenderBox>*/(finder).elementAt(index);
RenderBox box = tester.renderObjectList<RenderBox>(finder).elementAt(index);
Rect rect = new Rect.fromPoints(box.localToGlobal(Point.origin), box.localToGlobal(box.size.bottomRight(Point.origin)));
expect(rect, equals(ideal));
}
......@@ -27,7 +29,7 @@ void main() {
const double bigHeight = 550.0;
GlobalKey key1, key2, key3, key4, key5;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
......@@ -61,7 +63,7 @@ void main() {
const double bigHeight = 550.0;
GlobalKey key1, key2, key3, key4, key5;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
......@@ -151,7 +153,7 @@ void main() {
const double bigHeight = 650.0;
GlobalKey key1, key2, key3, key4, key5;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey(), height: bigHeight),
......
......@@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'test_widgets.dart';
void verifyPaintPosition(GlobalKey key, Offset ideal) {
RenderObject target = key.currentContext.findRenderObject();
expect(target.parent, new isInstanceOf<RenderViewport2>());
......@@ -18,7 +20,7 @@ void main() {
testWidgets('Sliver appbars - scrolling', (WidgetTester tester) async {
GlobalKey key1, key2, key3, key4, key5;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey()),
......@@ -52,7 +54,7 @@ void main() {
GlobalKey key = new GlobalKey();
TestDelegate delegate = new TestDelegate();
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(),
......@@ -65,7 +67,7 @@ void main() {
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
position.animate(to: RenderBigSliver.height + delegate.maxExtent - 5.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
RenderBox box = tester.renderObject/*<RenderBox>*/(find.byType(Container));
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container));
Rect rect = new Rect.fromPoints(box.localToGlobal(Point.origin), box.localToGlobal(box.size.bottomRight(Point.origin)));
expect(rect, equals(new Rect.fromLTWH(0.0, -195.0, 800.0, 200.0)));
});
......
......@@ -7,6 +7,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/material.dart';
import 'test_widgets.dart';
class TestSliverAppBarDelegate extends SliverAppBarDelegate {
TestSliverAppBarDelegate(this._maxExtent);
......@@ -74,7 +76,7 @@ void main() {
final GlobalKey centerKey = new GlobalKey();
await tester.pumpWidget(
new Scrollbar2(
child: new ScrollableViewport2(
child: new TestScrollable(
axisDirection: AxisDirection.down,
center: centerKey,
anchor: 0.25,
......
......@@ -8,6 +8,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'test_widgets.dart';
void verifyPaintPosition(GlobalKey key, Offset ideal) {
RenderObject target = key.currentContext.findRenderObject();
expect(target.parent, new isInstanceOf<RenderViewport2>());
......@@ -20,7 +22,7 @@ void main() {
testWidgets('Sliver protocol', (WidgetTester tester) async {
GlobalKey key1, key2, key3, key4, key5;
await tester.pumpWidget(
new ScrollableViewport2(
new TestScrollable(
axisDirection: AxisDirection.down,
slivers: <Widget>[
new BigSliver(key: key1 = new GlobalKey()),
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
final BoxDecoration kBoxDecorationA = const BoxDecoration(
......@@ -56,3 +57,49 @@ class FlipWidgetState extends State<FlipWidget> {
void flipStatefulWidget(WidgetTester tester) {
tester.state<FlipWidgetState>(find.byType(FlipWidget)).flip();
}
class TestScrollable extends StatelessWidget {
TestScrollable({
Key key,
this.initialScrollOffset: 0.0,
this.axisDirection: AxisDirection.down,
this.anchor: 0.0,
this.center,
this.scrollBehavior,
this.slivers: const <Widget>[],
}) {
assert(slivers != null);
}
final double initialScrollOffset;
final AxisDirection axisDirection;
final double anchor;
final Key center;
final ScrollBehavior2 scrollBehavior;
final List<Widget> slivers;
Axis get axis => axisDirectionToAxis(axisDirection);
@override
Widget build(BuildContext context) {
return new Scrollable2(
initialScrollOffset: initialScrollOffset,
axisDirection: axisDirection,
scrollBehavior: scrollBehavior,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport2(
axisDirection: axisDirection,
anchor: anchor,
offset: offset,
center: center,
slivers: slivers,
);
}
);
}
}
\ No newline at end of file
......@@ -18,8 +18,8 @@ import 'package:quiver/time.dart';
import 'package:test/test.dart' as test_package;
import 'package:vector_math/vector_math_64.dart';
import 'test_async_utils.dart';
import 'stack_manipulation.dart';
import 'test_async_utils.dart';
/// Phases that can be reached by [WidgetTester.pumpWidget] and
/// [TestWidgetsFlutterBinding.pump].
......
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