Reputation: 1575
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 a TextSpan
is tapped on, not hovered over with the mouse, and it uses a SnackBar
, whereas I want a tooltip.
Unfortunately I cannot just wrap the TextSpan
in a ToolTip
because my use case is in an override of TextEditingController.buildTextSpan
where I'm returning a TextSpan(children: childSpans)
, which means I have to use a subclass of TextSpan
.
Upvotes: 2
Views: 2619
Reputation: 8383
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:
RichText
Global Key, I get the RenderParagraph
RenderParagraph
and the localPosition
of the PointerHoverEvent
, I get the `InlineSpan, if anyTextSpan
, I highjacked the semanticsLabel
. This let me know easily if I need to display a Tooltip or not.The rest is just basic OverlayEntry management:
OverlayEntry
based on the BuildContext
, an offset
, and the Tooltip textOverlay.of(context).insert(_tooltipOverlay);
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,
);
}
}
Upvotes: 2