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

相关推荐
Geek_Vison6 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo6 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_738185826 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色12 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile1 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭2 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白2 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫2 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘2 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185823 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