From 0dafe1a480d063e822c0af1a5b533fc497bbe8d2 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 17 May 2016 16:02:12 -0700 Subject: [PATCH] Add dartdoc to Tooltip (#3957) Also, remove several unused configuration options and fix an animation leak. --- .../flutter/lib/src/material/tooltip.dart | 184 ++++++++---------- .../flutter/test/material/tooltip_test.dart | 49 ++--- 2 files changed, 96 insertions(+), 137 deletions(-) diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 1dc8a92b41..bbf97deaaf 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -8,59 +8,58 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'colors.dart'; -import 'theme.dart'; +import 'typography.dart'; -const double _kDefaultTooltipBorderRadius = 2.0; -const double _kDefaultTooltipHeight = 32.0; -const EdgeInsets _kDefaultTooltipPadding = const EdgeInsets.symmetric(horizontal: 16.0); -const double _kDefaultVerticalTooltipOffset = 24.0; -const EdgeInsets _kDefaultTooltipScreenEdgeMargin = const EdgeInsets.all(10.0); -const Duration _kDefaultTooltipFadeDuration = const Duration(milliseconds: 200); -const Duration _kDefaultTooltipShowDuration = const Duration(seconds: 2); +const double _kScreenEdgeMargin = 10.0; +const Duration _kFadeDuration = const Duration(milliseconds: 200); +const Duration _kShowDuration = const Duration(seconds: 2); +/// A material design tooltip. +/// +/// 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: +/// +/// * 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({ Key key, this.message, - this.backgroundColor, - this.textColor, - this.style, - this.opacity: 0.9, - this.borderRadius: _kDefaultTooltipBorderRadius, - this.height: _kDefaultTooltipHeight, - this.padding: _kDefaultTooltipPadding, - this.verticalOffset: _kDefaultVerticalTooltipOffset, - this.screenEdgeMargin: _kDefaultTooltipScreenEdgeMargin, + this.height: 32.0, + this.padding: const EdgeInsets.symmetric(horizontal: 16.0), + this.verticalOffset: 24.0, this.preferBelow: true, - this.fadeDuration: _kDefaultTooltipFadeDuration, - this.showDuration: _kDefaultTooltipShowDuration, this.child }) : super(key: key) { assert(message != null); - assert(opacity != null); - assert(borderRadius != null); assert(height != null); assert(padding != null); assert(verticalOffset != null); - assert(screenEdgeMargin != null); assert(preferBelow != null); - assert(fadeDuration != null); - assert(showDuration != null); assert(child != null); } + /// The text to display in the tooltip. final String message; - final Color backgroundColor; - - final Color textColor; - - final TextStyle style; - - final double opacity; - - final double borderRadius; - + /// 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. @@ -68,16 +67,16 @@ class Tooltip extends StatefulWidget { /// Defaults to 16.0 logical pixels in each direction. final EdgeInsets padding; + /// The amount of vertical distance between the widget and the displayed tooltip. 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 Duration fadeDuration; - - final Duration showDuration; - /// The widget below this widget in the tree. final Widget child; @@ -94,7 +93,6 @@ class Tooltip extends StatefulWidget { } class _TooltipState extends State { - AnimationController _controller; OverlayEntry _entry; Timer _timer; @@ -102,42 +100,36 @@ class _TooltipState extends State { @override void initState() { super.initState(); - _controller = new AnimationController(duration: config.fadeDuration) - ..addStatusListener((AnimationStatus status) { - switch (status) { - case AnimationStatus.completed: - assert(_entry != null); - assert(_timer == null); - resetShowTimer(); - break; - case AnimationStatus.dismissed: - assert(_entry != null); - assert(_timer == null); - _entry.remove(); - _entry = null; - break; - default: - break; - } - }); + _controller = new AnimationController(duration: _kFadeDuration) + ..addStatusListener(_handleStatusChanged); + } + + void _handleStatusChanged(AnimationStatus status) { + switch (status) { + case AnimationStatus.completed: + assert(_entry != null); + assert(_timer == null); + resetShowTimer(); + break; + case AnimationStatus.dismissed: + assert(_entry != null); + assert(_timer == null); + _entry.remove(); + _entry = null; + break; + default: + break; + } } @override void didUpdateConfig(Tooltip oldConfig) { super.didUpdateConfig(oldConfig); - if (config.fadeDuration != oldConfig.fadeDuration) - _controller.duration = config.fadeDuration; if (_entry != null && (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.padding != oldConfig.padding || - config.opacity != oldConfig.opacity || config.verticalOffset != oldConfig.verticalOffset || - config.screenEdgeMargin != oldConfig.screenEdgeMargin || config.preferBelow != oldConfig.preferBelow)) _entry.markNeedsBuild(); } @@ -145,7 +137,7 @@ class _TooltipState extends State { void resetShowTimer() { assert(_controller.status == AnimationStatus.completed); assert(_entry != null); - _timer = new Timer(config.showDuration, hideTooltip); + _timer = new Timer(_kShowDuration, hideTooltip); } void showTooltip() { @@ -153,22 +145,16 @@ class _TooltipState extends State { RenderBox box = context.findRenderObject(); Point target = box.localToGlobal(box.size.center(Point.origin)); _entry = new OverlayEntry(builder: (BuildContext context) { - TextStyle textStyle = (config.style ?? Theme.of(context).textTheme.body1).copyWith(color: config.textColor ?? Colors.white); return new _TooltipOverlay( message: config.message, - backgroundColor: config.backgroundColor ?? Colors.grey[700], - style: textStyle, - borderRadius: config.borderRadius, height: config.height, padding: config.padding, - opacity: config.opacity, animation: new CurvedAnimation( parent: _controller, curve: Curves.ease ), target: target, verticalOffset: config.verticalOffset, - screenEdgeMargin: config.screenEdgeMargin, preferBelow: config.preferBelow ); }); @@ -197,6 +183,15 @@ class _TooltipState extends State { super.deactivate(); } + @override + void dispose() { + _controller.stop(); + _entry?.remove(); + _entry = null; + assert(_timer == null); + super.dispose(); + } + @override Widget build(BuildContext context) { assert(Overlay.of(context, debugRequiredFor: config) != null); @@ -216,12 +211,11 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { _TooltipPositionDelegate({ this.target, this.verticalOffset, - this.screenEdgeMargin, this.preferBelow }); + final Point target; final double verticalOffset; - final EdgeInsets screenEdgeMargin; final bool preferBelow; @override @@ -230,21 +224,21 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { @override Offset getPositionForChild(Size size, Size childSize) { // VERTICAL DIRECTION - final bool fitsBelow = target.y + verticalOffset + childSize.height <= size.height - screenEdgeMargin.bottom; - final bool fitsAbove = target.y - verticalOffset - childSize.height >= screenEdgeMargin.top; + final bool fitsBelow = target.y + verticalOffset + childSize.height <= size.height - _kScreenEdgeMargin; + final bool fitsAbove = target.y - verticalOffset - childSize.height >= _kScreenEdgeMargin; final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow); double y; if (tooltipBelow) - y = math.min(target.y + verticalOffset, size.height - screenEdgeMargin.bottom); + y = math.min(target.y + verticalOffset, size.height - _kScreenEdgeMargin); else - y = math.max(target.y - verticalOffset - childSize.height, screenEdgeMargin.top); + y = math.max(target.y - verticalOffset - childSize.height, _kScreenEdgeMargin); // HORIZONTAL DIRECTION - double normalizedTargetX = target.x.clamp(screenEdgeMargin.left, size.width - screenEdgeMargin.right); + double normalizedTargetX = target.x.clamp(_kScreenEdgeMargin, size.width - _kScreenEdgeMargin); double x; - if (normalizedTargetX < screenEdgeMargin.left + childSize.width / 2.0) { - x = screenEdgeMargin.left; - } else if (normalizedTargetX > size.width - screenEdgeMargin.right - childSize.width / 2.0) { - x = size.width - screenEdgeMargin.right - childSize.width; + if (normalizedTargetX < _kScreenEdgeMargin + childSize.width / 2.0) { + x = _kScreenEdgeMargin; + } else if (normalizedTargetX > size.width - _kScreenEdgeMargin - childSize.width / 2.0) { + x = size.width - _kScreenEdgeMargin - childSize.width; } else { x = normalizedTargetX - childSize.width / 2.0; } @@ -255,7 +249,6 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate { bool shouldRelayout(_TooltipPositionDelegate oldDelegate) { return target != oldDelegate.target || verticalOffset != oldDelegate.verticalOffset - || screenEdgeMargin != oldDelegate.screenEdgeMargin || preferBelow != oldDelegate.preferBelow; } } @@ -264,30 +257,20 @@ class _TooltipOverlay extends StatelessWidget { _TooltipOverlay({ Key key, this.message, - this.backgroundColor, - this.style, - this.borderRadius, this.height, this.padding, - this.opacity, this.animation, this.target, this.verticalOffset, - this.screenEdgeMargin, this.preferBelow }) : super(key: key); final String message; - final Color backgroundColor; - final TextStyle style; - final double opacity; - final double borderRadius; final double height; final EdgeInsets padding; final Animation animation; final Point target; final double verticalOffset; - final EdgeInsets screenEdgeMargin; final bool preferBelow; @override @@ -302,23 +285,22 @@ class _TooltipOverlay extends StatelessWidget { delegate: new _TooltipPositionDelegate( target: target, verticalOffset: verticalOffset, - screenEdgeMargin: screenEdgeMargin, preferBelow: preferBelow ), child: new FadeTransition( opacity: animation, child: new Opacity( - opacity: opacity, + opacity: 0.9, child: new Container( decoration: new BoxDecoration( - backgroundColor: backgroundColor, - borderRadius: borderRadius + backgroundColor: Colors.grey[700], + borderRadius: 2.0 ), height: height, padding: padding, child: new Center( widthFactor: 1.0, - child: new Text(message, style: style) + child: new Text(message, style: Typography.white.body1) ) ) ) diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 1e7936c6ae..a7a5508c3d 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -43,10 +43,7 @@ void main() { height: 20.0, padding: const EdgeInsets.all(5.0), verticalOffset: 20.0, - screenEdgeMargin: const EdgeInsets.all(10.0), preferBelow: false, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -94,10 +91,7 @@ void main() { height: 20.0, padding: const EdgeInsets.all(5.0), verticalOffset: 20.0, - screenEdgeMargin: const EdgeInsets.all(10.0), preferBelow: false, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -146,10 +140,7 @@ void main() { height: 100.0, padding: const EdgeInsets.all(0.0), verticalOffset: 100.0, - screenEdgeMargin: const EdgeInsets.all(100.0), preferBelow: false, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -167,7 +158,7 @@ void main() { await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen - * ___ * }-100.0 margin + * ___ * }- 10.0 margin * |___| * }-100.0 height * | * }-100.0 vertical offset * o * y=300.0 @@ -197,13 +188,10 @@ void main() { child: new Tooltip( key: key, message: 'TIP', - height: 100.0, + height: 190.0, padding: const EdgeInsets.all(0.0), verticalOffset: 100.0, - screenEdgeMargin: const EdgeInsets.all(100.0), preferBelow: false, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -222,8 +210,8 @@ void main() { // we try to put it here but it doesn't fit: /********************* 800x600 screen - * ___ * }-100.0 margin - * |___| * }-100.0 height (starts at y=99.0) + * ___ * }- 10.0 margin + * |___| * }-190.0 height (starts at y=9.0) * | * }-100.0 vertical offset * o * y=299.0 * * @@ -237,14 +225,14 @@ void main() { * * * o * y=299.0 * _|_ * }-100.0 vertical offset - * |___| * }-100.0 height - * * }-100.0 margin + * |___| * }-190.0 height + * * }- 10.0 margin *********************/ 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.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 { @@ -262,13 +250,10 @@ void main() { child: new Tooltip( key: key, message: 'TIP', - height: 100.0, + height: 190.0, padding: const EdgeInsets.all(0.0), verticalOffset: 100.0, - screenEdgeMargin: const EdgeInsets.all(100.0), preferBelow: true, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -290,14 +275,14 @@ void main() { * * * o * y=300.0 * _|_ * }-100.0 vertical offset - * |___| * }-100.0 height - * * }-100.0 margin + * |___| * }-190.0 height + * * }- 10.0 margin *********************/ 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.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 { @@ -318,10 +303,7 @@ void main() { height: 10.0, padding: const EdgeInsets.all(0.0), verticalOffset: 10.0, - screenEdgeMargin: const EdgeInsets.all(10.0), preferBelow: true, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -373,10 +355,7 @@ void main() { height: 10.0, padding: const EdgeInsets.all(0.0), verticalOffset: 10.0, - screenEdgeMargin: const EdgeInsets.all(10.0), preferBelow: true, - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container( width: 0.0, height: 0.0 @@ -426,8 +405,6 @@ void main() { child: new Tooltip( key: key, message: 'TIP', - fadeDuration: const Duration(seconds: 1), - showDuration: const Duration(seconds: 2), child: new Container(width: 0.0, height: 0.0) ) ),