mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
This reverts commit eca9364069
because of four
performance regressions. Will fix and re-land.
This commit is contained in:
parent
6ed442a927
commit
7565093f3e
|
@ -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<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,17 +194,13 @@ 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(
|
||||
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<double> 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue