diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 1694c53b4d7..46a20b1b6e7 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -12,10 +12,8 @@ import 'feedback.dart'; import 'theme.dart'; import 'theme_data.dart'; -const Duration _kFadeInDuration = Duration(milliseconds: 150); -const Duration _kFadeOutDuration = Duration(milliseconds: 75); -const Duration _kDefaultShowDuration = Duration(milliseconds: 1500); -const Duration _kDefaultWaitDuration = Duration(milliseconds: 0); +const Duration _kFadeDuration = Duration(milliseconds: 200); +const Duration _kShowDuration = Duration(milliseconds: 1500); /// A material design tooltip. /// @@ -43,7 +41,7 @@ class Tooltip extends StatefulWidget { /// By default, tooltips prefer to appear below the [child] widget when the /// user long presses on the widget. /// - /// All of the arguments except [child] and [decoration] must not be null. + /// The [message] argument must not be null. const Tooltip({ Key key, @required this.message, @@ -52,25 +50,19 @@ class Tooltip extends StatefulWidget { this.verticalOffset = 24.0, this.preferBelow = true, this.excludeFromSemantics = false, - this.decoration, - this.waitDuration = _kDefaultWaitDuration, - this.showDuration = _kDefaultShowDuration, this.child, - }) : assert(message != null), - assert(height != null), - assert(padding != null), - assert(verticalOffset != null), - assert(preferBelow != null), - assert(excludeFromSemantics != null), - assert(waitDuration != null), - assert(showDuration != null), - super(key: key); + }) : assert(message != null), + assert(height != null), + assert(padding != null), + assert(verticalOffset != null), + assert(preferBelow != null), + assert(excludeFromSemantics != null), + super(key: key); /// The text to display in the tooltip. final String message; - /// The amount of vertical space the tooltip should occupy (inside its - /// padding). + /// The amount of vertical space the tooltip should occupy (inside its padding). final double height; /// The amount of space by which to inset the child. @@ -78,8 +70,7 @@ class Tooltip extends StatefulWidget { /// Defaults to 16.0 logical pixels in each direction. final EdgeInsetsGeometry padding; - /// The amount of vertical distance between the widget and the displayed - /// tooltip. + /// The amount of vertical distance between the widget and the displayed tooltip. final double verticalOffset; /// Whether the tooltip defaults to being displayed below the widget. @@ -98,23 +89,6 @@ class Tooltip extends StatefulWidget { /// {@macro flutter.widgets.child} final Widget child; - /// Specifies the decoration of the tooltip window. - /// - /// If not specified, defaults to a rounded rectangle with a border radius of - /// 4.0, and a color derived from the text theme. - final Decoration decoration; - - /// The amount of time that a pointer must hover over the widget before it - /// will show a tooltip. - /// - /// Defaults to 0 milliseconds (tooltips show immediately upon hover). - final Duration waitDuration; - - /// The amount of time that the tooltip will be shown once it has appeared. - /// - /// Defaults to 1.5 seconds. - final Duration showDuration; - @override _TooltipState createState() => _TooltipState(); @@ -124,72 +98,38 @@ class Tooltip extends StatefulWidget { properties.add(StringProperty('message', message, showName: false)); properties.add(DoubleProperty('vertical offset', verticalOffset)); properties.add(FlagProperty('position', value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true)); - properties.add(DiagnosticsProperty('waitDuration', waitDuration, defaultValue: _kDefaultWaitDuration)); - properties.add(DiagnosticsProperty('showDuration', showDuration, defaultValue: _kDefaultShowDuration)); } } class _TooltipState extends State with SingleTickerProviderStateMixin { AnimationController _controller; OverlayEntry _entry; - Timer _hideTimer; - Timer _showTimer; + Timer _timer; @override void initState() { super.initState(); - _controller = AnimationController(duration: _kFadeInDuration, vsync: this) + _controller = AnimationController(duration: _kFadeDuration, vsync: this) ..addStatusListener(_handleStatusChanged); } void _handleStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.dismissed) { - _hideTooltip(immediately: true); - } - } - - void _hideTooltip({bool immediately = false}) { - _showTimer?.cancel(); - _showTimer = null; - if (immediately) { + if (status == AnimationStatus.dismissed) _removeEntry(); - return; - } - _hideTimer ??= Timer(widget.showDuration, _controller.reverse); - } - - void _showTooltip({bool immediately = false}) { - _hideTimer?.cancel(); - _hideTimer = null; - if (immediately) { - ensureTooltipVisible(); - return; - } - _showTimer ??= Timer(widget.waitDuration, ensureTooltipVisible); } /// Shows the tooltip if it is not already visible. /// /// Returns `false` when the tooltip was already visible. bool ensureTooltipVisible() { - _showTimer?.cancel(); - _showTimer = null; if (_entry != null) { - // Stop trying to hide, if we were. - _hideTimer?.cancel(); - _hideTimer = null; + _timer?.cancel(); + _timer = null; _controller.forward(); return false; // Already visible. } - _createNewEntry(); - _controller.forward(); - return true; - } - - void _createNewEntry() { final RenderBox box = context.findRenderObject(); final Offset target = box.localToGlobal(box.size.center(Offset.zero)); - // 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. @@ -197,18 +137,9 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { message: widget.message, height: widget.height, padding: widget.padding, - decoration: widget.decoration, animation: CurvedAnimation( parent: _controller, curve: Curves.fastOutSlowIn, - // Add an interval here to make the fade out use a different (shorter) - // duration than the fade in. If _kFadeOutDuration is made longer than - // _kFadeInDuration, then the equation below will need to change. - reverseCurve: Interval( - 0.0, - _kFadeOutDuration.inMilliseconds / _kFadeInDuration.inMilliseconds, - curve: Curves.fastOutSlowIn, - ), ), target: target, verticalOffset: widget.verticalOffset, @@ -218,30 +149,31 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { Overlay.of(context, debugRequiredFor: widget).insert(_entry); GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent); SemanticsService.tooltip(widget.message); + _controller.forward(); + return true; } void _removeEntry() { - _hideTimer?.cancel(); - _hideTimer = null; - _entry?.remove(); + assert(_entry != null); + _timer?.cancel(); + _timer = null; + _entry.remove(); _entry = null; GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent); } void _handlePointerEvent(PointerEvent event) { assert(_entry != null); - if (event is PointerUpEvent || event is PointerCancelEvent) { - _hideTooltip(); - } else if (event is PointerDownEvent) { - _hideTooltip(immediately: true); - } + if (event is PointerUpEvent || event is PointerCancelEvent) + _timer ??= Timer(_kShowDuration, _controller.reverse); + else if (event is PointerDownEvent) + _controller.reverse(); } @override void deactivate() { - if (_entry != null) { - _hideTooltip(immediately: true); - } + if (_entry != null) + _controller.reverse(); super.deactivate(); } @@ -262,17 +194,13 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { assert(Overlay.of(context, debugRequiredFor: widget) != null); - return Listener( - onPointerEnter: (PointerEnterEvent event) => _showTooltip(), - onPointerExit: (PointerExitEvent event) => _hideTooltip(), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onLongPress: _handleLongPress, - excludeFromSemantics: true, - child: Semantics( - label: widget.excludeFromSemantics ? null : widget.message, - child: widget.child, - ), + return GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: _handleLongPress, + excludeFromSemantics: true, + child: Semantics( + label: widget.excludeFromSemantics ? null : widget.message, + child: widget.child, ), ); } @@ -288,9 +216,9 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { @required this.target, @required this.verticalOffset, @required this.preferBelow, - }) : assert(target != null), - assert(verticalOffset != null), - assert(preferBelow != null); + }) : assert(target != null), + assert(verticalOffset != null), + assert(preferBelow != null); /// The offset of the target the tooltip is positioned near in the global /// coordinate system. @@ -334,7 +262,6 @@ class _TooltipOverlay extends StatelessWidget { this.message, this.height, this.padding, - this.decoration, this.animation, this.target, this.verticalOffset, @@ -344,7 +271,6 @@ class _TooltipOverlay extends StatelessWidget { final String message; final double height; final EdgeInsetsGeometry padding; - final Decoration decoration; final Animation animation; final Offset target; final double verticalOffset; @@ -368,18 +294,21 @@ class _TooltipOverlay extends StatelessWidget { ), child: FadeTransition( opacity: animation, - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: height), - child: Container( - decoration: decoration ?? BoxDecoration( - color: darkTheme.backgroundColor.withOpacity(0.9), - borderRadius: BorderRadius.circular(4.0), - ), - padding: padding, - child: Center( - widthFactor: 1.0, - heightFactor: 1.0, - child: Text(message, style: darkTheme.textTheme.body1), + child: Opacity( + opacity: 0.9, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: height), + child: Container( + decoration: BoxDecoration( + color: darkTheme.backgroundColor, + borderRadius: BorderRadius.circular(2.0), + ), + padding: padding, + child: Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: Text(message, style: darkTheme.textTheme.body1), + ), ), ), ), diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index f81921f3078..5fe557196ed 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; @@ -68,7 +67,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen @@ -124,7 +123,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen @@ -176,7 +175,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen @@ -230,7 +229,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) // we try to put it here but it doesn't fit: @@ -295,7 +294,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen @@ -348,7 +347,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen @@ -403,7 +402,7 @@ void main() { ), ), ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. + (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen @@ -423,82 +422,6 @@ void main() { expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0)); }); - testWidgets('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Overlay( - initialEntries: [ - OverlayEntry( - builder: (BuildContext context) { - return Tooltip( - key: key, - message: tooltipText, - child: Container( - width: 0.0, - height: 0.0, - ), - ); - }, - ), - ], - ), - ), - ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. - await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) - - final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent.parent.parent.parent; - - expect(tip.size.height, equals(32.0)); - expect(tip.size.width, equals(74.0)); - expect(tip, paints..rrect( - rrect: RRect.fromRectAndRadius(tip.paintBounds, const Radius.circular(4.0)), - color: const Color(0xe6616161), - )); - }); - - testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - const Decoration customDecoration = ShapeDecoration( - shape: StadiumBorder(), - color: Color(0x80800000), - ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Overlay( - initialEntries: [ - OverlayEntry( - builder: (BuildContext context) { - return Tooltip( - key: key, - decoration: customDecoration, - message: tooltipText, - child: Container( - width: 0.0, - height: 0.0, - ), - ); - }, - ), - ], - ), - ), - ); - (key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file. - await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) - - final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent.parent.parent.parent; - - expect(tip.size.height, equals(32.0)); - expect(tip.size.width, equals(74.0)); - expect(tip, paints..path( - color: const Color(0x80800000), - )); - }); - testWidgets('Tooltip stays around', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -534,50 +457,6 @@ void main() { gesture.up(); }); - testWidgets('Tooltip shows/hides when hovered', (WidgetTester tester) async { - const Duration waitDuration = Duration(milliseconds: 0); - const Duration showDuration = Duration(milliseconds: 1500); - await tester.pumpWidget( - MaterialApp( - home: Center( - child: Tooltip( - message: tooltipText, - showDuration: showDuration, - waitDuration: waitDuration, - child: Container( - width: 100.0, - height: 100.0, - ), - ), - ), - ) - ); - - final Finder tooltip = find.byType(Tooltip); - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.moveTo(Offset.zero); - await tester.pump(); - await gesture.moveTo(tester.getCenter(tooltip)); - await tester.pump(); - // Wait for it to appear. - await tester.pump(waitDuration); - expect(find.text(tooltipText), findsOneWidget); - - // Wait a looong time to make sure that it doesn't go away if the mouse is - // still over the widget. - await tester.pump(const Duration(days: 1)); - await tester.pumpAndSettle(); - expect(find.text(tooltipText), findsOneWidget); - - await gesture.moveTo(Offset.zero); - await tester.pump(); - - // Wait for it to disappear. - await tester.pump(showDuration); - await tester.pumpAndSettle(); - expect(find.text(tooltipText), findsNothing); - }); - testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -621,7 +500,7 @@ void main() { expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true)); - // Before using "as dynamic" in your code, see note at the top of the file. + // before using "as dynamic" in your code, see note top of file (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)