You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中带可点击InkWell的RichText文本溢出实现难题

Flutter 内嵌可点击InkWell的RichText实现自适应溢出省略

问题根源

RichText里的WidgetSpan是作为独立Widget布局的,不会参与文本流的宽度计算,没法获取段落剩余空间,导致内部的Text无法根据可用宽度自动截断并显示省略号。

解决方案

方案1:手动计算宽度动态截断文本

通过TextPainter计算前缀文本占据的宽度,用容器总宽度减去该值得到WidgetSpan的可用宽度,再动态截断可点击文本或后缀文本,手动添加省略号。

class EllipsisRichText extends StatefulWidget {
  final String prefixText;
  final String clickableText;
  final String suffixText;
  final TextStyle? style;
  final TextStyle? clickableStyle;
  final VoidCallback? onTap;
  final double maxWidth;

  const EllipsisRichText({
    super.key,
    required this.prefixText,
    required this.clickableText,
    required this.suffixText,
    this.style,
    this.clickableStyle,
    this.onTap,
    required this.maxWidth,
  });

  @override
  State<EllipsisRichText> createState() => _EllipsisRichTextState();
}

class _EllipsisRichTextState extends State<EllipsisRichText> {
  late String _truncatedClickableText;
  late String _truncatedSuffixText;

  @override
  void initState() {
    super.initState();
    _calculateText();
  }

  @override
  void didUpdateWidget(covariant EllipsisRichText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.prefixText != widget.prefixText ||
        oldWidget.clickableText != widget.clickableText ||
        oldWidget.suffixText != widget.suffixText ||
        oldWidget.maxWidth != widget.maxWidth ||
        oldWidget.style != widget.style ||
        oldWidget.clickableStyle != widget.clickableStyle) {
      _calculateText();
    }
  }

  void _calculateText() {
    final defaultStyle = widget.style ?? const TextStyle();
    final clickableStyle = widget.clickableStyle ?? defaultStyle.copyWith(fontWeight: FontWeight.bold);

    // 计算前缀文本宽度
    final prefixPainter = TextPainter(
      text: TextSpan(text: widget.prefixText, style: defaultStyle),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: widget.maxWidth);
    final prefixWidth = prefixPainter.width;

    // 剩余可用宽度
    double remainingWidth = widget.maxWidth - prefixWidth;

    // 先判断完整内容是否能放下
    final fullClickablePainter = TextPainter(
      text: TextSpan(text: widget.clickableText, style: clickableStyle),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: remainingWidth);
    final fullClickableWidth = fullClickablePainter.width;

    final fullSuffixPainter = TextPainter(
      text: TextSpan(text: widget.suffixText, style: defaultStyle),
      textDirection: TextDirection.ltr,
    )..layout(maxWidth: remainingWidth - fullClickableWidth);
    final fullSuffixWidth = fullSuffixPainter.width;

    if (fullClickableWidth + fullSuffixWidth <= remainingWidth) {
      _truncatedClickableText = widget.clickableText;
      _truncatedSuffixText = widget.suffixText;
      return;
    }

    // 省略号宽度
    final ellipsisPainter = TextPainter(
      text: const TextSpan(text: '...'),
      textDirection: TextDirection.ltr,
    )..layout();
    final ellipsisWidth = ellipsisPainter.width;

    // 尝试截断后缀文本
    double availableForSuffix = remainingWidth - fullClickableWidth;
    if (availableForSuffix > ellipsisWidth) {
      _truncatedClickableText = widget.clickableText;
      _truncatedSuffixText = _truncateText(
        widget.suffixText,
        defaultStyle,
        availableForSuffix - ellipsisWidth,
      ) + '...';
      return;
    }

    // 后缀放不下,截断可点击文本
    double availableForClickable = remainingWidth - ellipsisWidth;
    _truncatedClickableText = _truncateText(
      widget.clickableText,
      clickableStyle,
      availableForClickable,
    ) + '...';
    _truncatedSuffixText = '';
  }

  String _truncateText(String text, TextStyle style, double maxWidth) {
    final painter = TextPainter(
      text: TextSpan(text: text, style: style),
      textDirection: TextDirection.ltr,
      maxLines: 1,
      ellipsis: '',
    )..layout(maxWidth: maxWidth);
    final truncatedOffset = painter.getPositionForOffset(Offset(maxWidth, 0)).offset;
    return text.substring(0, truncatedOffset > 0 ? truncatedOffset : 0);
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.maxWidth,
      child: RichText(
        overflow: TextOverflow.visible,
        text: TextSpan(
          style: widget.style ?? const TextStyle(),
          children: [
            TextSpan(text: widget.prefixText),
            WidgetSpan(
              alignment: PlaceholderAlignment.middle,
              child: InkWell(
                onTap: widget.onTap,
                child: Text(
                  _truncatedClickableText,
                  style: widget.clickableStyle ?? widget.style?.copyWith(fontWeight: FontWeight.bold),
                ),
              ),
            ),
            TextSpan(text: _truncatedSuffixText),
          ],
        ),
      ),
    );
  }
}

使用示例:

EllipsisRichText(
  maxWidth: 200,
  prefixText: 'Sir Arthur Conan Doyle wrote ',
  clickableText: 'Sherlock Holmes',
  suffixText: '',
  style: const TextStyle(color: Colors.black),
  clickableStyle: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
  onTap: () {
    print('Clicked Sherlock Holmes');
  },
)

方案2:用SelectableText替代RichText(更简单)

如果能接受文本选择的特性,直接用SelectableText.rich,给可点击的TextSpan添加TapGestureRecognizer,文本流会正常计算宽度,自动处理溢出省略,还不用WidgetSpan。

SelectableText.rich(
  TextSpan(
    style: const TextStyle(color: Colors.black),
    children: [
      const TextSpan(text: 'Sir Arthur Conan Doyle wrote '),
      TextSpan(
        text: 'Sherlock Holmes',
        style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
        recognizer: TapGestureRecognizer()
          ..onTap = () {
            print('Clicked Sherlock Holmes');
          },
      ),
    ],
  ),
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
  // 可选:禁用文本选择功能
  toolbarOptions: const ToolbarOptions(
    copy: false,
    cut: false,
    paste: false,
    selectAll: false,
  ),
  showCursor: false,
)

方案对比

  • 方案1:完全自定义截断逻辑,适配各种复杂场景,但需要手动计算宽度,代码量较大。
  • 方案2:代码简洁,原生支持溢出省略,适合不需要自定义截断逻辑的场景,缺点是带SelectableText的默认特性(可通过配置禁用)。

内容的提问来源于stack exchange,提问作者Zekfad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 14:05:35