将鼠标悬停在 TextSpan 上时显示工具提示

时间:2021-03-07 19:32:02

标签: flutter tooltip

当鼠标悬停在特定的 TextSpan 上时如何显示工具提示?我找到了 Flutter: How to display Tooltip for TextSpan inside RichText,但这是在点击 TextSpan 时,而不是用鼠标悬停,它使用 SnackBar,而我想要一个工具提示。

不幸的是,我不能只将 TextSpan 包装在 ToolTip 中,因为我的用例是在 TextEditingController.buildTextSpan 的覆盖中返回 TextSpan(children: childSpans),这意味着我必须使用 TextSpan 的子类。

1 个答案:

答案 0 :(得分:1)

我没有找到直接的方法来做到这一点。

但这里有一个可能的解决方案,在整个 RichText 上使用 Hover,然后确定哪个 TextSpan 是目标,以及它是否有工具提示。

enter image description here

不过,这并不容易。系好安全带!

我试图让我的申请尽可能简单:

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!'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

这个 RichTextTooltipDetector 是我将工具提示作为 OverlayEntries 处理的地方。

我的 RichTextTooltipDetector 只是一个 MouseRegion,我将在上面处理悬停事件。这个事件给了我鼠标的本地位置。这个位置与 RichText GlobalKey 一起,是我们确定是否有工具提示要显示的全部内容:

  1. RichText 全局密钥,我得到 RenderParagraph
  2. 从这个 RenderParagraphlocalPositionPointerHoverEvent,我得到 `InlineSpan,如果有的话
  3. 当我定义 TextSpan 时,我劫持了 semanticsLabel。这让我很容易知道是否需要显示工具提示。

剩下的只是基本的 OverlayEntry 管理:

  1. 根据 OverlayEntryBuildContext 和工具提示文本创建 offset
  2. 使用 Overlay.of(context).insert(_tooltipOverlay); 显示 OverlayEntry
  3. 1 秒后,用 OverlayEntry 隐藏 _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,
    );
  }
}