Commit 596eb033 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Loosen the constraints for Stack non-positioned children. (#9581)

Also:

 * Add three explicit sizing modes to Stack for non-positioned
   children: loose, expand, and passthrough. (All three are used.)

 * Fix a bug whereby layers would try to paint in the same frame as
   they were removed from layout (but not detached).

 * Fix a bug whereby Offstage wasn't properly marking the parent dirty
   when changing its sizedByParent flag.

 * Explicitly make Overlay expand non-positioned children.

 * Explicitly have InputDecoration pass through the constraints from
   its Row to its Stack children.
parent 24b40d87
......@@ -428,7 +428,10 @@ class InputDecorator extends StatelessWidget {
));
}
final Widget stack = new Stack(children: stackChildren);
final Widget stack = new Stack(
sizing: StackFit.passthrough,
children: stackChildren
);
if (decoration.icon != null) {
assert(!isCollapsed);
......
......@@ -1638,6 +1638,9 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// `super.markNeedsLayout()`, in the normal case, or call
/// [markParentNeedsLayout], in the case where the parent neds to be laid out
/// as well as the child.
///
/// If [sizedByParent] has changed, called
/// [markNeedsLayoutForSizedByParentChange] instead of [markNeedsLayout].
void markNeedsLayout() {
assert(_debugCanPerformMutations);
if (_needsLayout) {
......@@ -1664,9 +1667,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// Mark this render object's layout information as dirty, and then defer to
/// the parent.
///
/// This function should only be called from [markNeedsLayout] implementations
/// of subclasses that introduce more reasons for deferring the handling of
/// dirty layout to the parent. See [markNeedsLayout] for details.
/// This function should only be called from [markNeedsLayout] or
/// [markNeedsLayoutForSizedByParentChange] implementations of subclasses that
/// introduce more reasons for deferring the handling of dirty layout to the
/// parent. See [markNeedsLayout] for details.
///
/// Only call this if [parent] is not null.
@protected
......@@ -1681,6 +1685,18 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
assert(parent == this.parent);
}
/// Mark this render object's layout information as dirty (like
/// [markNeedsLayout]), and additionally also handle any necessary work to
/// handle the case where [sizedByParent] has changed value.
///
/// This should be called whenever [sizedByParent] might have changed.
///
/// Only call this if [parent] is not null.
void markNeedsLayoutForSizedByParentChange() {
markNeedsLayout();
markParentNeedsLayout();
}
void _cleanRelayoutBoundary() {
if (_relayoutBoundary != this) {
_relayoutBoundary = null;
......@@ -1878,6 +1894,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// Returning false is always correct, but returning true can be more
/// efficient when computing the size of this render object because we don't
/// need to recompute the size if the constraints don't change.
///
/// Typically, subclasses will always return the same value. If the value can
/// change, then, when it does change, the subclass should make sure to call
/// [markNeedsLayoutForSizedByParentChange].
@protected
bool get sizedByParent => false;
......@@ -2224,21 +2244,18 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
'disallowed.'
);
}
if (_needsLayout) {
throw new FlutterError(
'Tried to paint a RenderObject before it was laid out.\n'
'The following RenderObject was marked as dirty for layout at the '
'time that it was painted:\n'
' ${toStringShallow("\n ")}\n'
'A RenderObject that is still dirty for layout cannot be painted '
'because it does not know its own geometry yet.\n'
'Maybe one of the ancestors of this RenderObject was skipped '
'during the layout phase, but not skipped during the paint phase. '
'If the ancestor in question is below the nearest relayout boundary, '
'but is not below the nearest repaint boundary, that could cause '
'this error.'
);
}
return true;
});
// If we still need layout, then that means that we were skipped in the
// layout phase and therefore don't need painting. We might not know that
// yet (that is, our layer might not have been detached yet), because the
// same node that skipped us in layout is above us in the tree (obviously)
// and therefore may not have had a chance to paint yet (since the tree
// paints in reverse order). In particular this will happen if they are have
// a different layer, because there's a repaint boundary between us.
if (_needsLayout)
return;
assert(() {
if (_needsCompositingBitsUpdate) {
throw new FlutterError(
'Tried to paint a RenderObject before its compositing bits were '
......@@ -2651,7 +2668,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
void debugFillDescription(List<String> description) {
if (debugCreator != null)
description.add('creator: $debugCreator');
description.add('parentData: $parentData');
description.add('parentData: $parentData${ _debugCanParentUseSize ? " (can use size)" : ""}');
description.add('constraints: $constraints');
if (_layer != null) // don't access it via the "layer" getter since that's only valid when we don't need paint
description.add('layer: $_layer');
......
......@@ -2407,7 +2407,7 @@ class RenderOffstage extends RenderProxyBox {
if (value == _offstage)
return;
_offstage = value;
markNeedsLayout();
markNeedsLayoutForSizedByParentChange();
}
@override
......
......@@ -200,6 +200,48 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
}
}
/// How to size the non-positioned children of a [Stack].
///
/// This enum is used with [Stack.sizing] and [RenderStack.sizing] to control
/// how the [BoxConstraints] passed from the stack's parent to the stack's child
/// are adjusted.
///
/// See also:
///
/// * [Stack], the widget that uses this.
/// * [RenderStack], the render object that implements the stack algorithm.
enum StackFit {
/// The constraints passed to the stack from its parent are loosened.
///
/// For example, if the stack has constraints that force it to 350x600, then
/// this would allow the non-positioned children of the stack to have any
/// width from zero to 350 and any height from zero to 600.
///
/// See also:
///
/// * [Center], which loosens the constraints passed to its child and then
/// centers the child in itself.
/// * [BoxConstraints.loosen], which implements the loosening of box
/// constraints.
loose,
/// The constraints passed to the stack from its parent are tightened to the
/// biggest size allowed.
///
/// For example, if the stack has loose constraints with a width in the range
/// 10 to 100 and a height in the range 0 to 600, then the non-positioned
/// children of the stack would all be sized as 100 pixels wide and 600 high.
expand,
/// The constraints passed to the stack from its parent are passed unmodified
/// to the non-positioned children.
///
/// For example, if a [Stack] is an [Expanded] child of a [Row], the
/// horizontal constraints will be tight and the vertical constraints will be
/// loose.
passthrough,
}
/// Whether overflowing children should be clipped, or their overflow be
/// visible.
enum Overflow {
......@@ -255,10 +297,14 @@ class RenderStack extends RenderBox
/// top left corners.
RenderStack({
List<RenderBox> children,
FractionalOffset alignment: FractionalOffset.topLeft,
FractionalOffset alignment: FractionalOffset.center,
StackFit sizing: StackFit.loose,
Overflow overflow: Overflow.clip
}) : _alignment = alignment,
_sizing = sizing,
_overflow = overflow {
assert(alignment != null);
assert(sizing != null);
assert(overflow != null);
addAll(children);
}
......@@ -271,20 +317,6 @@ class RenderStack extends RenderBox
child.parentData = new StackParentData();
}
/// Whether overflowing children should be clipped. See [Overflow].
///
/// Some children in a stack might overflow its box. When this flag is set to
/// [Overflow.clipped], children cannot paint outside of the stack's box.
Overflow get overflow => _overflow;
Overflow _overflow;
set overflow(Overflow value) {
assert(value != null);
if (_overflow != value) {
_overflow = value;
markNeedsPaint();
}
}
/// How to align the non-positioned children in the stack.
///
/// The non-positioned children are placed relative to each other such that
......@@ -294,12 +326,42 @@ class RenderStack extends RenderBox
FractionalOffset get alignment => _alignment;
FractionalOffset _alignment;
set alignment(FractionalOffset value) {
assert(value != null);
if (_alignment != value) {
_alignment = value;
markNeedsLayout();
}
}
/// How to size the non-positioned children in the stack.
///
/// The constraints passed into the [RenderStack] from its parent are either
/// loosened ([StackFit.loose]) or tightened to their biggest size
/// ([StackFit.expand]).
StackFit get sizing => _sizing;
StackFit _sizing;
set sizing(StackFit value) {
assert(value != null);
if (_sizing != value) {
_sizing = value;
markNeedsLayout();
}
}
/// Whether overflowing children should be clipped. See [Overflow].
///
/// Some children in a stack might overflow its box. When this flag is set to
/// [Overflow.clipped], children cannot paint outside of the stack's box.
Overflow get overflow => _overflow;
Overflow _overflow;
set overflow(Overflow value) {
assert(value != null);
if (_overflow != value) {
_overflow = value;
markNeedsPaint();
}
}
double _getIntrinsicDimension(double mainChildSizeGetter(RenderBox child)) {
double extent = 0.0;
RenderBox child = firstChild;
......@@ -343,8 +405,23 @@ class RenderStack extends RenderBox
_hasVisualOverflow = false;
bool hasNonPositionedChildren = false;
double width = 0.0;
double height = 0.0;
double width = constraints.minWidth;
double height = constraints.minHeight;
BoxConstraints nonPositionedConstraints;
assert(sizing != null);
switch (sizing) {
case StackFit.loose:
nonPositionedConstraints = constraints.loosen();
break;
case StackFit.expand:
nonPositionedConstraints = new BoxConstraints.tight(constraints.biggest);
break;
case StackFit.passthrough:
nonPositionedConstraints = constraints;
break;
}
assert(nonPositionedConstraints != null);
RenderBox child = firstChild;
while (child != null) {
......@@ -353,8 +430,7 @@ class RenderStack extends RenderBox
if (!childParentData.isPositioned) {
hasNonPositionedChildren = true;
child.layout(constraints, parentUsesSize: true);
childParentData.offset = Offset.zero;
child.layout(nonPositionedConstraints, parentUsesSize: true);
final Size childSize = child.size;
width = math.max(width, childSize.width);
......
......@@ -43,6 +43,7 @@ export 'package:flutter/rendering.dart' show
RelativeRect,
ShaderCallback,
SingleChildLayoutDelegate,
StackFit,
TextOverflow,
ValueChanged,
ValueGetter,
......@@ -1647,6 +1648,7 @@ class Stack extends MultiChildRenderObjectWidget {
Stack({
Key key,
this.alignment: FractionalOffset.topLeft,
this.sizing: StackFit.loose,
this.overflow: Overflow.clip,
List<Widget> children: const <Widget>[],
}) : super(key: key, children: children);
......@@ -1659,6 +1661,13 @@ class Stack extends MultiChildRenderObjectWidget {
/// each non-positioned child will be located at the same global coordinate.
final FractionalOffset alignment;
/// How to size the non-positioned children in the stack.
///
/// The constraints passed into the [Stack] from its parent are either
/// loosened ([StackFit.loose]) or tightened to their biggest size
/// ([StackFit.expand]).
final StackFit sizing;
/// Whether overflowing children should be clipped. See [Overflow].
///
/// Some children in a stack might overflow its box. When this flag is set to
......@@ -1669,7 +1678,8 @@ class Stack extends MultiChildRenderObjectWidget {
RenderStack createRenderObject(BuildContext context) {
return new RenderStack(
alignment: alignment,
overflow: overflow
sizing: sizing,
overflow: overflow,
);
}
......@@ -1677,6 +1687,7 @@ class Stack extends MultiChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderStack renderObject) {
renderObject
..alignment = alignment
..sizing = sizing
..overflow = overflow;
}
}
......@@ -1696,9 +1707,10 @@ class IndexedStack extends Stack {
IndexedStack({
Key key,
FractionalOffset alignment: FractionalOffset.topLeft,
StackFit sizing: StackFit.loose,
this.index: 0,
List<Widget> children: const <Widget>[],
}) : super(key: key, alignment: alignment, children: children);
}) : super(key: key, alignment: alignment, sizing: sizing, children: children);
/// The index of the child to show.
final int index;
......
......@@ -198,9 +198,8 @@ class Overlay extends StatefulWidget {
/// The initial entries will be inserted into the overlay when its associated
/// [OverlayState] is initialized.
///
/// Rather than creating an overlay, consider using the overlay that has
/// already been created by the [WidgetsApp] or the [MaterialApp] for this
/// application.
/// Rather than creating an overlay, consider using the overlay that is
/// created by the [WidgetsApp] or the [MaterialApp] for the application.
const Overlay({
Key key,
this.initialEntries: const <OverlayEntry>[]
......@@ -362,7 +361,10 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
}
}
return new _Theatre(
onstage: new Stack(children: onstageChildren.reversed.toList(growable: false)),
onstage: new Stack(
sizing: StackFit.expand,
children: onstageChildren.reversed.toList(growable: false),
),
offstage: offstageChildren,
);
}
......
......@@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('InputDecorator always expands', (WidgetTester tester) async {
testWidgets('InputDecorator always expands horizontally', (WidgetTester tester) async {
final Key key = new UniqueKey();
await tester.pumpWidget(new Material(
......
......@@ -45,4 +45,6 @@ void main() {
expect(green.size.width, equals(100.0));
expect(green.size.height, equals(100.0));
});
// More tests in ../widgets/stack_test.dart
}
......@@ -76,7 +76,7 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // end transition
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
reason: 'because the barrier was dismissed');
reason: 'The route should have been dismissed by tapping the barrier.');
});
}
......
......@@ -22,25 +22,27 @@ void main() {
//
await tester.pumpWidget(
new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
label: 'L1'
label: 'L1',
),
new Semantics(
label: 'L2',
child: new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
checked: true
checked: true,
),
const Semantics(
checked: false
)
]
)
)
]
)
checked: false,
),
],
),
),
],
),
);
expect(semantics, hasSemantics(
......@@ -76,23 +78,25 @@ void main() {
//
await tester.pumpWidget(
new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
label: 'L1'
label: 'L1',
),
new Semantics(
label: 'L2',
child: new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
checked: true
checked: true,
),
const Semantics()
]
)
)
]
)
const Semantics(),
],
),
),
],
),
);
expect(semantics, hasSemantics(
......@@ -118,21 +122,23 @@ void main() {
//
await tester.pumpWidget(
new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(),
new Semantics(
label: 'L2',
child: new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
checked: true
checked: true,
),
const Semantics()
]
)
)
]
)
const Semantics(),
],
),
),
],
),
);
expect(semantics, hasSemantics(
......
......@@ -14,16 +14,17 @@ void main() {
await tester.pumpWidget(
new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
// this tests that empty nodes disappear
),
const Semantics(
// this tests whether you can have a container with no other semantics
container: true
container: true,
),
const Semantics(
label: 'label' // (force a fork)
label: 'label', // (force a fork)
),
]
)
......
......@@ -19,6 +19,7 @@ void main() {
label = '1';
await tester.pumpWidget(
new Stack(
sizing: StackFit.expand,
children: <Widget>[
new MergeSemantics(
child: new Semantics(
......@@ -26,19 +27,20 @@ void main() {
container: true,
child: new Semantics(
container: true,
label: label
label: label,
)
)
),
new MergeSemantics(
child: new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
checked: true
checked: true,
),
new Semantics(
label: label
)
label: label,
),
]
)
),
......@@ -69,6 +71,7 @@ void main() {
label = '2';
await tester.pumpWidget(
new Stack(
sizing: StackFit.expand,
children: <Widget>[
new MergeSemantics(
child: new Semantics(
......@@ -76,18 +79,19 @@ void main() {
container: true,
child: new Semantics(
container: true,
label: label
label: label,
)
)
),
new MergeSemantics(
child: new Stack(
sizing: StackFit.expand,
children: <Widget>[
const Semantics(
checked: true
checked: true,
),
new Semantics(
label: label
label: label,
)
]
)
......
......@@ -44,6 +44,8 @@ class TestSemantics {
final String label;
/// The bounding box for this node in its coordinate system.
///
/// Defaults to filling the screen.
final Rect rect;
/// The transform from this node's coordinate system to its parent's coordinate system.
......@@ -62,7 +64,7 @@ class TestSemantics {
actions: actions,
label: label,
rect: rect,
transform: transform
transform: transform,
);
}
......
......@@ -343,4 +343,70 @@ void main() {
box.paint(context, Offset.zero);
expect(context.invocations.first.memberName, equals(#paintChild));
});
testWidgets('Stack sizing: default', (WidgetTester tester) async {
final List<String> logs = <String>[];
await tester.pumpWidget(
new Center(
child: new ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 2.0,
maxWidth: 3.0,
minHeight: 5.0,
maxHeight: 7.0,
),
child: new Stack(
children: <Widget>[
new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
logs.add(constraints.toString());
return const Placeholder();
},
),
],
),
),
),
);
expect(logs, <String>['BoxConstraints(0.0<=w<=3.0, 0.0<=h<=7.0)']);
});
testWidgets('Stack sizing: explicit', (WidgetTester tester) async {
final List<String> logs = <String>[];
Widget buildStack(StackFit sizing) {
return new Center(
child: new ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 2.0,
maxWidth: 3.0,
minHeight: 5.0,
maxHeight: 7.0,
),
child: new Stack(
sizing: sizing,
children: <Widget>[
new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
logs.add(constraints.toString());
return const Placeholder();
},
),
],
),
),
);
}
await tester.pumpWidget(buildStack(StackFit.loose));
logs.add('=1=');
await tester.pumpWidget(buildStack(StackFit.expand));
logs.add('=2=');
await tester.pumpWidget(buildStack(StackFit.passthrough));
expect(logs, <String>[
'BoxConstraints(0.0<=w<=3.0, 0.0<=h<=7.0)',
'=1=',
'BoxConstraints(w=3.0, h=7.0)',
'=2=',
'BoxConstraints(2.0<=w<=3.0, 5.0<=h<=7.0)'
]);
});
}
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