Commit 0dafe1a4 authored by Adam Barth's avatar Adam Barth

Add dartdoc to Tooltip (#3957)

Also, remove several unused configuration options and fix an animation
leak.
parent 0e8e8bbb
...@@ -8,59 +8,58 @@ import 'dart:math' as math; ...@@ -8,59 +8,58 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'theme.dart'; import 'typography.dart';
const double _kDefaultTooltipBorderRadius = 2.0; const double _kScreenEdgeMargin = 10.0;
const double _kDefaultTooltipHeight = 32.0; const Duration _kFadeDuration = const Duration(milliseconds: 200);
const EdgeInsets _kDefaultTooltipPadding = const EdgeInsets.symmetric(horizontal: 16.0); const Duration _kShowDuration = const Duration(seconds: 2);
const double _kDefaultVerticalTooltipOffset = 24.0;
const EdgeInsets _kDefaultTooltipScreenEdgeMargin = const EdgeInsets.all(10.0); /// A material design tooltip.
const Duration _kDefaultTooltipFadeDuration = const Duration(milliseconds: 200); ///
const Duration _kDefaultTooltipShowDuration = const Duration(seconds: 2); /// Tooltips provide text labels that help explain the function of a button or
/// other user interface action. Wrap the button in a [Tooltip] widget to
/// show a label when the widget long pressed (or when the user takes some
/// other appropriate action).
///
/// Many widgets, such as [IconButton], [FloatingActionButton], and
/// [PopupMenuButton] have a `tooltip` property that, when non-null, causes the
/// widget to include a [Tooltip] in its build.
///
/// Tooltips improve the accessibility of visual widgets by proving a textual
/// representation of the widget, which, for example, can be vocalized by a
/// screen reader.
///
/// See also:
///
/// * <https://www.google.com/design/spec/components/tooltips.html>
class Tooltip extends StatefulWidget { class Tooltip extends StatefulWidget {
/// Creates a tooltip.
///
/// By default, tooltips prefer to appear below the [child] widget when the
/// user long presses on the widget.
///
/// The [message] argument cannot be null.
Tooltip({ Tooltip({
Key key, Key key,
this.message, this.message,
this.backgroundColor, this.height: 32.0,
this.textColor, this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
this.style, this.verticalOffset: 24.0,
this.opacity: 0.9,
this.borderRadius: _kDefaultTooltipBorderRadius,
this.height: _kDefaultTooltipHeight,
this.padding: _kDefaultTooltipPadding,
this.verticalOffset: _kDefaultVerticalTooltipOffset,
this.screenEdgeMargin: _kDefaultTooltipScreenEdgeMargin,
this.preferBelow: true, this.preferBelow: true,
this.fadeDuration: _kDefaultTooltipFadeDuration,
this.showDuration: _kDefaultTooltipShowDuration,
this.child this.child
}) : super(key: key) { }) : super(key: key) {
assert(message != null); assert(message != null);
assert(opacity != null);
assert(borderRadius != null);
assert(height != null); assert(height != null);
assert(padding != null); assert(padding != null);
assert(verticalOffset != null); assert(verticalOffset != null);
assert(screenEdgeMargin != null);
assert(preferBelow != null); assert(preferBelow != null);
assert(fadeDuration != null);
assert(showDuration != null);
assert(child != null); assert(child != null);
} }
/// The text to display in the tooltip.
final String message; final String message;
final Color backgroundColor; /// The amount of vertical space the tooltip should occupy (inside its padding).
final Color textColor;
final TextStyle style;
final double opacity;
final double borderRadius;
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.
...@@ -68,16 +67,16 @@ class Tooltip extends StatefulWidget { ...@@ -68,16 +67,16 @@ class Tooltip extends StatefulWidget {
/// Defaults to 16.0 logical pixels in each direction. /// Defaults to 16.0 logical pixels in each direction.
final EdgeInsets padding; final EdgeInsets padding;
/// The amount of vertical distance between the widget and the displayed tooltip.
final double verticalOffset; final double verticalOffset;
final EdgeInsets screenEdgeMargin; /// Whether the tooltip defaults to being displayed below the widget.
///
/// Defaults to true. If there is insufficient space to display the tooltip in
/// the preferred direction, the tooltip will be displayed in the opposite
/// direction.
final bool preferBelow; final bool preferBelow;
final Duration fadeDuration;
final Duration showDuration;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
final Widget child; final Widget child;
...@@ -94,7 +93,6 @@ class Tooltip extends StatefulWidget { ...@@ -94,7 +93,6 @@ class Tooltip extends StatefulWidget {
} }
class _TooltipState extends State<Tooltip> { class _TooltipState extends State<Tooltip> {
AnimationController _controller; AnimationController _controller;
OverlayEntry _entry; OverlayEntry _entry;
Timer _timer; Timer _timer;
...@@ -102,8 +100,11 @@ class _TooltipState extends State<Tooltip> { ...@@ -102,8 +100,11 @@ class _TooltipState extends State<Tooltip> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = new AnimationController(duration: config.fadeDuration) _controller = new AnimationController(duration: _kFadeDuration)
..addStatusListener((AnimationStatus status) { ..addStatusListener(_handleStatusChanged);
}
void _handleStatusChanged(AnimationStatus status) {
switch (status) { switch (status) {
case AnimationStatus.completed: case AnimationStatus.completed:
assert(_entry != null); assert(_entry != null);
...@@ -119,25 +120,16 @@ class _TooltipState extends State<Tooltip> { ...@@ -119,25 +120,16 @@ class _TooltipState extends State<Tooltip> {
default: default:
break; break;
} }
});
} }
@override @override
void didUpdateConfig(Tooltip oldConfig) { void didUpdateConfig(Tooltip oldConfig) {
super.didUpdateConfig(oldConfig); super.didUpdateConfig(oldConfig);
if (config.fadeDuration != oldConfig.fadeDuration)
_controller.duration = config.fadeDuration;
if (_entry != null && if (_entry != null &&
(config.message != oldConfig.message || (config.message != oldConfig.message ||
config.backgroundColor != oldConfig.backgroundColor ||
config.style != oldConfig.style ||
config.textColor != oldConfig.textColor ||
config.borderRadius != oldConfig.borderRadius ||
config.height != oldConfig.height || config.height != oldConfig.height ||
config.padding != oldConfig.padding || config.padding != oldConfig.padding ||
config.opacity != oldConfig.opacity ||
config.verticalOffset != oldConfig.verticalOffset || config.verticalOffset != oldConfig.verticalOffset ||
config.screenEdgeMargin != oldConfig.screenEdgeMargin ||
config.preferBelow != oldConfig.preferBelow)) config.preferBelow != oldConfig.preferBelow))
_entry.markNeedsBuild(); _entry.markNeedsBuild();
} }
...@@ -145,7 +137,7 @@ class _TooltipState extends State<Tooltip> { ...@@ -145,7 +137,7 @@ class _TooltipState extends State<Tooltip> {
void resetShowTimer() { void resetShowTimer() {
assert(_controller.status == AnimationStatus.completed); assert(_controller.status == AnimationStatus.completed);
assert(_entry != null); assert(_entry != null);
_timer = new Timer(config.showDuration, hideTooltip); _timer = new Timer(_kShowDuration, hideTooltip);
} }
void showTooltip() { void showTooltip() {
...@@ -153,22 +145,16 @@ class _TooltipState extends State<Tooltip> { ...@@ -153,22 +145,16 @@ class _TooltipState extends State<Tooltip> {
RenderBox box = context.findRenderObject(); RenderBox box = context.findRenderObject();
Point target = box.localToGlobal(box.size.center(Point.origin)); Point target = box.localToGlobal(box.size.center(Point.origin));
_entry = new OverlayEntry(builder: (BuildContext context) { _entry = new OverlayEntry(builder: (BuildContext context) {
TextStyle textStyle = (config.style ?? Theme.of(context).textTheme.body1).copyWith(color: config.textColor ?? Colors.white);
return new _TooltipOverlay( return new _TooltipOverlay(
message: config.message, message: config.message,
backgroundColor: config.backgroundColor ?? Colors.grey[700],
style: textStyle,
borderRadius: config.borderRadius,
height: config.height, height: config.height,
padding: config.padding, padding: config.padding,
opacity: config.opacity,
animation: new CurvedAnimation( animation: new CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.ease curve: Curves.ease
), ),
target: target, target: target,
verticalOffset: config.verticalOffset, verticalOffset: config.verticalOffset,
screenEdgeMargin: config.screenEdgeMargin,
preferBelow: config.preferBelow preferBelow: config.preferBelow
); );
}); });
...@@ -197,6 +183,15 @@ class _TooltipState extends State<Tooltip> { ...@@ -197,6 +183,15 @@ class _TooltipState extends State<Tooltip> {
super.deactivate(); super.deactivate();
} }
@override
void dispose() {
_controller.stop();
_entry?.remove();
_entry = null;
assert(_timer == null);
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(Overlay.of(context, debugRequiredFor: config) != null); assert(Overlay.of(context, debugRequiredFor: config) != null);
...@@ -216,12 +211,11 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { ...@@ -216,12 +211,11 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
_TooltipPositionDelegate({ _TooltipPositionDelegate({
this.target, this.target,
this.verticalOffset, this.verticalOffset,
this.screenEdgeMargin,
this.preferBelow this.preferBelow
}); });
final Point target; final Point target;
final double verticalOffset; final double verticalOffset;
final EdgeInsets screenEdgeMargin;
final bool preferBelow; final bool preferBelow;
@override @override
...@@ -230,21 +224,21 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { ...@@ -230,21 +224,21 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
@override @override
Offset getPositionForChild(Size size, Size childSize) { Offset getPositionForChild(Size size, Size childSize) {
// VERTICAL DIRECTION // VERTICAL DIRECTION
final bool fitsBelow = target.y + verticalOffset + childSize.height <= size.height - screenEdgeMargin.bottom; final bool fitsBelow = target.y + verticalOffset + childSize.height <= size.height - _kScreenEdgeMargin;
final bool fitsAbove = target.y - verticalOffset - childSize.height >= screenEdgeMargin.top; final bool fitsAbove = target.y - verticalOffset - childSize.height >= _kScreenEdgeMargin;
final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow); final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow);
double y; double y;
if (tooltipBelow) if (tooltipBelow)
y = math.min(target.y + verticalOffset, size.height - screenEdgeMargin.bottom); y = math.min(target.y + verticalOffset, size.height - _kScreenEdgeMargin);
else else
y = math.max(target.y - verticalOffset - childSize.height, screenEdgeMargin.top); y = math.max(target.y - verticalOffset - childSize.height, _kScreenEdgeMargin);
// HORIZONTAL DIRECTION // HORIZONTAL DIRECTION
double normalizedTargetX = target.x.clamp(screenEdgeMargin.left, size.width - screenEdgeMargin.right); double normalizedTargetX = target.x.clamp(_kScreenEdgeMargin, size.width - _kScreenEdgeMargin);
double x; double x;
if (normalizedTargetX < screenEdgeMargin.left + childSize.width / 2.0) { if (normalizedTargetX < _kScreenEdgeMargin + childSize.width / 2.0) {
x = screenEdgeMargin.left; x = _kScreenEdgeMargin;
} else if (normalizedTargetX > size.width - screenEdgeMargin.right - childSize.width / 2.0) { } else if (normalizedTargetX > size.width - _kScreenEdgeMargin - childSize.width / 2.0) {
x = size.width - screenEdgeMargin.right - childSize.width; x = size.width - _kScreenEdgeMargin - childSize.width;
} else { } else {
x = normalizedTargetX - childSize.width / 2.0; x = normalizedTargetX - childSize.width / 2.0;
} }
...@@ -255,7 +249,6 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { ...@@ -255,7 +249,6 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
bool shouldRelayout(_TooltipPositionDelegate oldDelegate) { bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
return target != oldDelegate.target return target != oldDelegate.target
|| verticalOffset != oldDelegate.verticalOffset || verticalOffset != oldDelegate.verticalOffset
|| screenEdgeMargin != oldDelegate.screenEdgeMargin
|| preferBelow != oldDelegate.preferBelow; || preferBelow != oldDelegate.preferBelow;
} }
} }
...@@ -264,30 +257,20 @@ class _TooltipOverlay extends StatelessWidget { ...@@ -264,30 +257,20 @@ class _TooltipOverlay extends StatelessWidget {
_TooltipOverlay({ _TooltipOverlay({
Key key, Key key,
this.message, this.message,
this.backgroundColor,
this.style,
this.borderRadius,
this.height, this.height,
this.padding, this.padding,
this.opacity,
this.animation, this.animation,
this.target, this.target,
this.verticalOffset, this.verticalOffset,
this.screenEdgeMargin,
this.preferBelow this.preferBelow
}) : super(key: key); }) : super(key: key);
final String message; final String message;
final Color backgroundColor;
final TextStyle style;
final double opacity;
final double borderRadius;
final double height; final double height;
final EdgeInsets padding; final EdgeInsets padding;
final Animation<double> animation; final Animation<double> animation;
final Point target; final Point target;
final double verticalOffset; final double verticalOffset;
final EdgeInsets screenEdgeMargin;
final bool preferBelow; final bool preferBelow;
@override @override
...@@ -302,23 +285,22 @@ class _TooltipOverlay extends StatelessWidget { ...@@ -302,23 +285,22 @@ class _TooltipOverlay extends StatelessWidget {
delegate: new _TooltipPositionDelegate( delegate: new _TooltipPositionDelegate(
target: target, target: target,
verticalOffset: verticalOffset, verticalOffset: verticalOffset,
screenEdgeMargin: screenEdgeMargin,
preferBelow: preferBelow preferBelow: preferBelow
), ),
child: new FadeTransition( child: new FadeTransition(
opacity: animation, opacity: animation,
child: new Opacity( child: new Opacity(
opacity: opacity, opacity: 0.9,
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: backgroundColor, backgroundColor: Colors.grey[700],
borderRadius: borderRadius borderRadius: 2.0
), ),
height: height, height: height,
padding: padding, padding: padding,
child: new Center( child: new Center(
widthFactor: 1.0, widthFactor: 1.0,
child: new Text(message, style: style) child: new Text(message, style: Typography.white.body1)
) )
) )
) )
......
...@@ -43,10 +43,7 @@ void main() { ...@@ -43,10 +43,7 @@ void main() {
height: 20.0, height: 20.0,
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0, verticalOffset: 20.0,
screenEdgeMargin: const EdgeInsets.all(10.0),
preferBelow: false, preferBelow: false,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -94,10 +91,7 @@ void main() { ...@@ -94,10 +91,7 @@ void main() {
height: 20.0, height: 20.0,
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0, verticalOffset: 20.0,
screenEdgeMargin: const EdgeInsets.all(10.0),
preferBelow: false, preferBelow: false,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -146,10 +140,7 @@ void main() { ...@@ -146,10 +140,7 @@ void main() {
height: 100.0, height: 100.0,
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
verticalOffset: 100.0, verticalOffset: 100.0,
screenEdgeMargin: const EdgeInsets.all(100.0),
preferBelow: false, preferBelow: false,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -167,7 +158,7 @@ void main() { ...@@ -167,7 +158,7 @@ void main() {
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
* ___ * }-100.0 margin * ___ * }- 10.0 margin
* |___| * }-100.0 height * |___| * }-100.0 height
* | * }-100.0 vertical offset * | * }-100.0 vertical offset
* o * y=300.0 * o * y=300.0
...@@ -197,13 +188,10 @@ void main() { ...@@ -197,13 +188,10 @@ void main() {
child: new Tooltip( child: new Tooltip(
key: key, key: key,
message: 'TIP', message: 'TIP',
height: 100.0, height: 190.0,
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
verticalOffset: 100.0, verticalOffset: 100.0,
screenEdgeMargin: const EdgeInsets.all(100.0),
preferBelow: false, preferBelow: false,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -222,8 +210,8 @@ void main() { ...@@ -222,8 +210,8 @@ void main() {
// we try to put it here but it doesn't fit: // we try to put it here but it doesn't fit:
/********************* 800x600 screen /********************* 800x600 screen
* ___ * }-100.0 margin * ___ * }- 10.0 margin
* |___| * }-100.0 height (starts at y=99.0) * |___| * }-190.0 height (starts at y=9.0)
* | * }-100.0 vertical offset * | * }-100.0 vertical offset
* o * y=299.0 * o * y=299.0
* * * *
...@@ -237,14 +225,14 @@ void main() { ...@@ -237,14 +225,14 @@ void main() {
* * * *
* o * y=299.0 * o * y=299.0
* _|_ * }-100.0 vertical offset * _|_ * }-100.0 vertical offset
* |___| * }-100.0 height * |___| * }-190.0 height
* * }-100.0 margin * * }- 10.0 margin
*********************/ *********************/
RenderBox tip = tester.renderObject(find.text('TIP')).parent; RenderBox tip = tester.renderObject(find.text('TIP')).parent;
expect(tip.size.height, equals(100.0)); expect(tip.size.height, equals(190.0));
expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(399.0)); expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(399.0));
expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(499.0)); expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(589.0));
}); });
testWidgets('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async { testWidgets('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async {
...@@ -262,13 +250,10 @@ void main() { ...@@ -262,13 +250,10 @@ void main() {
child: new Tooltip( child: new Tooltip(
key: key, key: key,
message: 'TIP', message: 'TIP',
height: 100.0, height: 190.0,
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
verticalOffset: 100.0, verticalOffset: 100.0,
screenEdgeMargin: const EdgeInsets.all(100.0),
preferBelow: true, preferBelow: true,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -290,14 +275,14 @@ void main() { ...@@ -290,14 +275,14 @@ void main() {
* * * *
* o * y=300.0 * o * y=300.0
* _|_ * }-100.0 vertical offset * _|_ * }-100.0 vertical offset
* |___| * }-100.0 height * |___| * }-190.0 height
* * }-100.0 margin * * }- 10.0 margin
*********************/ *********************/
RenderBox tip = tester.renderObject(find.text('TIP')).parent; RenderBox tip = tester.renderObject(find.text('TIP')).parent;
expect(tip.size.height, equals(100.0)); expect(tip.size.height, equals(190.0));
expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(400.0)); expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(400.0));
expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(500.0)); expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(590.0));
}); });
testWidgets('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async { testWidgets('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async {
...@@ -318,10 +303,7 @@ void main() { ...@@ -318,10 +303,7 @@ void main() {
height: 10.0, height: 10.0,
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
verticalOffset: 10.0, verticalOffset: 10.0,
screenEdgeMargin: const EdgeInsets.all(10.0),
preferBelow: true, preferBelow: true,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -373,10 +355,7 @@ void main() { ...@@ -373,10 +355,7 @@ void main() {
height: 10.0, height: 10.0,
padding: const EdgeInsets.all(0.0), padding: const EdgeInsets.all(0.0),
verticalOffset: 10.0, verticalOffset: 10.0,
screenEdgeMargin: const EdgeInsets.all(10.0),
preferBelow: true, preferBelow: true,
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container( child: new Container(
width: 0.0, width: 0.0,
height: 0.0 height: 0.0
...@@ -426,8 +405,6 @@ void main() { ...@@ -426,8 +405,6 @@ void main() {
child: new Tooltip( child: new Tooltip(
key: key, key: key,
message: 'TIP', message: 'TIP',
fadeDuration: const Duration(seconds: 1),
showDuration: const Duration(seconds: 2),
child: new Container(width: 0.0, height: 0.0) child: new Container(width: 0.0, height: 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