组件
封装好的组件,可以直接替代 Text
使用,LRUCache
是一种更高效的缓存结构,具体实现可以参考 Dart 实现 LRUCache
class EllipsisText extends StatelessWidget {
final String text;
final TextStyle? style;
final String ellipsis;
final int? maxLines;
static final _cache = LRUCache<int, String>(maxSize: 1000);
/// 当字体相关属性发生变化时,需要清空缓存
static void clearCache() {
_cache.clear();
}
const EllipsisText(
this.text, {
super.key,
this.style,
this.ellipsis = '...',
this.maxLines,
});
int _getCacheKey(
String text,
String ellipsis,
double maxWidth,
int? maxLines,
TextStyle? style,
TextScaler textScaler,
) {
return Object.hash(
text,
ellipsis,
maxWidth.toStringAsFixed(2),
maxLines,
style,
textScaler,
);
}
double _calculateTextWidth(String text, TextScaler textScaler) {
final span = TextSpan(text: text.fixAutoLines(), style: style);
final tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr,
textScaler: textScaler,
)..layout();
return tp.width;
}
TextPainter _createTextPainter(
String displayText,
double maxWidth,
TextScaler textScaler,
) {
return TextPainter(
text: TextSpan(text: displayText.fixAutoLines(), style: style),
maxLines: maxLines,
textDirection: TextDirection.ltr,
textScaler: textScaler,
)..layout(maxWidth: maxWidth);
}
@override
Widget build(BuildContext context) {
final textScaler = MediaQuery.textScalerOf(context);
return LayoutBuilder(builder: (context, constraints) {
final cacheKey = _getCacheKey(
text,
ellipsis,
constraints.maxWidth,
maxLines,
style,
textScaler,
);
if (text.isEmpty) {
return const SizedBox.shrink();
}
final cachedResult = _cache.get(cacheKey);
if (cachedResult != null) {
return Text(
cachedResult,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
style: style,
textScaler: textScaler,
);
}
if (!_createTextPainter(
text,
constraints.maxWidth,
textScaler,
).didExceedMaxLines) {
_cache.put(cacheKey, text.fixAutoLines());
return Text(
text.fixAutoLines(),
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
style: style,
textScaler: textScaler,
);
}
int leftIndex = 0;
int rightIndex = text.characters.length;
double leftWidth = 0;
double rightWidth = 0;
int lastValidLeftIndex = 0;
int lastValidRightIndex = text.characters.length;
while (leftIndex < rightIndex) {
final nextLeftWidth = _calculateTextWidth(
text.characters.elementAt(leftIndex), textScaler) +
leftWidth;
final nextRightWidth = _calculateTextWidth(
text.characters.elementAt(rightIndex - 1), textScaler) +
rightWidth;
final currentText =
'${text.charSubstring(0, leftIndex)}$ellipsis${text.charSubstring(rightIndex)}';
if (_createTextPainter(
currentText,
constraints.maxWidth,
textScaler,
).didExceedMaxLines) {
break;
} else {
lastValidLeftIndex = leftIndex;
lastValidRightIndex = rightIndex;
if (leftWidth <= rightWidth) {
leftWidth = nextLeftWidth;
leftIndex++;
} else {
rightWidth = nextRightWidth;
rightIndex--;
}
}
}
final truncatedText =
'${text.charSubstring(0, lastValidLeftIndex)}$ellipsis${text.charSubstring(lastValidRightIndex)}';
_cache.put(cacheKey, truncatedText.fixAutoLines());
return Text(
truncatedText.fixAutoLines(),
maxLines: maxLines,
style: style,
textScaler: textScaler,
);
});
}
}
extension StringExt on String {
String fixAutoLines() => characters.join('\u{200B}');
String charSubstring(int start, [int? end]) =>
characters.getRange(start, end).join();
String removeLineBreaks() => replaceAll(RegExp(r'[\r\n]+'), '');
}
效果图
实现思路
由于我们平时所用的字体基本都不是等宽字体,所以想要文本在中间省略就不能单纯的通过计算字符的数量来判断,因为我们需要在实际渲染宽度的中间省略。
TextPainter
好在 Flutter 为我们提供了一个可以高效计算文本渲染相关的 Object,通过使用 TextPainter,我们可以在不实际渲染文本的情况下获取文本的信息。
基本原理
对于多行文本来说,我们首先需要判断能不能放得下,如果没有超出所需要的行数,直接返回即可,这样可以减少计算的开销。
TextPainter
提供了一个 didExceedMaxLines
方法来知道有没有超出最大行数。
双指针
知道基本原理之后实现起来就比较容易了,我们可以选择使用一个比较简单的双指针,分别从文字开头和文字末尾往中间收缩。为了保证省略两侧宽度相等,当左侧宽度较小时,右移左指针,当右侧宽度较小时,左移右指针。
实现细节
Text 组件按字符换行
可能你会注意到上面的方法中使用了一个扩展,这个扩展的意义是,Flutter 在 Text 组件中,默认会按照词换行,也就是说 Flutter 总是希望一个单词能够完整的显示,如果是这样的话,可能会出现空行的情况,因为并不是所有的单词都能够刚好占满一整行。如果我们需要实现字符换行,一种比较简易的方法是在文本中插入零宽字符,这种字符不会显示出来,但是会破坏字符串原本的完整性,很多隐形水印也是通过零宽字符来实现的。
字符串中的 Characters
为了考虑更多的情况,比如带有 emoji 的字符串,我们就不能通过 String
中的方法对字符串进行操作了,因为编码格式的原因,对带有 emoji 的字符串进行 substring
、length
时,可能会得到预期之外的值,甚至导致异常。所以对于比较通用的组件,在可预见的未来中无法知道会不会出现 emoji 的情况,我们就需要通过 Characters
的方式对字符串进行操作,虽然这样可能会带来额外的开销。有关字符串编码的更多内容可以查看 Emoji 与 Unicode 编码