From 52c665d23fcf44b4dc2236448833ef8e47d3d92c Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 23 Jan 2020 11:32:04 -0800 Subject: [PATCH] Revert "Do not rebuild Routes when a new opaque Route is pushed on top (#48900)" (#49366) This reverts commit 8eecdbe823ea09dd723a8af34c7c275e5536d728. --- packages/flutter/lib/src/rendering/stack.dart | 102 ++-- packages/flutter/lib/src/widgets/overlay.dart | 487 +++++++----------- .../cupertino/nav_bar_transition_test.dart | 7 +- .../flutter/test/material/debug_test.dart | 4 +- .../flutter/test/material/stepper_test.dart | 7 +- .../flutter/test/widgets/navigator_test.dart | 44 -- .../flutter/test/widgets/overlay_test.dart | 368 ++----------- 7 files changed, 297 insertions(+), 722 deletions(-) diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index c72c1d146e..502588b095 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -425,8 +425,7 @@ class RenderStack extends RenderBox } } - /// Helper function for calculating the intrinsics metrics of a Stack. - static double getIntrinsicDimension(RenderBox firstChild, double mainChildSizeGetter(RenderBox child)) { + double _getIntrinsicDimension(double mainChildSizeGetter(RenderBox child)) { double extent = 0.0; RenderBox child = firstChild; while (child != null) { @@ -441,22 +440,22 @@ class RenderStack extends RenderBox @override double computeMinIntrinsicWidth(double height) { - return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); + return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicWidth(height)); } @override double computeMaxIntrinsicWidth(double height) { - return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); + return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicWidth(height)); } @override double computeMinIntrinsicHeight(double width) { - return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width)); + return _getIntrinsicDimension((RenderBox child) => child.getMinIntrinsicHeight(width)); } @override double computeMaxIntrinsicHeight(double width) { - return getIntrinsicDimension(firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width)); + return _getIntrinsicDimension((RenderBox child) => child.getMaxIntrinsicHeight(width)); } @override @@ -464,57 +463,6 @@ class RenderStack extends RenderBox 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 void performLayout() { _resolve(); @@ -579,7 +527,45 @@ class RenderStack extends RenderBox if (!childParentData.isPositioned) { childParentData.offset = _resolvedAlignment.alongOffset(size - child.size as Offset); } 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); diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 3ea523ce2d..609ad2f6ec 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -4,13 +4,13 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'basic.dart'; +import 'debug.dart'; import 'framework.dart'; import 'ticker_provider.dart'; @@ -115,7 +115,7 @@ class OverlayEntry { } OverlayState _overlay; - final GlobalKey<_OverlayEntryWidgetState> _key = GlobalKey<_OverlayEntryWidgetState>(); + final GlobalKey<_OverlayEntryState> _key = GlobalKey<_OverlayEntryState>(); /// Remove this entry from the overlay. /// @@ -152,30 +152,21 @@ class OverlayEntry { String toString() => '${describeIdentity(this)}(opaque: $opaque; maintainState: $maintainState)'; } -class _OverlayEntryWidget extends StatefulWidget { - const _OverlayEntryWidget({ - @required Key key, - @required this.entry, - this.tickerEnabled = true, - }) : assert(key != null), - assert(entry != null), - assert(tickerEnabled != null), - super(key: key); +class _OverlayEntry extends StatefulWidget { + _OverlayEntry(this.entry) + : assert(entry != null), + super(key: entry._key); final OverlayEntry entry; - final bool tickerEnabled; @override - _OverlayEntryWidgetState createState() => _OverlayEntryWidgetState(); + _OverlayEntryState createState() => _OverlayEntryState(); } -class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { +class _OverlayEntryState extends State<_OverlayEntry> { @override Widget build(BuildContext context) { - return TickerMode( - enabled: widget.tickerEnabled, - child: widget.entry.builder(context), - ); + return widget.entry.builder(context); } void _markNeedsBuild() { @@ -461,32 +452,28 @@ class OverlayState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - // This list is filled backwards and then reversed below before - // it is added to the tree. - final List children = []; + // These lists are filled backwards. For the offstage children that + // does not matter since they aren't rendered, but for the onstage + // children we reverse the list below before adding it to the tree. + final List onstageChildren = []; + final List offstageChildren = []; bool onstage = true; - int onstageCount = 0; for (int i = _entries.length - 1; i >= 0; i -= 1) { final OverlayEntry entry = _entries[i]; if (onstage) { - onstageCount += 1; - children.add(_OverlayEntryWidget( - key: entry._key, - entry: entry, - )); + onstageChildren.add(_OverlayEntry(entry)); if (entry.opaque) onstage = false; } else if (entry.maintainState) { - children.add(_OverlayEntryWidget( - key: entry._key, - entry: entry, - tickerEnabled: false, - )); + offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry))); } } return _Theatre( - skipCount: children.length - onstageCount, - children: children.reversed.toList(growable: false), + onstage: Stack( + fit: StackFit.expand, + children: onstageChildren.reversed.toList(growable: false), + ), + offstage: offstageChildren, ); } @@ -499,50 +486,36 @@ class OverlayState extends State with TickerProviderStateMixin { } } -/// Special version of a [Stack], that doesn't layout and render the first -/// [skipCount] children. +/// A widget that has one [onstage] child which is visible, and one or more +/// [offstage] widgets which are kept alive, and are built, but are not laid out +/// or painted. /// -/// The first [skipCount] children are considered "offstage". -class _Theatre extends MultiChildRenderObjectWidget { +/// The onstage widget must be a [Stack]. +/// +/// For convenience, it is legal to use [Positioned] widgets around the offstage +/// widgets. +class _Theatre extends RenderObjectWidget { _Theatre({ - Key key, - this.skipCount = 0, - List children = const [], - }) : assert(skipCount != null), - assert(skipCount >= 0), - assert(children != null), - assert(children.length >= skipCount), - super(key: key, children: children); + this.onstage, + @required this.offstage, + }) : assert(offstage != null), + assert(!offstage.any((Widget child) => child == null)); - final int skipCount; + final Stack onstage; + + final List offstage; @override _TheatreElement createElement() => _TheatreElement(this); @override - _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)); - } + _RenderTheatre createRenderObject(BuildContext context) => _RenderTheatre(); } -class _TheatreElement extends MultiChildRenderObjectElement { - _TheatreElement(_Theatre widget) : super(widget); +class _TheatreElement extends RenderObjectElement { + _TheatreElement(_Theatre widget) + : assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage)), + super(widget); @override _Theatre get widget => super.widget as _Theatre; @@ -550,268 +523,186 @@ class _TheatreElement extends MultiChildRenderObjectElement { @override _RenderTheatre get renderObject => super.renderObject as _RenderTheatre; + Element _onstage; + static final Object _onstageSlot = Object(); + + List _offstage; + final Set _forgottenOffstageChildren = HashSet(); + + @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 void debugVisitOnstageChildren(ElementVisitor visitor) { - assert(children.length >= widget.skipCount); - children.skip(widget.skipCount).forEach(visitor); + if (_onstage != null) + 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(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 { - _RenderTheatre({ - List children, - @required TextDirection textDirection, - int skipCount = 0, - }) : assert(skipCount != null), - assert(skipCount >= 0), - assert(textDirection != null), - _textDirection = textDirection, - _skipCount = skipCount { - addAll(children); - } - - bool _hasVisualOverflow = false; +// A render object which lays out and paints one subtree while keeping a list +// of other subtrees alive but not laid out or painted (the "zombie" children). +// +// The subtree that is laid out and painted must be a [RenderStack]. +// +// 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 +// of zombie children without changing their parent data objects. +class _RenderTheatre extends RenderBox + with RenderObjectWithChildMixin, RenderProxyBoxMixin, + ContainerRenderObjectMixin { @override - void setupParentData(RenderBox child) { + void setupParentData(RenderObject child) { if (child.parentData is! StackParentData) child.parentData = StackParentData(); } - Alignment _resolvedAlignment; - - 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(); - } - } - - 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; + // Because both RenderObjectWithChildMixin and ContainerRenderObjectMixin + // define redepthChildren, visitChildren and debugDescribeChildren and don't + // call super, we have to define them again here to make sure the work of both + // is done. + // + // 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 + // 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 - double computeMinIntrinsicWidth(double height) { - return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); + void redepthChildren() { + if (child != null) + redepthChild(child); + super.redepthChildren(); } @override - double computeMaxIntrinsicWidth(double height) { - return RenderStack.getIntrinsicDimension(_firstOnstageChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); - } - - @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) { + void visitChildren(RenderObjectVisitor visitor) { + if (child != null) visitor(child); - 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)); + super.visitChildren(visitor); } @override List debugDescribeChildren() { - final List offstageChildren = []; - final List onstageChildren = []; + final List children = [ + if (child != null) child.toDiagnosticsNode(name: 'onstage'), + ]; - int count = 1; - bool onstage = false; - RenderBox child = firstChild; - final RenderBox firstOnstageChild = _firstOnstageChild; - while (child != null) { - if (child == firstOnstageChild) { - onstage = true; - count = 1; - } + if (firstChild != null) { + RenderBox child = firstChild; - if (onstage) { - onstageChildren.add( - child.toDiagnosticsNode( - name: 'onstage $count', - ), - ); - } else { - offstageChildren.add( + int count = 1; + while (true) { + children.add( child.toDiagnosticsNode( name: 'offstage $count', style: DiagnosticsTreeStyle.offstage, ), ); + if (child == lastChild) + break; + final StackParentData childParentData = child.parentData as StackParentData; + child = childParentData.nextSibling; + count += 1; } - - final StackParentData childParentData = child.parentData as StackParentData; - child = childParentData.nextSibling; - count += 1; - } - - return [ - ...onstageChildren, - if (offstageChildren.isNotEmpty) - ...offstageChildren - else + } else { + children.add( DiagnosticsNode.message( 'no offstage children', style: DiagnosticsTreeStyle.offstage, ), - ]; + ); + } + return children; + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + if (child != null) + visitor(child); } } diff --git a/packages/flutter/test/cupertino/nav_bar_transition_test.dart b/packages/flutter/test/cupertino/nav_bar_transition_test.dart index f65d4b00ec..dda551e102 100644 --- a/packages/flutter/test/cupertino/nav_bar_transition_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_transition_test.dart @@ -72,9 +72,12 @@ CupertinoPageScaffold scaffoldForNavBar(Widget navBar) { } Finder flying(WidgetTester tester, Finder finder) { - final ContainerRenderObjectMixin theater = tester.renderObject(find.byType(Overlay)); + final RenderObjectWithChildMixin theater = + tester.renderObject(find.byType(Overlay)); + final RenderStack theaterStack = theater.child; final Finder lastOverlayFinder = find.byElementPredicate((Element element) { - return element is RenderObjectElement && element.renderObject == theater.lastChild; + return element is RenderObjectElement && + element.renderObject == theaterStack.lastChild; }); assert( diff --git a/packages/flutter/test/material/debug_test.dart b/packages/flutter/test/material/debug_test.dart index 8ab0c9a9f6..08743715af 100644 --- a/packages/flutter/test/material/debug_test.dart +++ b/packages/flutter/test/material/debug_test.dart @@ -132,8 +132,8 @@ void main() { ' Offstage\n' ' _ModalScopeStatus\n' ' _ModalScope-[LabeledGlobalKey<_ModalScopeState>#969b7]\n' - ' TickerMode\n' - ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#545d0]\n' + ' _OverlayEntry-[LabeledGlobalKey<_OverlayEntryState>#7a3ae]\n' + ' Stack\n' ' _Theatre\n' ' Overlay-[LabeledGlobalKey#31a52]\n' ' _FocusMarker\n' diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index bd6266b6d6..13ee688642 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -529,13 +529,12 @@ void main() { // which will change depending on where the test is run. expect(lines.length, greaterThan(7)); expect( - lines.take(9).join('\n'), + lines.take(8).join('\n'), equalsIgnoringHashCodes( '══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════\n' 'The following assertion was thrown building Stepper(dirty,\n' - 'dependencies: [TickerMode,\n' - '_LocalizationsScope-[GlobalKey#6b31b]], state:\n' - '_StepperState#1bf00):\n' + 'dependencies: [_LocalizationsScope-[GlobalKey#00000]], state:\n' + '_StepperState#00000):\n' 'Steppers must not be nested.\n' 'The material specification advises that one should avoid\n' 'embedding steppers within steppers.\n' diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index aace92daf5..5522c1179d 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -1186,33 +1186,6 @@ void main() { expect(find.byKey(const ValueKey('/A/B')), findsNothing); // popped expect(find.byKey(const ValueKey('/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 navigator = GlobalKey(); - final Key bottomRoute = UniqueKey(); - final Key topRoute = UniqueKey(); - await tester.pumpWidget( - MaterialApp( - navigatorKey: navigator, - routes: { - '/' : (BuildContext context) => StatefulTestWidget(key: bottomRoute), - '/a': (BuildContext context) => StatefulTestWidget(key: topRoute), - }, - ), - ); - expect(tester.state(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(find.byKey(bottomRoute, skipOffstage: false)).rebuildCount, 1); - - expect(tester.state(find.byKey(topRoute)).rebuildCount, 1); - }); } class NoAnimationPageRoute extends PageRouteBuilder { @@ -1226,20 +1199,3 @@ class NoAnimationPageRoute extends PageRouteBuilder { return super.createAnimationController()..value = 1.0; } } - -class StatefulTestWidget extends StatefulWidget { - const StatefulTestWidget({Key key}) : super(key: key); - - @override - State createState() => StatefulTestState(); -} - -class StatefulTestState extends State { - int rebuildCount = 0; - - @override - Widget build(BuildContext context) { - rebuildCount += 1; - return Container(); - } -} diff --git a/packages/flutter/test/widgets/overlay_test.dart b/packages/flutter/test/widgets/overlay_test.dart index 21b60d831f..0453fcd861 100644 --- a/packages/flutter/test/widgets/overlay_test.dart +++ b/packages/flutter/test/widgets/overlay_test.dart @@ -6,8 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; -import 'semantics_tester.dart'; - void main() { testWidgets('OverflowEntries context contains Overlay', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); @@ -27,9 +25,6 @@ void main() { return Container(); }, ), - OverlayEntry( - builder: (BuildContext context) => Container(), - ) ], ), ), @@ -41,42 +36,36 @@ void main() { expect( theater.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( - '_RenderTheatre#744c9\n' - ' │ parentData: \n' - ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' - ' │ size: Size(800.0, 600.0)\n' - ' │ skipCount: 0\n' - ' │ textDirection: ltr\n' - ' │\n' - ' ├─onstage 1: RenderLimitedBox#bb803\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' - ' │ │ maxWidth: 0.0\n' - ' │ │ maxHeight: 0.0\n' - ' │ │\n' - ' │ └─child: RenderConstrainedBox#62707\n' - ' │ parentData: (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' - ' ├─onstage 2: RenderLimitedBox#af5f1\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' - ' ╎ │ maxWidth: 0.0\n' - ' ╎ │ maxHeight: 0.0\n' - ' ╎ │\n' - ' ╎ └─child: RenderConstrainedBox#69c48\n' - ' ╎ parentData: (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' + '_RenderTheatre#f5cf2\n' + ' │ parentData: \n' + ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' + ' │ size: Size(800.0, 600.0)\n' + ' │\n' + ' ├─onstage: RenderStack#39819\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#d1448\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' + ' ╎ │ maxWidth: 0.0\n' + ' ╎ │ maxHeight: 0.0\n' + ' ╎ │\n' + ' ╎ └─child: RenderConstrainedBox#e8b87\n' + ' ╎ parentData: (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' ), ); }); @@ -114,52 +103,60 @@ void main() { expect( theater.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( - '_RenderTheatre#385b3\n' + '_RenderTheatre#b22a8\n' ' │ parentData: \n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' - ' │ skipCount: 2\n' - ' │ textDirection: ltr\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' - ' ╎ │ maxWidth: 0.0\n' - ' ╎ │ maxHeight: 0.0\n' + ' ╎ │ alignment: AlignmentDirectional.topStart\n' + ' ╎ │ textDirection: ltr\n' + ' ╎ │ fit: expand\n' + ' ╎ │ overflow: clip\n' ' ╎ │\n' - ' ╎ └─child: RenderConstrainedBox#21f3a\n' - ' ╎ parentData: (can use size)\n' - ' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n' - ' ╎ size: Size(800.0, 600.0)\n' - ' ╎ additionalConstraints: BoxConstraints(biggest)\n' + ' ╎ └─child 1: RenderLimitedBox#ca15b\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' + ' ╎ │ maxWidth: 0.0\n' + ' ╎ │ maxHeight: 0.0\n' + ' ╎ │\n' + ' ╎ └─child: RenderConstrainedBox#dffe5\n' + ' ╎ parentData: (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' - ' ╎╌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' ' ╎ │ constraints: MISSING\n' ' ╎ │ size: MISSING\n' ' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxHeight: 0.0\n' ' ╎ │\n' - ' ╎ └─child: RenderConstrainedBox#425fa NEEDS-LAYOUT NEEDS-PAINT\n' + ' ╎ └─child: RenderConstrainedBox#5a057 NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎ parentData: \n' ' ╎ constraints: MISSING\n' ' ╎ size: MISSING\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\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' ' │ constraints: MISSING\n' ' │ size: MISSING\n' ' │ maxWidth: 0.0\n' ' │ maxHeight: 0.0\n' ' │\n' - ' └─child: RenderConstrainedBox#b4d48 NEEDS-LAYOUT NEEDS-PAINT\n' + ' └─child: RenderConstrainedBox#c15f0 NEEDS-LAYOUT NEEDS-PAINT\n' ' parentData: \n' ' constraints: 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(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 overlayKey = GlobalKey(); - 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: [ - bottomEntry, - middleEntry, - topEntry, - ], - ), - ), - ); - - // All widgets are onstage. - expect(tester.state(find.byKey(bottom)).rebuildCount, 1); - expect(tester.state(find.byKey(middle)).rebuildCount, 1); - expect(tester.state(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(find.byKey(bottom, skipOffstage: false)).rebuildCount, 1); - expect(tester.state(find.byKey(middle)).rebuildCount, 1); - expect(tester.state(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 overlayKey = GlobalKey(); - 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: [ - bottomEntry, - topEntry, - ], - ), - ), - ); - - // Both widgets are onstage. - expect(tester.state(find.byKey(bottom)).rebuildCount, 1); - expect(tester.state(find.byKey(top)).rebuildCount, 1); - - overlayKey.currentState.rearrange([ - bottomEntry, middleEntry, topEntry, - ]); - await tester.pump(); - - // Bottom widget is offstage and did not rebuild. - expect(find.byKey(bottom), findsNothing); - expect(tester.state(find.byKey(bottom, skipOffstage: false)).rebuildCount, 1); - expect(tester.state(find.byKey(middle)).rebuildCount, 1); - expect(tester.state(find.byKey(top)).rebuildCount, 1); - }); - - testWidgets('entries below opaque entries are ignored for hit testing', (WidgetTester tester) async { - final GlobalKey overlayKey = GlobalKey(); - int bottomTapCount = 0; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Overlay( - key: overlayKey, - initialEntries: [ - 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 overlayKey = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Overlay( - key: overlayKey, - initialEntries: [ - 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( - 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 createState() => StatefulTestState(); -} - -class StatefulTestState extends State { - int rebuildCount = 0; - - @override - Widget build(BuildContext context) { - rebuildCount += 1; - return Container(); - } }