Unverified Commit 7565093f authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Revert "Add support for Tooltip hover (#31561)" (#31692)

This reverts commit eca93640 because of four
performance regressions. Will fix and re-land.
parent 6ed442a9
...@@ -12,10 +12,8 @@ import 'feedback.dart'; ...@@ -12,10 +12,8 @@ import 'feedback.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
const Duration _kFadeInDuration = Duration(milliseconds: 150); const Duration _kFadeDuration = Duration(milliseconds: 200);
const Duration _kFadeOutDuration = Duration(milliseconds: 75); const Duration _kShowDuration = Duration(milliseconds: 1500);
const Duration _kDefaultShowDuration = Duration(milliseconds: 1500);
const Duration _kDefaultWaitDuration = Duration(milliseconds: 0);
/// A material design tooltip. /// A material design tooltip.
/// ///
...@@ -43,7 +41,7 @@ class Tooltip extends StatefulWidget { ...@@ -43,7 +41,7 @@ class Tooltip extends StatefulWidget {
/// By default, tooltips prefer to appear below the [child] widget when the /// By default, tooltips prefer to appear below the [child] widget when the
/// user long presses on the widget. /// 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({ const Tooltip({
Key key, Key key,
@required this.message, @required this.message,
...@@ -52,25 +50,19 @@ class Tooltip extends StatefulWidget { ...@@ -52,25 +50,19 @@ class Tooltip extends StatefulWidget {
this.verticalOffset = 24.0, this.verticalOffset = 24.0,
this.preferBelow = true, this.preferBelow = true,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.decoration,
this.waitDuration = _kDefaultWaitDuration,
this.showDuration = _kDefaultShowDuration,
this.child, this.child,
}) : assert(message != null), }) : assert(message != null),
assert(height != null), assert(height != null),
assert(padding != null), assert(padding != null),
assert(verticalOffset != null), assert(verticalOffset != null),
assert(preferBelow != null), assert(preferBelow != null),
assert(excludeFromSemantics != null), assert(excludeFromSemantics != null),
assert(waitDuration != null), super(key: key);
assert(showDuration != null),
super(key: key);
/// The text to display in the tooltip. /// The text to display in the tooltip.
final String message; final String message;
/// The amount of vertical space the tooltip should occupy (inside its /// The amount of vertical space the tooltip should occupy (inside its padding).
/// padding).
final double height; final double height;
/// The amount of space by which to inset the child. /// The amount of space by which to inset the child.
...@@ -78,8 +70,7 @@ class Tooltip extends StatefulWidget { ...@@ -78,8 +70,7 @@ class Tooltip extends StatefulWidget {
/// Defaults to 16.0 logical pixels in each direction. /// Defaults to 16.0 logical pixels in each direction.
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// The amount of vertical distance between the widget and the displayed /// The amount of vertical distance between the widget and the displayed tooltip.
/// tooltip.
final double verticalOffset; final double verticalOffset;
/// Whether the tooltip defaults to being displayed below the widget. /// Whether the tooltip defaults to being displayed below the widget.
...@@ -98,23 +89,6 @@ class Tooltip extends StatefulWidget { ...@@ -98,23 +89,6 @@ class Tooltip extends StatefulWidget {
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
final Widget 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 @override
_TooltipState createState() => _TooltipState(); _TooltipState createState() => _TooltipState();
...@@ -124,72 +98,38 @@ class Tooltip extends StatefulWidget { ...@@ -124,72 +98,38 @@ class Tooltip extends StatefulWidget {
properties.add(StringProperty('message', message, showName: false)); properties.add(StringProperty('message', message, showName: false));
properties.add(DoubleProperty('vertical offset', verticalOffset)); properties.add(DoubleProperty('vertical offset', verticalOffset));
properties.add(FlagProperty('position', value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true)); properties.add(FlagProperty('position', value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true));
properties.add(DiagnosticsProperty<Duration>('waitDuration', waitDuration, defaultValue: _kDefaultWaitDuration));
properties.add(DiagnosticsProperty<Duration>('showDuration', showDuration, defaultValue: _kDefaultShowDuration));
} }
} }
class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
AnimationController _controller; AnimationController _controller;
OverlayEntry _entry; OverlayEntry _entry;
Timer _hideTimer; Timer _timer;
Timer _showTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController(duration: _kFadeInDuration, vsync: this) _controller = AnimationController(duration: _kFadeDuration, vsync: this)
..addStatusListener(_handleStatusChanged); ..addStatusListener(_handleStatusChanged);
} }
void _handleStatusChanged(AnimationStatus status) { void _handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed) { if (status == AnimationStatus.dismissed)
_hideTooltip(immediately: true);
}
}
void _hideTooltip({bool immediately = false}) {
_showTimer?.cancel();
_showTimer = null;
if (immediately) {
_removeEntry(); _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. /// Shows the tooltip if it is not already visible.
/// ///
/// Returns `false` when the tooltip was already visible. /// Returns `false` when the tooltip was already visible.
bool ensureTooltipVisible() { bool ensureTooltipVisible() {
_showTimer?.cancel();
_showTimer = null;
if (_entry != null) { if (_entry != null) {
// Stop trying to hide, if we were. _timer?.cancel();
_hideTimer?.cancel(); _timer = null;
_hideTimer = null;
_controller.forward(); _controller.forward();
return false; // Already visible. return false; // Already visible.
} }
_createNewEntry();
_controller.forward();
return true;
}
void _createNewEntry() {
final RenderBox box = context.findRenderObject(); final RenderBox box = context.findRenderObject();
final Offset target = box.localToGlobal(box.size.center(Offset.zero)); final Offset target = box.localToGlobal(box.size.center(Offset.zero));
// We create this widget outside of the overlay entry's builder to prevent // 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 // updated values from happening to leak into the overlay when the overlay
// rebuilds. // rebuilds.
...@@ -197,18 +137,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -197,18 +137,9 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
message: widget.message, message: widget.message,
height: widget.height, height: widget.height,
padding: widget.padding, padding: widget.padding,
decoration: widget.decoration,
animation: CurvedAnimation( animation: CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.fastOutSlowIn, 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, target: target,
verticalOffset: widget.verticalOffset, verticalOffset: widget.verticalOffset,
...@@ -218,30 +149,31 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -218,30 +149,31 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
Overlay.of(context, debugRequiredFor: widget).insert(_entry); Overlay.of(context, debugRequiredFor: widget).insert(_entry);
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent); GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
SemanticsService.tooltip(widget.message); SemanticsService.tooltip(widget.message);
_controller.forward();
return true;
} }
void _removeEntry() { void _removeEntry() {
_hideTimer?.cancel(); assert(_entry != null);
_hideTimer = null; _timer?.cancel();
_entry?.remove(); _timer = null;
_entry.remove();
_entry = null; _entry = null;
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent); GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent);
} }
void _handlePointerEvent(PointerEvent event) { void _handlePointerEvent(PointerEvent event) {
assert(_entry != null); assert(_entry != null);
if (event is PointerUpEvent || event is PointerCancelEvent) { if (event is PointerUpEvent || event is PointerCancelEvent)
_hideTooltip(); _timer ??= Timer(_kShowDuration, _controller.reverse);
} else if (event is PointerDownEvent) { else if (event is PointerDownEvent)
_hideTooltip(immediately: true); _controller.reverse();
}
} }
@override @override
void deactivate() { void deactivate() {
if (_entry != null) { if (_entry != null)
_hideTooltip(immediately: true); _controller.reverse();
}
super.deactivate(); super.deactivate();
} }
...@@ -262,17 +194,13 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -262,17 +194,13 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(Overlay.of(context, debugRequiredFor: widget) != null); assert(Overlay.of(context, debugRequiredFor: widget) != null);
return Listener( return GestureDetector(
onPointerEnter: (PointerEnterEvent event) => _showTooltip(), behavior: HitTestBehavior.opaque,
onPointerExit: (PointerExitEvent event) => _hideTooltip(), onLongPress: _handleLongPress,
child: GestureDetector( excludeFromSemantics: true,
behavior: HitTestBehavior.opaque, child: Semantics(
onLongPress: _handleLongPress, label: widget.excludeFromSemantics ? null : widget.message,
excludeFromSemantics: true, child: widget.child,
child: Semantics(
label: widget.excludeFromSemantics ? null : widget.message,
child: widget.child,
),
), ),
); );
} }
...@@ -288,9 +216,9 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { ...@@ -288,9 +216,9 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
@required this.target, @required this.target,
@required this.verticalOffset, @required this.verticalOffset,
@required this.preferBelow, @required this.preferBelow,
}) : assert(target != null), }) : assert(target != null),
assert(verticalOffset != null), assert(verticalOffset != null),
assert(preferBelow != null); assert(preferBelow != null);
/// The offset of the target the tooltip is positioned near in the global /// The offset of the target the tooltip is positioned near in the global
/// coordinate system. /// coordinate system.
...@@ -334,7 +262,6 @@ class _TooltipOverlay extends StatelessWidget { ...@@ -334,7 +262,6 @@ class _TooltipOverlay extends StatelessWidget {
this.message, this.message,
this.height, this.height,
this.padding, this.padding,
this.decoration,
this.animation, this.animation,
this.target, this.target,
this.verticalOffset, this.verticalOffset,
...@@ -344,7 +271,6 @@ class _TooltipOverlay extends StatelessWidget { ...@@ -344,7 +271,6 @@ class _TooltipOverlay extends StatelessWidget {
final String message; final String message;
final double height; final double height;
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
final Decoration decoration;
final Animation<double> animation; final Animation<double> animation;
final Offset target; final Offset target;
final double verticalOffset; final double verticalOffset;
...@@ -368,18 +294,21 @@ class _TooltipOverlay extends StatelessWidget { ...@@ -368,18 +294,21 @@ class _TooltipOverlay extends StatelessWidget {
), ),
child: FadeTransition( child: FadeTransition(
opacity: animation, opacity: animation,
child: ConstrainedBox( child: Opacity(
constraints: BoxConstraints(minHeight: height), opacity: 0.9,
child: Container( child: ConstrainedBox(
decoration: decoration ?? BoxDecoration( constraints: BoxConstraints(minHeight: height),
color: darkTheme.backgroundColor.withOpacity(0.9), child: Container(
borderRadius: BorderRadius.circular(4.0), decoration: BoxDecoration(
), color: darkTheme.backgroundColor,
padding: padding, borderRadius: BorderRadius.circular(2.0),
child: Center( ),
widthFactor: 1.0, padding: padding,
heightFactor: 1.0, child: Center(
child: Text(message, style: darkTheme.textTheme.body1), widthFactor: 1.0,
heightFactor: 1.0,
child: Text(message, style: darkTheme.textTheme.body1),
),
), ),
), ),
), ),
......
...@@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; ...@@ -11,7 +11,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
...@@ -68,7 +67,7 @@ void main() { ...@@ -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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/********************* 800x600 screen /********************* 800x600 screen
...@@ -124,7 +123,7 @@ void main() { ...@@ -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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/********************* 800x600 screen /********************* 800x600 screen
...@@ -176,7 +175,7 @@ void main() { ...@@ -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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/********************* 800x600 screen /********************* 800x600 screen
...@@ -230,7 +229,7 @@ void main() { ...@@ -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) 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: // we try to put it here but it doesn't fit:
...@@ -295,7 +294,7 @@ void main() { ...@@ -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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/********************* 800x600 screen /********************* 800x600 screen
...@@ -348,7 +347,7 @@ void main() { ...@@ -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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/********************* 800x600 screen /********************* 800x600 screen
...@@ -403,7 +402,7 @@ void main() { ...@@ -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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/********************* 800x600 screen /********************* 800x600 screen
...@@ -423,82 +422,6 @@ void main() { ...@@ -423,82 +422,6 @@ void main() {
expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(324.0)); 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>[
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>[
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 { testWidgets('Tooltip stays around', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -534,50 +457,6 @@ void main() { ...@@ -534,50 +457,6 @@ void main() {
gesture.up(); 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 { testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
...@@ -621,7 +500,7 @@ void main() { ...@@ -621,7 +500,7 @@ void main() {
expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true)); 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 (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) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
......
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