- Published on
How to create an animated tooltip widget with Flutter ?
You can find the result here and the complete source code here.
Create a tooltip shape with Shape Border
First, we are going to create a Shape Border we'll apply later to a container to get a perfect tooltip shape:
import 'package:flutter/material.dart';
class TooltipShapeBorder extends ShapeBorder {
final double arrowWidth;
final double arrowHeight;
final double arrowArc;
final double radius;
const TooltipShapeBorder({
this.radius = 10.0,
this.arrowWidth = 20.0,
this.arrowHeight = 10.0,
this.arrowArc = 0.0,
}) : assert(arrowArc <= 1.0 && arrowArc >= 0.0);
EdgeInsetsGeometry get dimensions => EdgeInsets.only(bottom: arrowHeight);
Path getInnerPath(Rect rect, {TextDirection? textDirection}) => Path();
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
rect = Rect.fromPoints(
rect.topLeft, rect.bottomRight - Offset(0, arrowHeight));
double x = arrowWidth, y = arrowHeight, r = 1 - arrowArc;
return Path()
..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(radius)))
..moveTo(rect.bottomCenter.dx + x / 2, rect.bottomCenter.dy)
..relativeLineTo(-x / 2 * r, y * r)
..relativeQuadraticBezierTo(
-x / 2 * (1 - r), y * (1 - r), -x * (1 - r), 0)
..relativeLineTo(-x / 2 * r, -y * r);
}
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
ShapeBorder scale(double t) => this;
}
Adding core widgets and animations
To get a smooth effect of pop, the trick is to play with two physics properties:
- scale
- fade
Scale
For a pop effect, we will use the easeOutBack curved animation in order to have a tooltip growing until a point it becomes bigger than its final size and go back to its target size.
Fade
To make the animation smooth, we will use the easeIn curved animation to have a progressively fade-in effect.
import 'package:flutter/material.dart';
import 'package:flutter_my_awesome_uis/fade_and_bounce_tooltip/tooltip_shape_border.dart';
class AwesomeTooltip extends StatefulWidget {
const AwesomeTooltip({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
State<AwesomeTooltip> createState() => _AwesomeTooltipState();
}
class _AwesomeTooltipState extends State<AwesomeTooltip>
with TickerProviderStateMixin {
late AnimationController _opacityController;
late AnimationController _scaleController;
late Animation<double> _opacityAnimation;
late Animation<double> _scaleAnimation;
void initState() {
super.initState();
_opacityController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_opacityAnimation =
CurvedAnimation(parent: _opacityController, curve: Curves.easeIn);
_scaleController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500), value: 0.3);
_scaleAnimation =
CurvedAnimation(parent: _scaleController, curve: Curves.easeOutBack);
_scaleController.forward();
_opacityController.forward();
}
void dispose() {
_opacityController.dispose();
_scaleController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacityAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Container(
decoration: const ShapeDecoration(
color: Colors.green,
shape: TooltipShapeBorder(arrowArc: 0.3, radius: 8),
shadows: [
BoxShadow(
color: Colors.black26, blurRadius: 4.0, offset: Offset(2, 2))
],
),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text(
"Wow, this tooltip is awesome ! ",
style: TextStyle(color: Colors.white),
),
),
),
),
);
}
}
Play with the code, change constants and see what fits your need !