Revert "Do not rebuild Routes when a new opaque Route is pushed on top (#48900)" (#49366)

This reverts commit 8eecdbe823ea09dd723a8af34c7c275e5536d728.
This commit is contained in:
Maurice Parrish 2020-01-23 11:32:04 -08:00 committed by GitHub
parent a1fa1a3d26
commit 52c665d23f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 297 additions and 722 deletions

View File

@ -425,8 +425,7 @@ class RenderStack extends RenderBox
} }
} }
/// Helper function for calculating the intrinsics metrics of a Stack. double _getIntrinsicDimension(double mainChildSizeGetter(RenderBox child)) {
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) {
@ -441,22 +440,22 @@ class RenderStack extends RenderBox
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicWidth(height));
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicWidth(height));
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width)); return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicHeight(width));
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width)); return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicHeight(width));
} }
@override @override
@ -464,57 +463,6 @@ 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();
@ -579,7 +527,45 @@ 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 {
_hasVisualOverflow = layoutPositionedChild(child, childParentData, size, _resolvedAlignment) || _hasVisualOverflow; 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 = _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);

View File

@ -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 {
} }
OverlayState _overlay; OverlayState _overlay;
final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>(); final GlobalKey<_OverlayEntryState> _key = GlobalKey<_OverlayEntryState>();
/// Remove this entry from the overlay. /// Remove this entry from the overlay.
/// ///
@ -152,30 +152,21 @@ class OverlayEntry {
String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)';
} }
class _OverlayEntryWidget extends StatefulWidget { class _OverlayEntry extends StatefulWidget {
const _OverlayEntryWidget({ _OverlayEntry(this.entry)
@required Key key, : assert(entry != null),
@required this.entry, super(key: entry._key);
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
_OverlayEntryWidgetState createState() => _OverlayEntryWidgetState(); _OverlayEntryState createState() => _OverlayEntryState();
} }
class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { class _OverlayEntryState extends State<_OverlayEntry> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TickerMode( return widget.entry.builder(context);
enabled: widget.tickerEnabled,
child: widget.entry.builder(context),
);
} }
void _markNeedsBuild() { void _markNeedsBuild() {
@ -461,32 +452,28 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This list is filled backwards and then reversed below before // These lists are filled backwards. For the offstage children that
// it is added to the tree. // does not matter since they aren't rendered, but for the onstage
final List<Widget> children = <Widget>[]; // children we reverse the list below before adding it to the tree.
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) {
onstageCount += 1; onstageChildren.add(_OverlayEntry(entry));
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) {
children.add(_OverlayEntryWidget( offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
key: entry._key,
entry: entry,
tickerEnabled: false,
));
} }
} }
return _Theatre( return _Theatre(
skipCount: children.length - onstageCount, onstage: Stack(
children: children.reversed.toList(growable: false), fit: StackFit.expand,
children: onstageChildren.reversed.toList(growable: false),
),
offstage: offstageChildren,
); );
} }
@ -499,50 +486,36 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
} }
} }
/// Special version of a [Stack], that doesn't layout and render the first /// A widget that has one [onstage] child which is visible, and one or more
/// [skipCount] children. /// [offstage] widgets which are kept alive, and are built, but are not laid out
/// or painted.
/// ///
/// The first [skipCount] children are considered "offstage". /// The onstage widget must be a [Stack].
class _Theatre extends MultiChildRenderObjectWidget { ///
/// For convenience, it is legal to use [Positioned] widgets around the offstage
/// widgets.
class _Theatre extends RenderObjectWidget {
_Theatre({ _Theatre({
Key key, this.onstage,
this.skipCount = 0, @required this.offstage,
List<Widget> children = const <Widget>[], }) : assert(offstage != null),
}) : assert(skipCount != null), assert(!offstage.any((Widget child) => child == null));
assert(skipCount >= 0),
assert(children != null),
assert(children.length >= skipCount),
super(key: key, children: children);
final int skipCount; final Stack onstage;
final List<Widget> offstage;
@override @override
_TheatreElement createElement() => _TheatreElement(this); _TheatreElement createElement() => _TheatreElement(this);
@override @override
_RenderTheatre createRenderObject(BuildContext context) { _RenderTheatre createRenderObject(BuildContext context) => _RenderTheatre();
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 MultiChildRenderObjectElement { class _TheatreElement extends RenderObjectElement {
_TheatreElement(_Theatre widget) : super(widget); _TheatreElement(_Theatre widget)
: assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage)),
super(widget);
@override @override
_Theatre get widget => super.widget as _Theatre; _Theatre get widget => super.widget as _Theatre;
@ -550,268 +523,186 @@ class _TheatreElement extends MultiChildRenderObjectElement {
@override @override
_RenderTheatre get renderObject => super.renderObject as _RenderTheatre; _RenderTheatre get renderObject => super.renderObject as _RenderTheatre;
Element _onstage;
static final Object _onstageSlot = Object();
List<Element> _offstage;
final Set<Element> _forgottenOffstageChildren = HashSet<Element>();
@override
void insertChildRenderObject(RenderBox child, dynamic slot) {
assert(renderObject.debugValidateChild(child));
if (slot == _onstageSlot) {
assert(child is RenderStack);
renderObject.child = child as RenderStack;
} else {
assert(slot == null || slot is Element);
renderObject.insert(child, after: slot?.renderObject as RenderBox);
}
}
@override
void moveChildRenderObject(RenderBox child, dynamic slot) {
if (slot == _onstageSlot) {
renderObject.remove(child);
assert(child is RenderStack);
renderObject.child = child as RenderStack;
} else {
assert(slot == null || slot is Element);
if (renderObject.child == child) {
renderObject.child = null;
renderObject.insert(child, after: slot?.renderObject as RenderBox);
} else {
renderObject.move(child, after: slot?.renderObject as RenderBox);
}
}
}
@override
void removeChildRenderObject(RenderBox child) {
if (renderObject.child == child) {
renderObject.child = null;
} else {
renderObject.remove(child);
}
}
@override
void visitChildren(ElementVisitor visitor) {
if (_onstage != null)
visitor(_onstage);
for (final Element child in _offstage) {
if (!_forgottenOffstageChildren.contains(child))
visitor(child);
}
}
@override @override
void debugVisitOnstageChildren(ElementVisitor visitor) { void debugVisitOnstageChildren(ElementVisitor visitor) {
assert(children.length >= widget.skipCount); if (_onstage != null)
children.skip(widget.skipCount).forEach(visitor); visitor(_onstage);
}
@override
bool forgetChild(Element child) {
if (child == _onstage) {
_onstage = null;
} else {
assert(_offstage.contains(child));
assert(!_forgottenOffstageChildren.contains(child));
_forgottenOffstageChildren.add(child);
}
return true;
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
_offstage = List<Element>(widget.offstage.length);
Element previousChild;
for (int i = 0; i < _offstage.length; i += 1) {
final Element newChild = inflateWidget(widget.offstage[i], previousChild);
_offstage[i] = newChild;
previousChild = newChild;
}
}
@override
void update(_Theatre newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
_offstage = updateChildren(_offstage, widget.offstage, forgottenChildren: _forgottenOffstageChildren);
_forgottenOffstageChildren.clear();
} }
} }
class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> { // A render object which lays out and paints one subtree while keeping a list
_RenderTheatre({ // of other subtrees alive but not laid out or painted (the "zombie" children).
List<RenderBox> children, //
@required TextDirection textDirection, // The subtree that is laid out and painted must be a [RenderStack].
int skipCount = 0, //
}) : assert(skipCount != null), // This class uses [StackParentData] objects for its parent data so that the
assert(skipCount >= 0), // children of its primary subtree's stack can be moved to this object's list
assert(textDirection != null), // of zombie children without changing their parent data objects.
_textDirection = textDirection, class _RenderTheatre extends RenderBox
_skipCount = skipCount { with RenderObjectWithChildMixin<RenderStack>, RenderProxyBoxMixin<RenderStack>,
addAll(children); ContainerRenderObjectMixin<RenderBox, StackParentData> {
}
bool _hasVisualOverflow = false;
@override @override
void setupParentData(RenderBox child) { void setupParentData(RenderObject child) {
if (child.parentData is! StackParentData) if (child.parentData is! StackParentData)
child.parentData = StackParentData(); child.parentData = StackParentData();
} }
Alignment _resolvedAlignment; // Because both RenderObjectWithChildMixin and ContainerRenderObjectMixin
// define redepthChildren, visitChildren and debugDescribeChildren and don't
void _resolve() { // call super, we have to define them again here to make sure the work of both
if (_resolvedAlignment != null) // is done.
return; //
_resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection); // We chose to put ContainerRenderObjectMixin last in the inheritance chain so
} // that we can call super to hit its more complex definitions of
// redepthChildren and visitChildren, and then duplicate the more trivial
void _markNeedResolution() { // definition from RenderObjectWithChildMixin inline in our version here.
_resolvedAlignment = null; //
markNeedsLayout(); // This code duplication is suboptimal.
} // TODO(ianh): Replace this with a better solution once https://github.com/dart-lang/sdk/issues/27100 is fixed
//
TextDirection get textDirection => _textDirection; // For debugDescribeChildren we just roll our own because otherwise the line
TextDirection _textDirection; // drawings won't really work as well.
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();
}
}
RenderBox get _firstOnstageChild {
if (skipCount == super.childCount) {
return null;
}
RenderBox child = super.firstChild;
for (int toSkip = skipCount; toSkip > 0; toSkip--) {
final StackParentData childParentData = child.parentData as StackParentData;
child = childParentData.nextSibling;
assert(child != null);
}
return child;
}
RenderBox get _lastOnstageChild => skipCount == super.childCount ? null : lastChild;
int get _onstageChildCount => childCount - skipCount;
@override @override
double computeMinIntrinsicWidth(double height) { void redepthChildren() {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); if (child != null)
redepthChild(child);
super.redepthChildren();
} }
@override @override
double computeMaxIntrinsicWidth(double height) { void visitChildren(RenderObjectVisitor visitor) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); if (child != null)
}
@override
double computeMinIntrinsicHeight(double width) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
}
@override
double computeMaxIntrinsicHeight(double width) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!debugNeedsLayout);
double result;
RenderBox child = _firstOnstageChild;
while (child != null) {
assert(!child.debugNeedsLayout);
final StackParentData childParentData = child.parentData as StackParentData;
double candidate = child.getDistanceToActualBaseline(baseline);
if (candidate != null) {
candidate += childParentData.offset.dy;
if (result != null) {
result = math.min(result, candidate);
} else {
result = candidate;
}
}
child = childParentData.nextSibling;
}
return result;
}
@override
bool get sizedByParent => true;
@override
void performResize() {
size = constraints.biggest;
assert(size.isFinite);
}
@override
void performLayout() {
_hasVisualOverflow = false;
if (_onstageChildCount == 0) {
return;
}
_resolve();
assert(_resolvedAlignment != null);
// 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
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
RenderBox child = _lastOnstageChild;
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;
}
@protected
void paintStack(PaintingContext context, Offset offset) {
RenderBox child = _firstOnstageChild;
while (child != null) {
final StackParentData childParentData = child.parentData as StackParentData;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
} else {
paintStack(context, offset);
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
RenderBox child = _firstOnstageChild;
while (child != null) {
visitor(child); visitor(child);
final StackParentData childParentData = child.parentData as StackParentData; super.visitChildren(visitor);
child = childParentData.nextSibling;
}
}
@override
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('skipCount', skipCount));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
} }
@override @override
List<DiagnosticsNode> debugDescribeChildren() { List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[]; final List<DiagnosticsNode> children = <DiagnosticsNode>[
final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[]; if (child != null) child.toDiagnosticsNode(name: 'onstage'),
];
if (firstChild != null) {
RenderBox child = firstChild;
int count = 1; int count = 1;
bool onstage = false; while (true) {
RenderBox child = firstChild; children.add(
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; final StackParentData childParentData = child.parentData as StackParentData;
child = childParentData.nextSibling; child = childParentData.nextSibling;
count += 1; count += 1;
} }
} else {
return <DiagnosticsNode>[ children.add(
...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);
} }
} }

