Flutter TextPainter 计算文本高度和行数

在开发中有的时候需要去计算文本的高度或者行数,从而控制展示的内容,比如进一步设置展示控件的高度,或者根据行数进行不同的内容展示。

在原生 Android 开发时,View 的绘制流程分为 measure,layout,draw 三个阶段,通过拿到 View 的 viewTreeObserver 对其添加相应的监听,一般来说添加对 layout 阶段的监听 addOnGlobalLayoutListener,因为在 layout 阶段后,就可以拿到 View 的高度 getHeight() 等信息。

在之前的文章
Android TextView实现超过固定行数折叠内容中就使用到了 addOnGlobalLayoutListener。

在 Flutter 中,可以使用 TextPainter 去计算文本的高度和行数。TextPainter 可以精细地控制文本样式,用于文本布局和绘制。TextPainter 使用 TextSpan 对象表示文本内容,将 TextSpan tree 绘制进 Canvas 中。

复制代码
    TextPainter textPainter = TextPainter(
      // 最大行数
      maxLines: maxLines,
      textDirection: Directionality.maybeOf(context),
      // 文本内容以及文本样式
      text: TextSpan(
        text: text,
        style: TextStyle(
          fontWeight: fontWeight, //字重
          fontSize: fontSize, //字体大小
          height: height, //行高
        ),
      ),
    );

TextSpan 中填充了文本内容,通过 style 控制文本样式,此外还有 maxLines、textAlign、textDirection 等参数。

复制代码
textPainter.layout(maxWidth: maxWidth);
textPainter.paint(canvas, offset);

layout 方法进行文本布局,可以设定宽度的最大值和最小值,根据设定的宽度和文本内容计算出文本的大小和位置。

paint 方法进行绘制,可以设定偏移位置,将文本绘制到 Canvas 上。

在 layout 方法之后,就可以拿到文本高度和行数这些信息了:

复制代码
// 文本的高度
textPainter.height;
// 文本的行数
textPainter.computeLineMetrics().length;

将 TextPainter 配合 LayoutBuilder 使用,以下是个简单的例子:

复制代码
    LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        const TextStyle textStyle = TextStyle(
          color: Colors.black,
          fontSize: 12,
        );
        TextPainter textPainter = TextPainter(
          textDirection: Directionality.maybeOf(context),
          text: TextSpan(text: text, style: textStyle),
        );
        textPainter.layout(maxWidth: 300);
        return SizedBox(
          width: textPainter.width,
          height: textPainter.height,
          child: Text(
            text,
            style: textStyle,
          ),
        );
      },
    ),

auto_size_text 插件,自动调整文本字体大小,也是使用的 TextPainter 配合 LayoutBuilder:

复制代码
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, size) {
      ...
      final result = _calculateFontSize(size, style, maxLines);
      final fontSize = result[0] as double;
      final textFits = result[1] as bool;

      Widget text;

      if (widget.group != null) {
        widget.group!._updateFontSize(this, fontSize);
        text = _buildText(widget.group!._fontSize, style, maxLines);
      } else {
        text = _buildText(fontSize, style, maxLines);
      }

      if (widget.overflowReplacement != null && !textFits) {
        return widget.overflowReplacement!;
      } else {
        return text;
      }
    });
  }

_calculateFontSize 方法计算出最终文本的字体大小:

复制代码
  List _calculateFontSize(
      BoxConstraints size, TextStyle? style, int? maxLines) {
    final span = TextSpan(
      style: widget.textSpan?.style ?? style,
      text: widget.textSpan?.text ?? widget.data,
      children: widget.textSpan?.children,
      recognizer: widget.textSpan?.recognizer,
    );
    ...
    int left;
    int right;
    ...
    var lastValueFits = false;
    while (left <= right) {
      final mid = (left + (right - left) / 2).floor();
      double scale;
      ...
      if (_checkTextFits(span, scale, maxLines, size)) {
        left = mid + 1;
        lastValueFits = true;
      } else {
        right = mid - 1;
      }
    }

    if (!lastValueFits) {
      right += 1;
    }

    double fontSize;
    if (presetFontSizes == null) {
      fontSize = right * userScale * widget.stepGranularity;
    } else {
      fontSize = presetFontSizes[right] * userScale;
    }

    return <Object>[fontSize, lastValueFits];
  }

可以看到使用了二分查找法来查找最合适的字体大小,_checkTextFits 方法检查某个字体大小下是否能完整展示。

复制代码
  bool _checkTextFits(
      TextSpan text, double scale, int? maxLines, BoxConstraints constraints) {
    if (!widget.wrapWords) {
      final words = text.toPlainText().split(RegExp('\\s+'));

      final wordWrapTextPainter = TextPainter(
        text: TextSpan(
          style: text.style,
          text: words.join('\n'),
        ),
        textAlign: widget.textAlign ?? TextAlign.left,
        textDirection: widget.textDirection ?? TextDirection.ltr,
        textScaleFactor: scale,
        maxLines: words.length,
        locale: widget.locale,
        strutStyle: widget.strutStyle,
      );

      wordWrapTextPainter.layout(maxWidth: constraints.maxWidth);

      if (wordWrapTextPainter.didExceedMaxLines ||
          wordWrapTextPainter.width > constraints.maxWidth) {
        return false;
      }
    }

    final textPainter = TextPainter(
      text: text,
      textAlign: widget.textAlign ?? TextAlign.left,
      textDirection: widget.textDirection ?? TextDirection.ltr,
      textScaleFactor: scale,
      maxLines: maxLines,
      locale: widget.locale,
      strutStyle: widget.strutStyle,
    );

    textPainter.layout(maxWidth: constraints.maxWidth);

    return !(textPainter.didExceedMaxLines ||
        textPainter.height > constraints.maxHeight ||
        textPainter.width > constraints.maxWidth);
  }

_checkTextFits 方法通过构建 TextPainter 对象,经过 layout 方法布局后,检查其文本内容是否未被完全展示或者其宽高是否超过了限制的宽高。

相关推荐
TE-茶叶蛋5 小时前
Uniapp、Flutter 和 React Native 全面对比
flutter·react native·uni-app
只可远观1 天前
Flutter目录结构介绍、入口、Widget、Center组件、Text组件、MaterialApp组件、Scaffold组件
前端·flutter
周胡杰1 天前
组件导航 (HMRouter)+flutter项目搭建-混合开发+分栏效果
前端·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
肥肥呀呀呀1 天前
flutter Stream 有哪两种订阅模式。
flutter
WDeLiang1 天前
Flutter - 集成三方库:日志(logger)
flutter·dart
hudawei9961 天前
flutter缓存网络视频到本地,可离线观看
flutter·缓存·音视频
0wioiw02 天前
Flutter基础()
flutter
肥肥呀呀呀2 天前
flutter 视频通话flutter_webrtc
flutter
明似水2 天前
2025年Flutter项目管理技能要求
flutter
肥肥呀呀呀2 天前
flutter使用命令生成BinarySize分析图
flutter