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 {
// _handleMouseExit. The set is cleared in _handleTapToDismiss, typically when
// a PointerDown event interacts with some other UI component.
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;
void _handleStatusChanged(AnimationStatus status) {
assert(mounted);
final bool entryNeedsUpdating;
switch (status) {
case AnimationStatus.dismissed:
entryNeedsUpdating = _animationStatus != AnimationStatus.dismissed;
if (entryNeedsUpdating) {
Tooltip._openedTooltips.remove(this);
_overlayController.hide();
}
case AnimationStatus.completed:
case AnimationStatus.forward:
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 */ });
switch ((_isTooltipVisible(_animationStatus), _isTooltipVisible(status))) {
case (true, false):
Tooltip._openedTooltips.remove(this);
_overlayController.hide();
case (false, true):
_overlayController.show();
Tooltip._openedTooltips.add(this);
SemanticsService.tooltip(_tooltipMessage);
case (true, true) || (false, false):
break;
}
_animationStatus = status;
}
......@@ -753,10 +750,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
decoration: widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration,
textStyle: widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle,
textAlign: widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign,
animation: CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
),
animation: CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn),
target: target,
verticalOffset: widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset,
preferBelow: widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow,
......
......@@ -48,7 +48,7 @@ Offset positionDependentBox({
// VERTICAL DIRECTION
final bool fitsBelow = target.dy + verticalOffset + childSize.height <= size.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;
if (tooltipBelow) {
y = math.min(target.dy + verticalOffset, size.height - margin);
......@@ -56,19 +56,11 @@ Offset positionDependentBox({
y = math.max(target.dy - verticalOffset - childSize.height, margin);
}
// HORIZONTAL DIRECTION
final double x;
if (size.width - margin * 2.0 < childSize.width) {
x = (size.width - childSize.width) / 2.0;
} else {
final double normalizedTargetX = clampDouble(target.dx, margin, size.width - margin);
final double edge = margin + childSize.width / 2.0;
if (normalizedTargetX < edge) {
x = margin;
} else if (normalizedTargetX > size.width - edge) {
x = size.width - margin - childSize.width;
} else {
x = normalizedTargetX - childSize.width / 2.0;
}
}
final double flexibleSpace = size.width - childSize.width;
final double x = flexibleSpace <= 2 * margin
// If there's not enough horizontal space for margin + child, center the
// child.
? flexibleSpace / 2.0
: clampDouble(target.dx - childSize.width / 2, margin, flexibleSpace - margin);
return Offset(x, y);
}
......@@ -2245,6 +2245,39 @@ void main() {
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(
......
......@@ -164,6 +164,97 @@ void main() {
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 {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
final Widget widget = Directionality(
......@@ -1407,6 +1498,64 @@ void main() {
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', () {
......
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