Unverified Commit bc5ea0a0 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Reland "Do not rebuild Routes when a new opaque Route is pushed on top" (#49376)

parent c341d4b7
...@@ -425,7 +425,8 @@ class RenderStack extends RenderBox ...@@ -425,7 +425,8 @@ class RenderStack extends RenderBox
} }
} }
double _getIntrinsicDimension(double mainChildSizeGetter(RenderBox child)) { /// Helper function for calculating the intrinsics metrics of a Stack.
static double getIntrinsicDimension(RenderBox firstChild, double mainChildSizeGetter(RenderBox child)) {
double extent = 0.0; double extent = 0.0;
RenderBox child = firstChild; RenderBox child = firstChild;
while (child != null) { while (child != null) {
...@@ -440,22 +441,22 @@ class RenderStack extends RenderBox ...@@ -440,22 +441,22 @@ class RenderStack extends RenderBox
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicWidth(height)); return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicWidth(height)); return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicHeight(width)); return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicHeight(width)); return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
} }
@override @override
...@@ -463,6 +464,57 @@ class RenderStack extends RenderBox ...@@ -463,6 +464,57 @@ class RenderStack extends RenderBox
return defaultComputeDistanceToHighestActualBaseline(baseline); return defaultComputeDistanceToHighestActualBaseline(baseline);
} }
/// Lays out the positioned `child` according to `alignment` within a Stack of `size`.
///
/// Returns true when the child has visual overflow.
static bool layoutPositionedChild(RenderBox child, StackParentData childParentData, Size size, Alignment alignment) {
assert(childParentData.isPositioned);
assert(child.parentData == childParentData);
bool hasVisualOverflow = false;
BoxConstraints childConstraints = const BoxConstraints();
if (childParentData.left != null && childParentData.right != null)
childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
else if (childParentData.width != null)
childConstraints = childConstraints.tighten(width: childParentData.width);
if (childParentData.top != null && childParentData.bottom != null)
childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
else if (childParentData.height != null)
childConstraints = childConstraints.tighten(height: childParentData.height);
child.layout(childConstraints, parentUsesSize: true);
double x;
if (childParentData.left != null) {
x = childParentData.left;
} else if (childParentData.right != null) {
x = size.width - childParentData.right - child.size.width;
} else {
x = alignment.alongOffset(size - child.size as Offset).dx;
}
if (x < 0.0 || x + child.size.width > size.width)
hasVisualOverflow = true;
double y;
if (childParentData.top != null) {
y = childParentData.top;
} else if (childParentData.bottom != null) {
y = size.height - childParentData.bottom - child.size.height;
} else {
y = alignment.alongOffset(size - child.size as Offset).dy;
}
if (y < 0.0 || y + child.size.height > size.height)
hasVisualOverflow = true;
childParentData.offset = Offset(x, y);
return hasVisualOverflow;
}
@override @override
void performLayout() { void performLayout() {
_resolve(); _resolve();
...@@ -527,45 +579,7 @@ class RenderStack extends RenderBox ...@@ -527,45 +579,7 @@ class RenderStack extends RenderBox
if (!childParentData.isPositioned) { if (!childParentData.isPositioned) {
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size as Offset); childParentData.offset = _resolvedAlignment.alongOffset(size - child.size as Offset);
} else { } else {
BoxConstraints childConstraints = const BoxConstraints(); _hasVisualOverflow = layoutPositionedChild(child, childParentData, size, _resolvedAlignment) || _hasVisualOverflow;
if (childParentData.left != null && childParentData.right != null)
childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
else if (childParentData.width != null)
childConstraints = childConstraints.tighten(width: childParentData.width);
if (childParentData.top != null && childParentData.bottom != null)
childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
else if (childParentData.height != null)
childConstraints = childConstraints.tighten(height: childParentData.height);
child.layout(childConstraints, parentUsesSize: true);
double x;
if (childParentData.left != null) {
x = childParentData.left;
} else if (childParentData.right != null) {
x = size.width - childParentData.right - child.size.width;
} else {
x = _resolvedAlignment.alongOffset(size - child.size as Offset).dx;
}
if (x < 0.0 || x + child.size.width > size.width)
_hasVisualOverflow = true;
double y;
if (childParentData.top != null) {
y = childParentData.top;
} else if (childParentData.bottom != null) {
y = size.height - childParentData.bottom - child.size.height;
} else {
y = _resolvedAlignment.alongOffset(size - child.size as Offset).dy;
}
if (y < 0.0 || y + child.size.height > size.height)
_hasVisualOverflow = true;
childParentData.offset = Offset(x, y);
} }
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'basic.dart'; import 'basic.dart';
import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
...@@ -115,7 +115,7 @@ class OverlayEntry { ...@@ -115,7 +115,7 @@ class OverlayEntry {
} }
OverlayState _overlay; OverlayState _overlay;
final GlobalKey<_OverlayEntryState> _key = GlobalKey<_OverlayEntryState>(); final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>();
/// Remove this entry from the overlay. /// Remove this entry from the overlay.
/// ///
...@@ -152,21 +152,30 @@ class OverlayEntry { ...@@ -152,21 +152,30 @@ class OverlayEntry {
String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
} }
class _OverlayEntry extends StatefulWidget { class _OverlayEntryWidget extends StatefulWidget {
_OverlayEntry(this.entry) const _OverlayEntryWidget({
: assert(entry != null), @required Key key,
super(key: entry._key); @required this.entry,
this.tickerEnabled = true,
}) : assert(key != null),
assert(entry != null),
assert(tickerEnabled != null),
super(key: key);
final OverlayEntry entry; final OverlayEntry entry;
final bool tickerEnabled;
@override @override
_OverlayEntryState createState() => _OverlayEntryState(); _OverlayEntryWidgetState createState() => _OverlayEntryWidgetState();
} }
class _OverlayEntryState extends State<_OverlayEntry> { class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.entry.builder(context); return TickerMode(
enabled: widget.tickerEnabled,
child: widget.entry.builder(context),
);
} }
void _markNeedsBuild() { void _markNeedsBuild() {
...@@ -452,28 +461,32 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -452,28 +461,32 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// These lists are filled backwards. For the offstage children that // This list is filled backwards and then reversed below before
// does not matter since they aren't rendered, but for the onstage // it is added to the tree.
// children we reverse the list below before adding it to the tree. final List<Widget> children = <Widget>[];
final List<Widget> onstageChildren = <Widget>[];
final List<Widget> offstageChildren = <Widget>[];
bool onstage = true; bool onstage = true;
int onstageCount = 0;
for (int i = _entries.length - 1; i >= 0; i -= 1) { for (int i = _entries.length - 1; i >= 0; i -= 1) {
final OverlayEntry entry = _entries[i]; final OverlayEntry entry = _entries[i];
if (onstage) { if (onstage) {
onstageChildren.add(_OverlayEntry(entry)); onstageCount += 1;
children.add(_OverlayEntryWidget(
key: entry._key,
entry: entry,
));
if (entry.opaque) if (entry.opaque)
onstage = false; onstage = false;
} else if (entry.maintainState) { } else if (entry.maintainState) {
offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry))); children.add(_OverlayEntryWidget(
key: entry._key,
entry: entry,
tickerEnabled: false,
));
} }
} }
return _Theatre( return _Theatre(
onstage: Stack( skipCount: children.length - onstageCount,
fit: StackFit.expand, children: children.reversed.toList(growable: false),
children: onstageChildren.reversed.toList(growable: false),
),
offstage: offstageChildren,
); );
} }
...@@ -486,36 +499,50 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -486,36 +499,50 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
} }
} }
/// A widget that has one [onstage] child which is visible, and one or more /// Special version of a [Stack], that doesn't layout and render the first
/// [offstage] widgets which are kept alive, and are built, but are not laid out /// [skipCount] children.
/// or painted.
/// ///
/// The onstage widget must be a [Stack]. /// The first [skipCount] children are considered "offstage".
/// class _Theatre extends MultiChildRenderObjectWidget {
/// For convenience, it is legal to use [Positioned] widgets around the offstage
/// widgets.
class _Theatre extends RenderObjectWidget {
_Theatre({ _Theatre({
this.onstage, Key key,
@required this.offstage, this.skipCount = 0,
}) : assert(offstage != null), List<Widget> children = const <Widget>[],
assert(!offstage.any((Widget child) => child == null)); }) : assert(skipCount != null),
assert(skipCount >= 0),
final Stack onstage; assert(children != null),
assert(children.length >= skipCount),
super(key: key, children: children);
final List<Widget> offstage; final int skipCount;
@override @override
_TheatreElement createElement() => _TheatreElement(this); _TheatreElement createElement() => _TheatreElement(this);
@override @override
_RenderTheatre createRenderObject(BuildContext context) => _RenderTheatre(); _RenderTheatre createRenderObject(BuildContext context) {
return _RenderTheatre(
skipCount: skipCount,
textDirection: Directionality.of(context),
);
}
@override
void updateRenderObject(BuildContext context, _RenderTheatre renderObject) {
renderObject
..skipCount = skipCount
..textDirection = Directionality.of(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('skipCount', skipCount));
}
} }
class _TheatreElement extends RenderObjectElement { class _TheatreElement extends MultiChildRenderObjectElement {
_TheatreElement(_Theatre widget) _TheatreElement(_Theatre widget) : super(widget);
: assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage)),
super(widget);
@override @override
_Theatre get widget => super.widget as _Theatre; _Theatre get widget => super.widget as _Theatre;
...@@ -523,186 +550,268 @@ class _TheatreElement extends RenderObjectElement { ...@@ -523,186 +550,268 @@ class _TheatreElement extends RenderObjectElement {
@override @override
_RenderTheatre get renderObject => super.renderObject as _RenderTheatre; _RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
Element _onstage; @override
static final Object _onstageSlot = Object(); void debugVisitOnstageChildren(ElementVisitor visitor) {
assert(children.length >= widget.skipCount);
children.skip(widget.skipCount).forEach(visitor);
}
}
class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> {
_RenderTheatre({
List<RenderBox> children,
@required TextDirection textDirection,
int skipCount = 0,
}) : assert(skipCount != null),
assert(skipCount >= 0),
assert(textDirection != null),
_textDirection = textDirection,
_skipCount = skipCount {
addAll(children);
}
List<Element> _offstage; bool _hasVisualOverflow = false;
final Set<Element> _forgottenOffstageChildren = HashSet<Element>();
@override @override
void insertChildRenderObject(RenderBox child, dynamic slot) { void setupParentData(RenderBox child) {
assert(renderObject.debugValidateChild(child)); if (child.parentData is! StackParentData)
if (slot == _onstageSlot) { child.parentData = StackParentData();
assert(child is RenderStack); }
renderObject.child = child as RenderStack;
} else { Alignment _resolvedAlignment;
assert(slot == null || slot is Element);
renderObject.insert(child, after: slot?.renderObject as RenderBox); void _resolve() {
if (_resolvedAlignment != null)
return;
_resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
}
void _markNeedResolution() {
_resolvedAlignment = null;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value)
return;
_textDirection = value;
_markNeedResolution();
}
int get skipCount => _skipCount;
int _skipCount;
set skipCount(int value) {
assert(value != null);
if (_skipCount != value) {
_skipCount = value;
markNeedsLayout();
} }
} }
@override RenderBox get _firstOnstageChild {
void moveChildRenderObject(RenderBox child, dynamic slot) { if (skipCount == super.childCount) {
if (slot == _onstageSlot) { return null;
renderObject.remove(child); }
assert(child is RenderStack); RenderBox child = super.firstChild;
renderObject.child = child as RenderStack; for (int toSkip = skipCount; toSkip > 0; toSkip--) {
} else { final StackParentData childParentData = child.parentData as StackParentData;
assert(slot == null || slot is Element); child = childParentData.nextSibling;
if (renderObject.child == child) { assert(child != null);
renderObject.child = null;
renderObject.insert(child, after: slot?.renderObject as RenderBox);
} else {
renderObject.move(child, after: slot?.renderObject as RenderBox);
}
} }
return child;
} }
RenderBox get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
int get _onstageChildCount => childCount - skipCount;
@override @override
void removeChildRenderObject(RenderBox child) { double computeMinIntrinsicWidth(double height) {
if (renderObject.child == child) { return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
renderObject.child = null;
} else {
renderObject.remove(child);
}
} }
@override @override
void visitChildren(ElementVisitor visitor) { double computeMaxIntrinsicWidth(double height) {
if (_onstage != null) return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
visitor(_onstage);
for (final Element child in _offstage) {
if (!_forgottenOffstageChildren.contains(child))
visitor(child);
}
} }
@override @override
void debugVisitOnstageChildren(ElementVisitor visitor) { double computeMinIntrinsicHeight(double width) {
if (_onstage != null) return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
visitor(_onstage);
} }
@override @override
bool forgetChild(Element child) { double computeMaxIntrinsicHeight(double width) {
if (child == _onstage) { return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
_onstage = null;
} else {
assert(_offstage.contains(child));
assert(!_forgottenOffstageChildren.contains(child));
_forgottenOffstageChildren.add(child);
}
return true;
} }
@override @override
void mount(Element parent, dynamic newSlot) { double computeDistanceToActualBaseline(TextBaseline baseline) {
super.mount(parent, newSlot); assert(!debugNeedsLayout);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot); double result;
_offstage = List<Element>(widget.offstage.length); RenderBox child = _firstOnstageChild;
Element previousChild; while (child != null) {
for (int i = 0; i < _offstage.length; i += 1) { assert(!child.debugNeedsLayout);
final Element newChild = inflateWidget(widget.offstage[i], previousChild); final StackParentData childParentData = child.parentData as StackParentData;
_offstage[i] = newChild; double candidate = child.getDistanceToActualBaseline(baseline);
previousChild = newChild; if (candidate != null) {
candidate += childParentData.offset.dy;
if (result != null) {
result = math.min(result, candidate);
} else {
result = candidate;
}
}
child = childParentData.nextSibling;
} }
return result;
} }
@override @override
void update(_Theatre newWidget) { bool get sizedByParent => true;
super.update(newWidget);
assert(widget == newWidget); @override
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot); void performResize() {
_offstage = updateChildren(_offstage, widget.offstage, forgottenChildren: _forgottenOffstageChildren); size = constraints.biggest;
_forgottenOffstageChildren.clear(); assert(size.isFinite);
} }
}
// A render object which lays out and paints one subtree while keeping a list @override
// of other subtrees alive but not laid out or painted (the "zombie" children). void performLayout() {
// _hasVisualOverflow = false;
// The subtree that is laid out and painted must be a [RenderStack].
// if (_onstageChildCount == 0) {
// This class uses [StackParentData] objects for its parent data so that the return;
// children of its primary subtree's stack can be moved to this object's list }
// of zombie children without changing their parent data objects.
class _RenderTheatre extends RenderBox _resolve();
with RenderObjectWithChildMixin<RenderStack>, RenderProxyBoxMixin<RenderStack>, assert(_resolvedAlignment != null);
ContainerRenderObjectMixin<RenderBox, StackParentData> {
// Same BoxConstraints as used by RenderStack for StackFit.expand.
final BoxConstraints nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
RenderBox child = _firstOnstageChild;
while (child != null) {
final StackParentData childParentData = child.parentData as StackParentData;
if (!childParentData.isPositioned) {
child.layout(nonPositionedConstraints, parentUsesSize: true);
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size as Offset);
} else {
_hasVisualOverflow = RenderStack.layoutPositionedChild(child, childParentData, size, _resolvedAlignment) || _hasVisualOverflow;
}
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
}
@override @override
void setupParentData(RenderObject child) { bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
if (child.parentData is! StackParentData) RenderBox child = _lastOnstageChild;
child.parentData = StackParentData(); for (int i = 0; i < _onstageChildCount; i++) {
assert(child != null);
final StackParentData childParentData = child.parentData as StackParentData;
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed);
},
);
if (isHit)
return true;
child = childParentData.previousSibling;
}
return false;
} }
// Because both RenderObjectWithChildMixin and ContainerRenderObjectMixin @protected
// define redepthChildren, visitChildren and debugDescribeChildren and don't void paintStack(PaintingContext context, Offset offset) {
// call super, we have to define them again here to make sure the work of both RenderBox child = _firstOnstageChild;
// is done. while (child != null) {
// final StackParentData childParentData = child.parentData as StackParentData;
// We chose to put ContainerRenderObjectMixin last in the inheritance chain so context.paintChild(child, childParentData.offset + offset);
// that we can call super to hit its more complex definitions of child = childParentData.nextSibling;
// redepthChildren and visitChildren, and then duplicate the more trivial }
// definition from RenderObjectWithChildMixin inline in our version here. }
//
// This code duplication is suboptimal.
// TODO(ianh): Replace this with a better solution once https://github.com/dart-lang/sdk/issues/27100 is fixed
//
// For debugDescribeChildren we just roll our own because otherwise the line
// drawings won't really work as well.
@override @override
void redepthChildren() { void paint(PaintingContext context, Offset offset) {
if (child != null) if (_hasVisualOverflow) {
redepthChild(child); context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
super.redepthChildren(); } else {
paintStack(context, offset);
}
} }
@override @override
void visitChildren(RenderObjectVisitor visitor) { void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null) RenderBox child = _firstOnstageChild;
while (child != null) {
visitor(child); visitor(child);
super.visitChildren(visitor); final StackParentData childParentData = child.parentData as StackParentData;
child = childParentData.nextSibling;
}
} }
@override @override
List<DiagnosticsNode> debugDescribeChildren() { Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null;
final List<DiagnosticsNode> children = <DiagnosticsNode>[
if (child != null) child.toDiagnosticsNode(name: 'onstage'),
];
if (firstChild != null) { @override
RenderBox child = firstChild; void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('skipCount', skipCount));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
}
int count = 1; @override
while (true) { List<DiagnosticsNode> debugDescribeChildren() {
children.add( final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[];
final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[];
int count = 1;
bool onstage = false;
RenderBox child = firstChild;
final RenderBox firstOnstageChild = _firstOnstageChild;
while (child != null) {
if (child == firstOnstageChild) {
onstage = true;
count = 1;
}
if (onstage) {
onstageChildren.add(
child.toDiagnosticsNode(
name: 'onstage $count',
),
);
} else {
offstageChildren.add(
child.toDiagnosticsNode( child.toDiagnosticsNode(
name: 'offstage $count', name: 'offstage $count',
style: DiagnosticsTreeStyle.offstage, style: DiagnosticsTreeStyle.offstage,
), ),
); );
if (child == lastChild)
break;
final StackParentData childParentData = child.parentData as StackParentData;
child = childParentData.nextSibling;
count += 1;
} }
} else {
children.add( final StackParentData childParentData = child.parentData as StackParentData;
child = childParentData.nextSibling;
count += 1;
}
return <DiagnosticsNode>[
...onstageChildren,
if (offstageChildren.isNotEmpty)
...offstageChildren
else
DiagnosticsNode.message( DiagnosticsNode.message(
'no offstage children', 'no offstage children',
style: DiagnosticsTreeStyle.offstage, style: DiagnosticsTreeStyle.offstage,
), ),
); ];
}
return children;
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null)
visitor(child);
} }
} }
...@@ -72,12 +72,9 @@ CupertinoPageScaffold scaffoldForNavBar(Widget navBar) { ...@@ -72,12 +72,9 @@ CupertinoPageScaffold scaffoldForNavBar(Widget navBar) {
} }
Finder flying(WidgetTester tester, Finder finder) { Finder flying(WidgetTester tester, Finder finder) {
final RenderObjectWithChildMixin<RenderStack> theater = final ContainerRenderObjectMixin<RenderBox, StackParentData> theater = tester.renderObject(find.byType(Overlay));
tester.renderObject(find.byType(Overlay));
final RenderStack theaterStack = theater.child;
final Finder lastOverlayFinder = find.byElementPredicate((Element element) { final Finder lastOverlayFinder = find.byElementPredicate((Element element) {
return element is RenderObjectElement && return element is RenderObjectElement && element.renderObject == theater.lastChild;
element.renderObject == theaterStack.lastChild;
}); });
assert( assert(
......
...@@ -132,8 +132,8 @@ void main() { ...@@ -132,8 +132,8 @@ void main() {
' Offstage\n' ' Offstage\n'
' _ModalScopeStatus\n' ' _ModalScopeStatus\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#969b7]\n' ' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#969b7]\n'
' _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#7a3ae]\n' ' TickerMode\n'
' Stack\n' ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#545d0]\n'
' _Theatre\n' ' _Theatre\n'
' Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n' ' Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n'
' _FocusMarker\n' ' _FocusMarker\n'
......
...@@ -524,23 +524,27 @@ void main() { ...@@ -524,23 +524,27 @@ void main() {
expect(errorDetails, isNotNull); expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull); expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace // Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n'); final String fullErrorMessage = errorDetails.toString();
final List<String> lines = fullErrorMessage.split('\n');
// The lines in the middle of the error message contain the stack trace // The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run. // which will change depending on where the test is run.
expect(lines.length, greaterThan(7)); final String errorMessage = lines.takeWhile(
expect( (String line) => line != '',
lines.take(8).join('\n'), ).join('\n');
equalsIgnoringHashCodes( expect(errorMessage.length, lessThan(fullErrorMessage.length));
'══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n' expect(errorMessage, startsWith(
'The following assertion was thrown building Stepper(dirty,\n' '══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n'
'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n' 'The following assertion was thrown building Stepper('
'_StepperState#00000):\n' ));
'Steppers must not be nested.\n' // The description string of the stepper looks slightly different depending
'The material specification advises that one should avoid\n' // on the platform and is omitted here.
'embedding steppers within steppers.\n' expect(errorMessage, endsWith(
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage' '):\n'
), 'Steppers must not be nested.\n'
); 'The material specification advises that one should avoid\n'
'embedding steppers within steppers.\n'
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
));
}); });
///https://github.com/flutter/flutter/issues/16920 ///https://github.com/flutter/flutter/issues/16920
......
...@@ -1465,6 +1465,33 @@ void main() { ...@@ -1465,6 +1465,33 @@ void main() {
expect(find.byKey(const ValueKey<String>('/A/B')), findsNothing); // popped expect(find.byKey(const ValueKey<String>('/A/B')), findsNothing); // popped
expect(find.byKey(const ValueKey<String>('/C')), findsOneWidget); expect(find.byKey(const ValueKey<String>('/C')), findsOneWidget);
}); });
testWidgets('Pushing opaque Route does not rebuild routes below', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/45797.
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
final Key bottomRoute = UniqueKey();
final Key topRoute = UniqueKey();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
routes: <String, WidgetBuilder>{
'/' : (BuildContext context) => StatefulTestWidget(key: bottomRoute),
'/a': (BuildContext context) => StatefulTestWidget(key: topRoute),
},
),
);
expect(tester.state<StatefulTestState>(find.byKey(bottomRoute)).rebuildCount, 1);
navigator.currentState.pushNamed('/a');
await tester.pumpAndSettle();
// Bottom route is offstage and did not rebuild.
expect(find.byKey(bottomRoute), findsNothing);
expect(tester.state<StatefulTestState>(find.byKey(bottomRoute, skipOffstage: false)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(topRoute)).rebuildCount, 1);
});
} }
class NoAnimationPageRoute extends PageRouteBuilder<void> { class NoAnimationPageRoute extends PageRouteBuilder<void> {
...@@ -1478,3 +1505,20 @@ class NoAnimationPageRoute extends PageRouteBuilder<void> { ...@@ -1478,3 +1505,20 @@ class NoAnimationPageRoute extends PageRouteBuilder<void> {
return super.createAnimationController()..value = 1.0; return super.createAnimationController()..value = 1.0;
} }
} }
class StatefulTestWidget extends StatefulWidget {
const StatefulTestWidget({Key key}) : super(key: key);
@override
State<StatefulTestWidget> createState() => StatefulTestState();
}
class StatefulTestState extends State<StatefulTestWidget> {
int rebuildCount = 0;
@override
Widget build(BuildContext context) {
rebuildCount += 1;
return Container();
}
}
...@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; ...@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'semantics_tester.dart';
void main() { void main() {
testWidgets('OverflowEntries context contains Overlay', (WidgetTester tester) async { testWidgets('OverflowEntries context contains Overlay', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey(); final GlobalKey overlayKey = GlobalKey();
...@@ -25,6 +27,9 @@ void main() { ...@@ -25,6 +27,9 @@ void main() {
return Container(); return Container();
}, },
), ),
OverlayEntry(
builder: (BuildContext context) => Container(),
)
], ],
), ),
), ),
...@@ -36,36 +41,42 @@ void main() { ...@@ -36,36 +41,42 @@ void main() {
expect( expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info), theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderTheatre#f5cf2\n' '_RenderTheatre#744c9\n'
' │ parentData: <none>\n' ' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │\n' ' │ skipCount: 0\n'
' ├─onstage: RenderStack#39819\n' ' │ textDirection: ltr\n'
' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' │\n'
' ╎ │ size)\n' ' ├─onstage 1: RenderLimitedBox#bb803\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n'
' ╎ │ size: Size(800.0, 600.0)\n' ' │ │ size)\n'
' ╎ │ alignment: AlignmentDirectional.topStart\n' ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ │ textDirection: ltr\n' ' │ │ size: Size(800.0, 600.0)\n'
' ╎ │ fit: expand\n' ' │ │ maxWidth: 0.0\n'
' ╎ │ overflow: clip\n' ' │ │ maxHeight: 0.0\n'
' ╎ │\n' ' │ │\n'
' ╎ └─child 1: RenderLimitedBox#d1448\n' ' │ └─child: RenderConstrainedBox#62707\n'
' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' │ parentData: <none> (can use size)\n'
' ╎ │ size)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' ╎ │ size: Size(800.0, 600.0)\n' ' │ additionalConstraints: BoxConstraints(biggest)\n'
' ╎ │ maxWidth: 0.0\n' ' │\n'
' ╎ │ maxHeight: 0.0\n' ' ├─onstage 2: RenderLimitedBox#af5f1\n'
' ╎ │\n' ' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n'
' ╎ └─child: RenderConstrainedBox#e8b87\n' ' ╎ │ size)\n'
' ╎ parentData: <none> (can use size)\n' ' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ │ size: Size(800.0, 600.0)\n'
' ╎ size: Size(800.0, 600.0)\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ additionalConstraints: BoxConstraints(biggest)\n' ' ╎ │ maxHeight: 0.0\n'
' ╎\n' ' ╎ │\n'
' └╌no offstage children\n' ' ╎ └─child: RenderConstrainedBox#69c48\n'
' ╎ parentData: <none> (can use size)\n'
' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ size: Size(800.0, 600.0)\n'
' ╎ additionalConstraints: BoxConstraints(biggest)\n'
' ╎\n'
' └╌no offstage children\n'
), ),
); );
}); });
...@@ -103,60 +114,52 @@ void main() { ...@@ -103,60 +114,52 @@ void main() {
expect( expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info), theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderTheatre#b22a8\n' '_RenderTheatre#385b3\n'
' │ parentData: <none>\n' ' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │ skipCount: 2\n'
' │ textDirection: ltr\n'
' │\n' ' │\n'
' ├─onstage: RenderStack#eab87\n' ' ├─onstage 1: RenderLimitedBox#0a77a\n'
' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n'
' ╎ │ size)\n' ' ╎ │ size)\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ │ size: Size(800.0, 600.0)\n' ' ╎ │ size: Size(800.0, 600.0)\n'
' ╎ │ alignment: AlignmentDirectional.topStart\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ │ textDirection: ltr\n' ' ╎ │ maxHeight: 0.0\n'
' ╎ │ fit: expand\n'
' ╎ │ overflow: clip\n'
' ╎ │\n' ' ╎ │\n'
' ╎ └─child 1: RenderLimitedBox#ca15b\n' ' ╎ └─child: RenderConstrainedBox#21f3a\n'
' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' ╎ parentData: <none> (can use size)\n'
' ╎ │ size)\n' ' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ size: Size(800.0, 600.0)\n'
' ╎ │ size: Size(800.0, 600.0)\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\n'
' ╎ │ maxWidth: 0.0\n'
' ╎ │ maxHeight: 0.0\n'
' ╎ │\n'
' ╎ └─child: RenderConstrainedBox#dffe5\n'
' ╎ parentData: <none> (can use size)\n'
' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ size: Size(800.0, 600.0)\n'
' ╎ additionalConstraints: BoxConstraints(biggest)\n'
' ╎\n' ' ╎\n'
' ╎╌offstage 1: RenderLimitedBox#b6f09 NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎╌offstage 1: RenderLimitedBox#62c8c NEEDS-LAYOUT NEEDS-PAINT\n'
' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0)\n' ' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0)\n'
' ╎ │ constraints: MISSING\n' ' ╎ │ constraints: MISSING\n'
' ╎ │ size: MISSING\n' ' ╎ │ size: MISSING\n'
' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ │ maxHeight: 0.0\n' ' ╎ │ maxHeight: 0.0\n'
' ╎ │\n' ' ╎ │\n'
' ╎ └─child: RenderConstrainedBox#5a057 NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎ └─child: RenderConstrainedBox#425fa NEEDS-LAYOUT NEEDS-PAINT\n'
' ╎ parentData: <none>\n' ' ╎ parentData: <none>\n'
' ╎ constraints: MISSING\n' ' ╎ constraints: MISSING\n'
' ╎ size: MISSING\n' ' ╎ size: MISSING\n'
' ╎ additionalConstraints: BoxConstraints(biggest)\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\n'
' ╎\n' ' ╎\n'
' └╌offstage 2: RenderLimitedBox#f689e NEEDS-LAYOUT NEEDS-PAINT\n' ' └╌offstage 2: RenderLimitedBox#03ae2 NEEDS-LAYOUT NEEDS-PAINT\n'
' │ parentData: not positioned; offset=Offset(0.0, 0.0)\n' ' │ parentData: not positioned; offset=Offset(0.0, 0.0)\n'
' │ constraints: MISSING\n' ' │ constraints: MISSING\n'
' │ size: MISSING\n' ' │ size: MISSING\n'
' │ maxWidth: 0.0\n' ' │ maxWidth: 0.0\n'
' │ maxHeight: 0.0\n' ' │ maxHeight: 0.0\n'
' │\n' ' │\n'
' └─child: RenderConstrainedBox#c15f0 NEEDS-LAYOUT NEEDS-PAINT\n' ' └─child: RenderConstrainedBox#b4d48 NEEDS-LAYOUT NEEDS-PAINT\n'
' parentData: <none>\n' ' parentData: <none>\n'
' constraints: MISSING\n' ' constraints: MISSING\n'
' size: MISSING\n' ' size: MISSING\n'
' additionalConstraints: BoxConstraints(biggest)\n' ' additionalConstraints: BoxConstraints(biggest)\n',
), ),
); );
}); });
...@@ -698,4 +701,261 @@ void main() { ...@@ -698,4 +701,261 @@ void main() {
expect(find.byKey(root), findsNothing); expect(find.byKey(root), findsNothing);
expect(find.byKey(top), findsOneWidget); expect(find.byKey(top), findsOneWidget);
}); });
testWidgets('OverlayEntries do not rebuild when opaqueness changes', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/45797.
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
final Key bottom = UniqueKey();
final Key middle = UniqueKey();
final Key top = UniqueKey();
final Widget bottomWidget = StatefulTestWidget(key: bottom);
final Widget middleWidget = StatefulTestWidget(key: middle);
final Widget topWidget = StatefulTestWidget(key: top);
final OverlayEntry bottomEntry = OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return bottomWidget;
},
);
final OverlayEntry middleEntry = OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return middleWidget;
},
);
final OverlayEntry topEntry = OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return topWidget;
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
bottomEntry,
middleEntry,
topEntry,
],
),
),
);
// All widgets are onstage.
expect(tester.state<StatefulTestState>(find.byKey(bottom)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(middle)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1);
middleEntry.opaque = true;
await tester.pump();
// Bottom widget is offstage and did not rebuild.
expect(find.byKey(bottom), findsNothing);
expect(tester.state<StatefulTestState>(find.byKey(bottom, skipOffstage: false)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(middle)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1);
});
testWidgets('OverlayEntries do not rebuild when opaque entry is added', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/45797.
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
final Key bottom = UniqueKey();
final Key middle = UniqueKey();
final Key top = UniqueKey();
final Widget bottomWidget = StatefulTestWidget(key: bottom);
final Widget middleWidget = StatefulTestWidget(key: middle);
final Widget topWidget = StatefulTestWidget(key: top);
final OverlayEntry bottomEntry = OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return bottomWidget;
},
);
final OverlayEntry middleEntry = OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext context) {
return middleWidget;
},
);
final OverlayEntry topEntry = OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return topWidget;
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
bottomEntry,
topEntry,
],
),
),
);
// Both widgets are onstage.
expect(tester.state<StatefulTestState>(find.byKey(bottom)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1);
overlayKey.currentState.rearrange(<OverlayEntry>[
bottomEntry, middleEntry, topEntry,
]);
await tester.pump();
// Bottom widget is offstage and did not rebuild.
expect(find.byKey(bottom), findsNothing);
expect(tester.state<StatefulTestState>(find.byKey(bottom, skipOffstage: false)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(middle)).rebuildCount, 1);
expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1);
});
testWidgets('entries below opaque entries are ignored for hit testing', (WidgetTester tester) async {
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
int bottomTapCount = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
bottomTapCount++;
},
);
},
),
],
),
),
);
expect(bottomTapCount, 0);
await tester.tap(find.byKey(overlayKey));
expect(bottomTapCount, 1);
overlayKey.currentState.insert(OverlayEntry(
maintainState: true,
opaque: true,
builder: (BuildContext context) {
return Container();
},
));
await tester.pump();
// Bottom is offstage and does not receive tap events.
expect(find.byType(GestureDetector), findsNothing);
expect(find.byType(GestureDetector, skipOffstage: false), findsOneWidget);
await tester.tap(find.byKey(overlayKey));
expect(bottomTapCount, 1);
int topTapCount = 0;
overlayKey.currentState.insert(OverlayEntry(
maintainState: true,
opaque: true,
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
topTapCount++;
},
);
},
));
await tester.pump();
expect(topTapCount, 0);
await tester.tap(find.byKey(overlayKey));
expect(topTapCount, 1);
expect(bottomTapCount, 1);
});
testWidgets('Semantics of entries below opaque entries are ignored', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
maintainState: true,
builder: (BuildContext context) {
return const Text('bottom');
},
),
OverlayEntry(
maintainState: true,
opaque: true,
builder: (BuildContext context) {
return const Text('top');
},
),
],
),
),
);
expect(find.text('bottom'), findsNothing);
expect(find.text('bottom', skipOffstage: false), findsOneWidget);
expect(find.text('top'), findsOneWidget);
expect(semantics, includesNodeWith(label: 'top'));
expect(semantics, isNot(includesNodeWith(label: 'bottom')));
semantics.dispose();
});
testWidgets('Can used Positioned within OverlayEntry', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return const Positioned(
left: 145,
top: 123,
child: Text('positioned child'),
);
},
),
],
),
),
);
expect(tester.getTopLeft(find.text('positioned child')), const Offset(145, 123));
});
}
class StatefulTestWidget extends StatefulWidget {
const StatefulTestWidget({Key key}) : super(key: key);
@override
State<StatefulTestWidget> createState() => StatefulTestState();
}
class StatefulTestState extends State<StatefulTestWidget> {
int rebuildCount = 0;
@override
Widget build(BuildContext context) {
rebuildCount += 1;
return Container();
}
} }
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