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

This reverts commit eca9364069 because of four
performance regressions. Will fix and re-land.
This commit is contained in:
Greg Spencer 2019-04-26 14:29:19 -07:00 committed by GitHub
parent 6ed442a927
commit 7565093f3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 254 deletions

View file

@ -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,9 +50,6 @@ 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),
@ -62,15 +57,12 @@ class Tooltip extends StatefulWidget {
assert(verticalOffset != null),
assert(preferBelow != null),
assert(excludeFromSemantics != null),
assert(waitDuration != null),
assert(showDuration != 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<Duration>('waitDuration', waitDuration, defaultValue: _kDefaultWaitDuration));
properties.add(DiagnosticsProperty<Duration>('showDuration', showDuration, defaultValue: _kDefaultShowDuration));
}
}
class _TooltipState extends State<Tooltip> 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<Tooltip> 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<Tooltip> 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,10 +194,7 @@ class _TooltipState extends State<Tooltip> 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(
return GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: _handleLongPress,
excludeFromSemantics: true,
@ -273,7 +202,6 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
label: widget.excludeFromSemantics ? null : widget.message,
child: widget.child,
),
),
);
}
}
@ -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<double> animation;
final Offset target;
final double verticalOffset;
@ -368,12 +294,14 @@ class _TooltipOverlay extends StatelessWidget {
),
child: FadeTransition(
opacity: animation,
child: Opacity(
opacity: 0.9,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: height),
child: Container(
decoration: decoration ?? BoxDecoration(
color: darkTheme.backgroundColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(4.0),
decoration: BoxDecoration(
color: darkTheme.backgroundColor,
borderRadius: BorderRadius.circular(2.0),
),
padding: padding,
child: Center(
@ -386,6 +314,7 @@ class _TooltipOverlay extends StatelessWidget {
),
),
),
),
);
}
}

View file

@ -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>[
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 {
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)