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 方法計算出最終文本的字體大?。?/p>

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容