在开发中有的时候需要去计算文本的高度或者行数,从而控制展示的内容,比如进一步设置展示控件的高度,或者根据行数进行不同的内容展示。
在原生 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 方法布局后,检查其文本内容是否未被完全展示或者其宽高是否超过了限制的宽高。