Unverified Commit 8eecdbe8 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

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

parent b530111c
...@@ -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() {
...@@ -442,28 +451,32 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -442,28 +451,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,
); );
} }
...@@ -476,36 +489,50 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin { ...@@ -476,36 +489,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].
/// ///
/// For convenience, it is legal to use [Positioned] widgets around the offstage /// The first [skipCount] children are considered "offstage".
/// widgets. class _Theatre extends MultiChildRenderObjectWidget {
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;
...@@ -513,186 +540,268 @@ class _TheatreElement extends RenderObjectElement { ...@@ -513,186 +540,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 {
assert(slot == null || slot is Element);
renderObject.insert(child, after: slot?.renderObject as RenderBox);
} }
Alignment _resolvedAlignment;
void _resolve() {
if (_resolvedAlignment != null)
return;
_resolvedAlignment = AlignmentDirectional.topStart.resolve(textDirection);
} }
@override void _markNeedResolution() {
void moveChildRenderObject(RenderBox child, dynamic slot) { _resolvedAlignment = null;
if (slot == _onstageSlot) { markNeedsLayout();
renderObject.remove(child); }
assert(child is RenderStack);
renderObject.child = child as RenderStack; TextDirection get textDirection => _textDirection;
} else { TextDirection _textDirection;
assert(slot == null || slot is Element); set textDirection(TextDirection value) {
if (renderObject.child == child) { if (_textDirection == value)
renderObject.child = null; return;
renderObject.insert(child, after: slot?.renderObject as RenderBox); _textDirection = value;
} else { _markNeedResolution();
renderObject.move(child, after: slot?.renderObject as RenderBox);
} }
int get skipCount => _skipCount;
int _skipCount;
set skipCount(int value) {
assert(value != null);
if (_skipCount != value) {
_skipCount = value;
markNeedsLayout();
} }
} }
@override RenderBox get _firstOnstageChild {
void removeChildRenderObject(RenderBox child) { if (skipCount == super.childCount) {
if (renderObject.child == child) { return null;
renderObject.child = null;
} else {
renderObject.remove(child);
} }
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
void visitChildren(ElementVisitor visitor) { double computeMinIntrinsicWidth(double height) {
if (_onstage != null) return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
visitor(_onstage);
for (final Element child in _offstage) {
if (!_forgottenOffstageChildren.contains(child))
visitor(child);
} }
@override
double computeMaxIntrinsicWidth(double height) {
return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
} }
@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; }
@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 { } else {
assert(_offstage.contains(child)); result = candidate;
assert(!_forgottenOffstageChildren.contains(child));
_forgottenOffstageChildren.add(child);
} }
return true; }
child = childParentData.nextSibling;
}
return result;
} }
@override @override
void mount(Element parent, dynamic newSlot) { bool get sizedByParent => true;
super.mount(parent, newSlot);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot); @override
_offstage = List<Element>(widget.offstage.length); void performResize() {
Element previousChild; size = constraints.biggest;
for (int i = 0; i < _offstage.length; i += 1) { assert(size.isFinite);
final Element newChild = inflateWidget(widget.offstage[i], previousChild);
_offstage[i] = newChild;
previousChild = newChild;
}
} }
@override @override
void update(_Theatre newWidget) { void performLayout() {
super.update(newWidget); _hasVisualOverflow = false;
assert(widget == newWidget);
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot); if (_onstageChildCount == 0) {
_offstage = updateChildren(_offstage, widget.offstage, forgottenChildren: _forgottenOffstageChildren); return;
_forgottenOffstageChildren.clear();
} }
}
// A render object which lays out and paints one subtree while keeping a list _resolve();
// of other subtrees alive but not laid out or painted (the "zombie" children). assert(_resolvedAlignment != null);
//
// The subtree that is laid out and painted must be a [RenderStack]. // Same BoxConstraints as used by RenderStack for StackFit.expand.
// final BoxConstraints nonPositionedConstraints = BoxConstraints.tight(constraints.biggest);
// This class uses [StackParentData] objects for its parent data so that the
// children of its primary subtree's stack can be moved to this object's list RenderBox child = _firstOnstageChild;
// of zombie children without changing their parent data objects. while (child != null) {
class _RenderTheatre extends RenderBox final StackParentData childParentData = child.parentData as StackParentData;
with RenderObjectWithChildMixin<RenderStack>, RenderProxyBoxMixin<RenderStack>,
ContainerRenderObjectMixin<RenderBox, 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
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> children = <DiagnosticsNode>[ final List<DiagnosticsNode> offstageChildren = <DiagnosticsNode>[];
if (child != null) child.toDiagnosticsNode(name: 'onstage'), final List<DiagnosticsNode> onstageChildren = <DiagnosticsNode>[];
];
if (firstChild != null) { int count = 1;
bool onstage = false;
RenderBox child = firstChild; RenderBox child = firstChild;
final RenderBox firstOnstageChild = _firstOnstageChild;
while (child != null) {
if (child == firstOnstageChild) {
onstage = true;
count = 1;
}
int count = 1; if (onstage) {
while (true) { onstageChildren.add(
children.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 {
children.add( 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'
......
...@@ -529,12 +529,13 @@ void main() { ...@@ -529,12 +529,13 @@ 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(8).join('\n'), lines.take(9).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: [_LocalizationsScope-[GlobalKey#00000]], state:\n' 'dependencies: [TickerMode,\n'
'_StepperState#00000):\n' '_LocalizationsScope-[GlobalKey#6b31b]], state:\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'
......
...@@ -1186,6 +1186,33 @@ void main() { ...@@ -1186,6 +1186,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> {
...@@ -1199,3 +1226,20 @@ class NoAnimationPageRoute extends PageRouteBuilder<void> { ...@@ -1199,3 +1226,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,22 +41,28 @@ void main() { ...@@ -36,22 +41,28 @@ 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'
' │ skipCount: 0\n'
' │ textDirection: ltr\n'
' │\n' ' │\n'
' ├─onstage: RenderStack#39819\n' ' ├─onstage 1: RenderLimitedBox#bb803\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' ' │ │\n'
' ╎ │ overflow: clip\n' ' │ └─child: RenderConstrainedBox#62707\n'
' ╎ │\n' ' │ parentData: <none> (can use size)\n'
' ╎ └─child 1: RenderLimitedBox#d1448\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\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'
...@@ -59,7 +70,7 @@ void main() { ...@@ -59,7 +70,7 @@ void main() {
' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ │ maxHeight: 0.0\n' ' ╎ │ maxHeight: 0.0\n'
' ╎ │\n' ' ╎ │\n'
' ╎ └─child: RenderConstrainedBox#e8b87\n' ' ╎ └─child: RenderConstrainedBox#69c48\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'
...@@ -103,22 +114,14 @@ void main() { ...@@ -103,22 +114,14 @@ 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'
' ╎ │ 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'
...@@ -126,37 +129,37 @@ void main() { ...@@ -126,37 +129,37 @@ void main() {
' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxWidth: 0.0\n'
' ╎ │ maxHeight: 0.0\n' ' ╎ │ maxHeight: 0.0\n'
' ╎ │\n' ' ╎ │\n'
' ╎ └─child: RenderConstrainedBox#dffe5\n' ' ╎ └─child: RenderConstrainedBox#21f3a\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#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