Show tooltip when TextSpan is hovered over
I didn't find a direct way to do this.
But here is a possible solution using Hover on the whole RichText
and then identifying which TextSpan
is the target, and whether or not it has a tooltip.
Though, it's not an easy ride. Buckle up!
I tried to keep my application as simple as possible:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TextSpan Hover Demo',
home: HomePage(),
),
);
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _textKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: RichTextTooltipDetector(
textKey: _textKey,
child: RichText(
key: _textKey,
text: TextSpan(
text: 'Hello ',
children: <TextSpan>[
TextSpan(
text: 'bold',
style: TextStyle(fontWeight: FontWeight.bold),
semanticsLabel: 'Tooltip: Yeah! It works',
),
TextSpan(text: ' world!'),
],
),
),
),
),
);
}
}
This RichTextTooltipDetector
is where I handle the Tooltips as OverlayEntries
.
My RichTextTooltipDetector
is just a MouseRegion
on which I will handle the hover events. This event gives me the local position of the mouse. This position, together with the RichText GlobalKey, is all we need to identify whether we have a tooltip to show or not:
- From the
RichText
Global Key, I get theRenderParagraph
- From this
RenderParagraph
and thelocalPosition
of thePointerHoverEvent
, I get the `InlineSpan, if any - When I defined the
TextSpan
, I highjacked thesemanticsLabel
. This let me know easily if I need to display a Tooltip or not.
The rest is just basic OverlayEntry management:
- Creating an
OverlayEntry
based on theBuildContext
, anoffset
, and the Tooltip text - Displaying the OverlayEntry with
Overlay.of(context).insert(_tooltipOverlay);
- After 1 second, hiding the
OverlayEntry
with_tooltipOverlay?.remove();
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class _RichTextTooltipDetectorState extends State<RichTextTooltipDetector> {
OverlayEntry _tooltipOverlay;
Timer _timer;
RenderParagraph get _renderParagraph =>
widget.textKey.currentContext?.findRenderObject() as RenderParagraph;
InlineSpan _span(PointerHoverEvent event, RenderParagraph paragraph) {
final textPosition = paragraph.getPositionForOffset(event.localPosition);
return paragraph.text.getSpanForPosition(textPosition);
}
String _tooltipText(PointerHoverEvent event, RenderParagraph paragraph) {
final span = _span(event, paragraph);
return span is TextSpan &&
span.semanticsLabel != null &&
span.semanticsLabel.startsWith('Tooltip: ')
? span.semanticsLabel.split('Tooltip: ')[1]
: '';
}
OverlayEntry _createTooltip(
BuildContext context, String text, Offset offset) {
return OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
top: offset.dy,
child: Material(
elevation: 4.0,
child: Container(
decoration: BoxDecoration(
color: Colors.white70,
border: Border.all(color: Colors.black87, width: 3.0),
),
padding: EdgeInsets.all(8.0),
child: Text(text),
),
),
),
);
}
void _showTooltip() {
Overlay.of(context).insert(_tooltipOverlay);
_timer = Timer(Duration(seconds: 1), () => _hideTooltip());
}
void _hideTooltip() {
_timer?.cancel();
_tooltipOverlay?.remove();
_tooltipOverlay = null;
_timer = null;
}
void _handleHover(BuildContext context, PointerHoverEvent event) {
_hideTooltip();
final paragraph = _renderParagraph;
if (event is! PointerHoverEvent || paragraph == null) return;
final tooltipText = _tooltipText(event, paragraph);
if (tooltipText.isNotEmpty) {
RenderBox renderBox = context.findRenderObject();
var offset = renderBox.localToGlobal(event.localPosition);
_tooltipOverlay = _createTooltip(context, tooltipText, offset);
_showTooltip();
}
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onHover: (event) => _handleHover(context, event),
child: widget.child,
);
}
}
Camelid
Updated on December 28, 2022Comments
-
Camelid over 1 year
How do I show a tooltip when a particular
TextSpan
is hovered over? I found Flutter: How to display Tooltip for TextSpan inside RichText but that is for when aTextSpan
is tapped on, not hovered over with the mouse, and it uses aSnackBar
, whereas I want a tooltip.Unfortunately I cannot just wrap the
TextSpan
in aToolTip
because my use case is in an override ofTextEditingController.buildTextSpan
where I'm returning aTextSpan(children: childSpans)
, which means I have to use a subclass ofTextSpan
. -
Camelid about 3 yearsThank you for the very detailed response! However, my specific use case is for showing a tooltip when users hover up different spans of text in a
TextField
. I tried several different ways to adapt your approach for my specific use case, but because I am new to Flutter I was unable to get it to work. How should I adapt your approach for aTextField
? Thanks again! -
Thierry about 3 yearsMaybe you could update your question with a Minimal Code Sample. It always attracts more targeted answers.