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 方法布局后,检查其文本内容是否未被完全展示或者其宽高是否超过了限制的宽高。

相关推荐
火柴就是我5 小时前
flutter rust bridge 编译成so 文件 或者 .a文件 依赖到主项目
flutter·ios·rust
pengyu6 小时前
系统化掌握Flutter组件之Transform:空间魔法师
android·flutter·dart
坚果的博客7 小时前
鸿蒙版Flutter快递查询助手
flutter·华为·harmonyos
晴天学长8 小时前
一个多功能的GetX 项目代码生成工具
前端·flutter
蜡笔小新..9 小时前
Windows下配置Flutter移动开发环境以及AndroidStudio安装和模拟机配置
windows·flutter
顾林海11 小时前
深入理解 Dart 函数:从基础到高阶应用
android·前端·flutter
A0微声z12 小时前
从0到1掌握Flutter(二)环境搭建与认识工程
flutter
SoaringHeart13 小时前
Flutter进阶:局部嵌套导航实现 Navigator
前端·flutter
张风捷特烈14 小时前
Trae&Flutter | 助力 TolyUI 模块管理与发布
android·flutter·trae
恋猫de小郭15 小时前
再聊 Flutter Riverpod ,注解模式下的 Riverpod 有什么特别之处,还有发展方向
android·前端·flutter