Add dartdoc to Tooltip (#3957)
Also, remove several unused configuration options and fix an animation leak.
This commit is contained in:
parent
0e8e8bbb24
commit
0dafe1a480
@ -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:
|
||||
///
|
||||
/// * <https://www.google.com/design/spec/components/tooltips.html>
|
||||
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<Tooltip> {
|
||||
|
||||
AnimationController _controller;
|
||||
OverlayEntry _entry;
|
||||
Timer _timer;
|
||||
@ -102,8 +100,11 @@ class _TooltipState extends State<Tooltip> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = new AnimationController(duration: config.fadeDuration)
|
||||
..addStatusListener((AnimationStatus status) {
|
||||
_controller = new AnimationController(duration: _kFadeDuration)
|
||||
..addStatusListener(_handleStatusChanged);
|
||||
}
|
||||
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
switch (status) {
|
||||
case AnimationStatus.completed:
|
||||
assert(_entry != null);
|
||||
@ -119,25 +120,16 @@ class _TooltipState extends State<Tooltip> {
|
||||
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<Tooltip> {
|
||||
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<Tooltip> {
|
||||
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<Tooltip> {
|
||||
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<double> 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)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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)
|
||||
)
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user