Unverified Commit 04ff86f0 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Relax `OverlayPortal` asserts (#129053)

Fixes https://github.com/flutter/flutter/issues/129025

Also 
- simplifies OverlayPortal code a bit and adds an assert.
-  `Tooltip` shouldn't rebuild when hiding/showing the tooltip
parent 99aaff53
...@@ -425,30 +425,27 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -425,30 +425,27 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
// _handleMouseExit. The set is cleared in _handleTapToDismiss, typically when // _handleMouseExit. The set is cleared in _handleTapToDismiss, typically when
// a PointerDown event interacts with some other UI component. // a PointerDown event interacts with some other UI component.
final Set<int> _activeHoveringPointerDevices = <int>{}; final Set<int> _activeHoveringPointerDevices = <int>{};
static bool _isTooltipVisible(AnimationStatus status) {
return switch (status) {
AnimationStatus.completed || AnimationStatus.forward || AnimationStatus.reverse => true,
AnimationStatus.dismissed => false,
};
}
AnimationStatus _animationStatus = AnimationStatus.dismissed; AnimationStatus _animationStatus = AnimationStatus.dismissed;
void _handleStatusChanged(AnimationStatus status) { void _handleStatusChanged(AnimationStatus status) {
assert(mounted); assert(mounted);
final bool entryNeedsUpdating; switch ((_isTooltipVisible(_animationStatus), _isTooltipVisible(status))) {
switch (status) { case (true, false):
case AnimationStatus.dismissed: Tooltip._openedTooltips.remove(this);
entryNeedsUpdating = _animationStatus != AnimationStatus.dismissed; _overlayController.hide();
if (entryNeedsUpdating) { case (false, true):
Tooltip._openedTooltips.remove(this); _overlayController.show();
_overlayController.hide(); Tooltip._openedTooltips.add(this);
} SemanticsService.tooltip(_tooltipMessage);
case AnimationStatus.completed: case (true, true) || (false, false):
case AnimationStatus.forward: break;
case AnimationStatus.reverse:
entryNeedsUpdating = _animationStatus == AnimationStatus.dismissed;
if (entryNeedsUpdating) {
_overlayController.show();
Tooltip._openedTooltips.add(this);
SemanticsService.tooltip(_tooltipMessage);
}
}
if (entryNeedsUpdating) {
setState(() { /* Rebuild to update the OverlayEntry */ });
} }
_animationStatus = status; _animationStatus = status;
} }
...@@ -753,10 +750,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -753,10 +750,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
decoration: widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration, decoration: widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration,
textStyle: widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle, textStyle: widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle,
textAlign: widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign, textAlign: widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign,
animation: CurvedAnimation( animation: CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn),
parent: _controller,
curve: Curves.fastOutSlowIn,
),
target: target, target: target,
verticalOffset: widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset, verticalOffset: widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset,
preferBelow: widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow, preferBelow: widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow,
......
...@@ -48,7 +48,7 @@ Offset positionDependentBox({ ...@@ -48,7 +48,7 @@ Offset positionDependentBox({
// VERTICAL DIRECTION // VERTICAL DIRECTION
final bool fitsBelow = target.dy + verticalOffset + childSize.height <= size.height - margin; final bool fitsBelow = target.dy + verticalOffset + childSize.height <= size.height - margin;
final bool fitsAbove = target.dy - verticalOffset - childSize.height >= margin; final bool fitsAbove = target.dy - verticalOffset - childSize.height >= margin;
final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow); final bool tooltipBelow = fitsAbove == fitsBelow ? preferBelow : fitsBelow;
final double y; final double y;
if (tooltipBelow) { if (tooltipBelow) {
y = math.min(target.dy + verticalOffset, size.height - margin); y = math.min(target.dy + verticalOffset, size.height - margin);
...@@ -56,19 +56,11 @@ Offset positionDependentBox({ ...@@ -56,19 +56,11 @@ Offset positionDependentBox({
y = math.max(target.dy - verticalOffset - childSize.height, margin); y = math.max(target.dy - verticalOffset - childSize.height, margin);
} }
// HORIZONTAL DIRECTION // HORIZONTAL DIRECTION
final double x; final double flexibleSpace = size.width - childSize.width;
if (size.width - margin * 2.0 < childSize.width) { final double x = flexibleSpace <= 2 * margin
x = (size.width - childSize.width) / 2.0; // If there's not enough horizontal space for margin + child, center the
} else { // child.
final double normalizedTargetX = clampDouble(target.dx, margin, size.width - margin); ? flexibleSpace / 2.0
final double edge = margin + childSize.width / 2.0; : clampDouble(target.dx - childSize.width / 2, margin, flexibleSpace - margin);
if (normalizedTargetX < edge) {
x = margin;
} else if (normalizedTargetX > size.width - edge) {
x = size.width - margin - childSize.width;
} else {
x = normalizedTargetX - childSize.width / 2.0;
}
}
return Offset(x, y); return Offset(x, y);
} }
...@@ -2245,6 +2245,39 @@ void main() { ...@@ -2245,6 +2245,39 @@ void main() {
reason: 'Tooltip should NOT be visible when hovered and tapped, when trigger mode is tap', reason: 'Tooltip should NOT be visible when hovered and tapped, when trigger mode is tap',
); );
}); });
testWidgetsWithLeakTracking('Tooltip does not rebuild for fade in / fade out animation', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.square(
dimension: 10.0,
child: Tooltip(
message: tooltipText,
waitDuration: Duration(seconds: 1),
triggerMode: TooltipTriggerMode.longPress,
child: SizedBox.expand(),
),
),
),
),
);
final TooltipState tooltipState = tester.state(find.byType(Tooltip));
final Element element = tooltipState.context as Element;
// The Tooltip widget itself is almost stateless thus doesn't need
// rebuilding.
expect(element.dirty, isFalse);
expect(tooltipState.ensureTooltipVisible(), isTrue);
expect(element.dirty, isFalse);
await tester.pump(const Duration(seconds: 1));
expect(element.dirty, isFalse);
expect(Tooltip.dismissAllToolTips(), isTrue);
expect(element.dirty, isFalse);
await tester.pump(const Duration(seconds: 1));
expect(element.dirty, isFalse);
});
} }
Future<void> setWidgetForTooltipMode( Future<void> setWidgetForTooltipMode(
......
...@@ -164,6 +164,97 @@ void main() { ...@@ -164,6 +164,97 @@ void main() {
await tester.pumpWidget(SizedBox(child: widget)); await tester.pumpWidget(SizedBox(child: widget));
}); });
testWidgets('Safe to hide overlay child and remove OverlayPortal in the same frame', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/129025.
final Widget widget = Directionality(
key: GlobalKey(debugLabel: 'key'),
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
},
),
],
),
);
controller1.show();
await tester.pumpWidget(widget);
controller1.hide();
await tester.pumpWidget(const SizedBox());
expect(tester.takeException(), isNull);
});
testWidgets('Safe to hide overlay child and reparent OverlayPortal in the same frame', (WidgetTester tester) async {
final OverlayPortal overlayPortal = OverlayPortal(
key: GlobalKey(debugLabel: 'key'),
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
List<Widget> children = <Widget>[ const SizedBox(), overlayPortal ];
late StateSetter setState;
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Column(children: children);
},
),
],
),
);
controller1.show();
await tester.pumpWidget(widget);
controller1.hide();
setState(() {
children = <Widget>[ overlayPortal, const SizedBox() ];
});
await tester.pumpWidget(widget);
expect(tester.takeException(), isNull);
});
testWidgets('Safe to hide overlay child and reparent OverlayPortal in the same frame 2', (WidgetTester tester) async {
final Widget widget = Directionality(
key: GlobalKey(debugLabel: 'key'),
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
},
),
],
),
);
controller1.show();
await tester.pumpWidget(widget);
controller1.hide();
await tester.pumpWidget(SizedBox(child: widget));
expect(tester.takeException(), isNull);
});
testWidgets('Throws when the same controller is attached to multiple OverlayPortal', (WidgetTester tester) async { testWidgets('Throws when the same controller is attached to multiple OverlayPortal', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller'); final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
final Widget widget = Directionality( final Widget widget = Directionality(
...@@ -1407,6 +1498,64 @@ void main() { ...@@ -1407,6 +1498,64 @@ void main() {
expect(counter2.layoutCount, 3); expect(counter2.layoutCount, 3);
}); });
}); });
testWidgets('Safe to move the overlay child to a different Overlay and remove the old Overlay', (WidgetTester tester) async {
controller1.show();
final GlobalKey key = GlobalKey(debugLabel: 'key');
final GlobalKey oldOverlayKey = GlobalKey(debugLabel: 'old overlay');
final GlobalKey newOverlayKey = GlobalKey(debugLabel: 'new overlay');
final GlobalKey overlayChildKey = GlobalKey(debugLabel: 'overlay child key');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: oldOverlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
key: key,
controller: controller1,
overlayChildBuilder: (BuildContext context) => SizedBox(key: overlayChildKey),
child: const SizedBox(),
);
},
),
],
),
),
);
expect(find.byKey(overlayChildKey), findsOneWidget);
expect(find.byKey(newOverlayKey), findsNothing);
expect(find.byKey(oldOverlayKey), findsOneWidget);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: newOverlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
key: key,
controller: controller1,
overlayChildBuilder: (BuildContext context) => SizedBox(key: overlayChildKey),
child: const SizedBox(),
);
},
),
],
),
),
);
expect(tester.takeException(), isNull);
expect(find.byKey(overlayChildKey), findsOneWidget);
expect(find.byKey(newOverlayKey), findsOneWidget);
expect(find.byKey(oldOverlayKey), findsNothing);
});
}); });
group('Paint order', () { group('Paint order', () {
......
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