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·性能优化
恋猫de小郭12 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
小蜜蜂嗡嗡19 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
瓜子三百克1 天前
十、高级概念
flutter
帅次1 天前
Objective-C面向对象编程:类、对象、方法详解(保姆级教程)
flutter·macos·ios·objective-c·iphone·swift·safari
小蜜蜂嗡嗡2 天前
flutter flutter_vlc_player播放视频设置循环播放失效、初始化后获取不到视频宽高
flutter
孤鸿玉2 天前
[Flutter小技巧] Row中widget高度自适应的几种方法
flutter
bawomingtian1232 天前
FlutterView 源码解析
flutter
Zender Han2 天前
Flutter 进阶:实现带圆角的 CircularProgressIndicator
flutter