migrate Tooltip
to use OverlayPortal
(#127728)
https://github.com/flutter/flutter/issues/7151 isn't a problem with OverlayPortal so the test is removed. Also removed some `mounted` checks since they're no longer needed.
This commit is contained in:
parent
9e8143a047
commit
e1fdb1aa74
@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'feedback.dart';
|
||||
import 'text_theme.dart';
|
||||
import 'theme.dart';
|
||||
import 'tooltip_theme.dart';
|
||||
import 'tooltip_visibility.dart';
|
||||
@ -388,23 +389,17 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
static const bool _defaultEnableFeedback = true;
|
||||
static const TextAlign _defaultTextAlign = TextAlign.start;
|
||||
|
||||
late double _height;
|
||||
late EdgeInsetsGeometry _padding;
|
||||
late EdgeInsetsGeometry _margin;
|
||||
late Decoration _decoration;
|
||||
late TextStyle _textStyle;
|
||||
late TextAlign _textAlign;
|
||||
late double _verticalOffset;
|
||||
late bool _preferBelow;
|
||||
late bool _excludeFromSemantics;
|
||||
OverlayEntry? _entry;
|
||||
final OverlayPortalController _overlayController = OverlayPortalController();
|
||||
|
||||
late Duration _showDuration;
|
||||
late Duration _hoverShowDuration;
|
||||
late Duration _waitDuration;
|
||||
late TooltipTriggerMode _triggerMode;
|
||||
late bool _enableFeedback;
|
||||
// From InheritedWidgets
|
||||
late bool _visible;
|
||||
late TooltipThemeData _tooltipTheme;
|
||||
|
||||
Duration get _showDuration => widget.showDuration ?? _tooltipTheme.showDuration ?? _defaultShowDuration;
|
||||
Duration get _hoverShowDuration => widget.showDuration ?? _tooltipTheme.showDuration ?? _defaultHoverShowDuration;
|
||||
Duration get _waitDuration => widget.waitDuration ?? _tooltipTheme.waitDuration ?? _defaultWaitDuration;
|
||||
TooltipTriggerMode get _triggerMode => widget.triggerMode ?? _tooltipTheme.triggerMode ?? _defaultTriggerMode;
|
||||
bool get _enableFeedback => widget.enableFeedback ?? _tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
|
||||
|
||||
/// The plain text message for this tooltip.
|
||||
///
|
||||
@ -438,14 +433,16 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
case AnimationStatus.dismissed:
|
||||
entryNeedsUpdating = _animationStatus != AnimationStatus.dismissed;
|
||||
if (entryNeedsUpdating) {
|
||||
_removeEntry();
|
||||
Tooltip._openedTooltips.remove(this);
|
||||
_overlayController.hide();
|
||||
}
|
||||
case AnimationStatus.completed:
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
entryNeedsUpdating = _animationStatus == AnimationStatus.dismissed;
|
||||
if (entryNeedsUpdating) {
|
||||
_createNewEntry();
|
||||
_overlayController.show();
|
||||
Tooltip._openedTooltips.add(this);
|
||||
SemanticsService.tooltip(_tooltipMessage);
|
||||
}
|
||||
}
|
||||
@ -620,11 +617,6 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
// (even these tooltips are still hovered),
|
||||
// iii. The last hovering device leaves the tooltip.
|
||||
void _handleMouseEnter(PointerEnterEvent event) {
|
||||
// The callback is also used in an OverlayEntry, so there's a chance that
|
||||
// this widget is already unmounted.
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
// _handleMouseEnter is only called when the mouse starts to hover over this
|
||||
// tooltip (including the actual tooltip it shows on the overlay), and this
|
||||
// tooltip is the first to be hit in the widget tree's hit testing order.
|
||||
@ -646,7 +638,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
}
|
||||
|
||||
void _handleMouseExit(PointerExitEvent event) {
|
||||
if (!mounted || _activeHoveringPointerDevices.isEmpty) {
|
||||
if (_activeHoveringPointerDevices.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_activeHoveringPointerDevices.remove(event.device);
|
||||
@ -694,6 +686,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_visible = TooltipVisibility.of(context);
|
||||
_tooltipTheme = TooltipTheme.of(context);
|
||||
}
|
||||
|
||||
// https://material.io/components/tooltips#specs
|
||||
@ -719,8 +712,8 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
};
|
||||
}
|
||||
|
||||
double _getDefaultFontSize() {
|
||||
return switch (Theme.of(context).platform) {
|
||||
static double _getDefaultFontSize(TargetPlatform platform) {
|
||||
return switch (platform) {
|
||||
TargetPlatform.macOS ||
|
||||
TargetPlatform.linux ||
|
||||
TargetPlatform.windows => 12.0,
|
||||
@ -730,58 +723,50 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
};
|
||||
}
|
||||
|
||||
void _createNewEntry() {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
);
|
||||
|
||||
final RenderBox box = context.findRenderObject()! as RenderBox;
|
||||
Widget _buildTooltipOverlay(BuildContext context) {
|
||||
final OverlayState overlayState = Overlay.of(context, debugRequiredFor: widget);
|
||||
final RenderBox box = this.context.findRenderObject()! as RenderBox;
|
||||
final Offset target = box.localToGlobal(
|
||||
box.size.center(Offset.zero),
|
||||
ancestor: overlayState.context.findRenderObject(),
|
||||
);
|
||||
|
||||
// We create this widget outside of the overlay entry's builder to prevent
|
||||
// updated values from happening to leak into the overlay when the overlay
|
||||
// rebuilds.
|
||||
final Widget overlay = Directionality(
|
||||
textDirection: Directionality.of(context),
|
||||
child: _TooltipOverlay(
|
||||
richMessage: widget.richMessage ?? TextSpan(text: widget.message),
|
||||
height: _height,
|
||||
padding: _padding,
|
||||
margin: _margin,
|
||||
onEnter: _handleMouseEnter,
|
||||
onExit: _handleMouseExit,
|
||||
decoration: _decoration,
|
||||
textStyle: _textStyle,
|
||||
textAlign: _textAlign,
|
||||
animation: CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
),
|
||||
target: target,
|
||||
verticalOffset: _verticalOffset,
|
||||
preferBelow: _preferBelow,
|
||||
final (TextStyle defaultTextStyle, BoxDecoration defaultDecoration) = switch (Theme.of(context)) {
|
||||
ThemeData(brightness: Brightness.dark, :final TextTheme textTheme, :final TargetPlatform platform) => (
|
||||
textTheme.bodyMedium!.copyWith(color: Colors.black, fontSize: _getDefaultFontSize(platform)),
|
||||
BoxDecoration(color: Colors.white.withOpacity(0.9), borderRadius: const BorderRadius.all(Radius.circular(4))),
|
||||
),
|
||||
);
|
||||
final OverlayEntry entry = _entry = OverlayEntry(builder: (BuildContext context) => overlay);
|
||||
overlayState.insert(entry);
|
||||
Tooltip._openedTooltips.add(this);
|
||||
}
|
||||
ThemeData(brightness: Brightness.light, :final TextTheme textTheme, :final TargetPlatform platform) => (
|
||||
textTheme.bodyMedium!.copyWith(color: Colors.white, fontSize: _getDefaultFontSize(platform)),
|
||||
BoxDecoration(color: Colors.grey[700]!.withOpacity(0.9), borderRadius: const BorderRadius.all(Radius.circular(4))),
|
||||
),
|
||||
};
|
||||
|
||||
void _removeEntry() {
|
||||
Tooltip._openedTooltips.remove(this);
|
||||
_entry?.remove();
|
||||
_entry?.dispose();
|
||||
_entry = null;
|
||||
final TooltipThemeData tooltipTheme = _tooltipTheme;
|
||||
return _TooltipOverlay(
|
||||
richMessage: widget.richMessage ?? TextSpan(text: widget.message),
|
||||
height: widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight(),
|
||||
padding: widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding(),
|
||||
margin: widget.margin ?? tooltipTheme.margin ?? _defaultMargin,
|
||||
onEnter: _handleMouseEnter,
|
||||
onExit: _handleMouseExit,
|
||||
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,
|
||||
),
|
||||
target: target,
|
||||
verticalOffset: widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset,
|
||||
preferBelow: widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
|
||||
_removeEntry();
|
||||
Tooltip._openedTooltips.remove(this);
|
||||
_longPressRecognizer?.dispose();
|
||||
_tapRecognizer?.dispose();
|
||||
_timer?.cancel();
|
||||
@ -798,47 +783,9 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
return widget.child ?? const SizedBox.shrink();
|
||||
}
|
||||
assert(debugCheckHasOverlay(context));
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
|
||||
final TextStyle defaultTextStyle;
|
||||
final BoxDecoration defaultDecoration;
|
||||
if (theme.brightness == Brightness.dark) {
|
||||
defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.black,
|
||||
fontSize: _getDefaultFontSize(),
|
||||
);
|
||||
defaultDecoration = BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
);
|
||||
} else {
|
||||
defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: _getDefaultFontSize(),
|
||||
);
|
||||
defaultDecoration = BoxDecoration(
|
||||
color: Colors.grey[700]!.withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
);
|
||||
}
|
||||
|
||||
_height = widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight();
|
||||
_padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
|
||||
_margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
|
||||
_verticalOffset = widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset;
|
||||
_preferBelow = widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow;
|
||||
_excludeFromSemantics = widget.excludeFromSemantics ?? tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
|
||||
_decoration = widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration;
|
||||
_textStyle = widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle;
|
||||
_textAlign = widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign;
|
||||
_waitDuration = widget.waitDuration ?? tooltipTheme.waitDuration ?? _defaultWaitDuration;
|
||||
_showDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultShowDuration;
|
||||
_hoverShowDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultHoverShowDuration;
|
||||
_triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
|
||||
_enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
|
||||
|
||||
final bool excludeFromSemantics = widget.excludeFromSemantics ?? _tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
|
||||
Widget result = Semantics(
|
||||
tooltip: _excludeFromSemantics ? null : _tooltipMessage,
|
||||
tooltip: excludeFromSemantics ? null : _tooltipMessage,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
@ -854,8 +801,11 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
return OverlayPortal(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildTooltipOverlay,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1041,15 +1041,9 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// The tooltip overlay still on the tree and it will removed in the next frame.
|
||||
|
||||
// Dispatch the mouse in and out events before the overlay detached.
|
||||
await gesture.moveTo(tester.getCenter(find.text(tooltipText)));
|
||||
await gesture.moveTo(Offset.zero);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Go without crashes.
|
||||
await gesture.removePointer();
|
||||
// The tooltip should be removed, including the overlay child.
|
||||
expect(find.text(tooltipText), findsNothing);
|
||||
expect(find.byTooltip(tooltipText), findsNothing);
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('Calling ensureTooltipVisible on an unmounted TooltipState returns false', (WidgetTester tester) async {
|
||||
@ -1435,35 +1429,6 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('Tooltip overlay does not update', (WidgetTester tester) async {
|
||||
Widget buildApp(String text) {
|
||||
return MaterialApp(
|
||||
home: Center(
|
||||
child: Tooltip(
|
||||
message: text,
|
||||
child: Container(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
color: Colors.green[500],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp(tooltipText));
|
||||
await tester.longPress(find.byType(Tooltip));
|
||||
expect(find.text(tooltipText), findsOneWidget);
|
||||
await tester.pumpWidget(buildApp('NEW'));
|
||||
expect(find.text(tooltipText), findsOneWidget);
|
||||
await tester.tapAt(const Offset(5.0, 5.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text(tooltipText), findsNothing);
|
||||
await tester.longPress(find.byType(Tooltip));
|
||||
expect(find.text(tooltipText), findsNothing);
|
||||
});
|
||||
|
||||
testWidgetsWithLeakTracking('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
|
||||
Widget buildApp(String text, { required double textScaleFactor }) {
|
||||
return MediaQuery(
|
||||
|
Loading…
x
Reference in New Issue
Block a user