View File

@ -72,9 +72,12 @@ CupertinoPageScaffold scaffoldForNavBar(Widget navBar) {
} }
Finder flying(WidgetTester tester, Finder finder) { Finder flying(WidgetTester tester, Finder finder) {
final ContainerRenderObjectMixin<RenderBox, StackParentData> theater = tester.renderObject(find.byType(Overlay)); final RenderObjectWithChildMixin<RenderStack> theater =
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 && element.renderObject == theater.lastChild; return element is RenderObjectElement &&
element.renderObject == theaterStack.lastChild;
}); });
assert( assert(

View File

@ -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'
' TickerMode\n' ' _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#7a3ae]\n'
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#545d0]\n' ' Stack\n'
' _Theatre\n' ' _Theatre\n'
' Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n' ' Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n'
' _FocusMarker\n' ' _FocusMarker\n'

View File

@ -529,13 +529,12 @@ void main() {
// 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)); expect(lines.length, greaterThan(7));
expect( expect(
lines.take(9).join('\n'), lines.take(8).join('\n'),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n' '══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n'
'The following assertion was thrown building Stepper(dirty,\n' 'The following assertion was thrown building Stepper(dirty,\n'
'dependencies: [TickerMode,\n' 'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n'
'_LocalizationsScope-[GlobalKey#6b31b]], state:\n' '_StepperState#00000):\n'
'_StepperState#1bf00):\n'
'Steppers must not be nested.\n' 'Steppers must not be nested.\n'
'The material specification advises that one should avoid\n' 'The material specification advises that one should avoid\n'
'embedding steppers within steppers.\n' 'embedding steppers within steppers.\n'

View File

@ -1186,33 +1186,6 @@ 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> {
@ -1226,20 +1199,3 @@ 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();
}
}

View File

@ -6,8 +6,6 @@ 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();
@ -27,9 +25,6 @@ void main() {
return Container(); return Container();
}, },
), ),
OverlayEntry(
builder: (BuildContext context) => Container(),
)
], ],
), ),
), ),
@ -41,28 +36,22 @@ void main() {
expect( expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info), theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderTheatre#744c9\n' '_RenderTheatre#f5cf2\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: 0\n'
' │ textDirection: ltr\n'
'\n' '\n'
' ├─onstage 1: RenderLimitedBox#bb803\n' ' ├─onstage: RenderStack#39819\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'
' │ │ maxWidth: 0.0\n' ' ╎ │ alignment: AlignmentDirectional.topStart\n'
' │ │ maxHeight: 0.0\n' ' ╎ │ textDirection: ltr\n'
' │ │\n' ' ╎ │ fit: expand\n'
' │ └─child: RenderConstrainedBox#62707\n' ' ╎ │ overflow: clip\n'
' │ parentData: <none> (can use size)\n' ' ╎ │\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ └─child 1: RenderLimitedBox#d1448\n'
' │ size: Size(800.0, 600.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
'\n'
' ├─onstage 2: RenderLimitedBox#af5f1\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'
@ -70,7 +59,7 @@ void main() {
' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ │ maxHeight: 0.0\n' ' ╎ │ maxHeight: 0.0\n'
' ╎ │\n' ' ╎ │\n'
' ╎ └─child: RenderConstrainedBox#69c48\n' ' ╎ └─child: RenderConstrainedBox#e8b87\n'
' ╎ parentData: <none> (can use 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'
@ -114,14 +103,22 @@ void main() {
expect( expect(
theater.toStringDeep(minLevel: DiagnosticLevel.info), theater.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderTheatre#385b3\n' '_RenderTheatre#b22a8\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 1: RenderLimitedBox#0a77a\n' ' ├─onstage: RenderStack#eab87\n'
' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n'
' ╎ │ size)\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' ╎ │ size: Size(800.0, 600.0)\n'
' ╎ │ alignment: AlignmentDirectional.topStart\n'
' ╎ │ textDirection: ltr\n'
' ╎ │ fit: expand\n'
' ╎ │ overflow: clip\n'
' ╎ │\n'
' ╎ └─child 1: RenderLimitedBox#ca15b\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'
@ -129,37 +126,37 @@ void main() {
' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ │ maxHeight: 0.0\n' ' ╎ │ maxHeight: 0.0\n'
' ╎ │\n' ' ╎ │\n'
'└─child: RenderConstrainedBox#21f3a\n' ' └─child: RenderConstrainedBox#dffe5\n'
' ╎ parentData: <none> (can use 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'
' ╎ additionalConstraints: BoxConstraints(biggest)\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\n'
'\n' '\n'
' ╎╌offstage 1: RenderLimitedBox#62c8c NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎╌offstage 1: RenderLimitedBox#b6f09 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#425fa NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎ └─child: RenderConstrainedBox#5a057 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#03ae2 NEEDS-LAYOUT NEEDS-PAINT\n' ' └╌offstage 2: RenderLimitedBox#f689e 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#b4d48 NEEDS-LAYOUT NEEDS-PAINT\n' ' └─child: RenderConstrainedBox#c15f0 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'
), ),
); );
}); });
@ -701,261 +698,4 @@ 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();
}
} }