Loading... ## 组件 封装好的组件,可以直接替代 `Text` 使用,`LRUCache` 是一种更高效的缓存结构,具体实现可以参考 <a class="post_link" href="https://yooss.cn/archives/202/"><i data-feather="file-text"></i>Dart 实现 LRUCache</a> ```dart 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` 的方式对字符串进行操作,虽然这样可能会带来额外的开销。有关字符串编码的更多内容可以查看 <a class="post_link" href="https://yooss.cn/archives/203/"><i data-feather="file-text"></i>Emoji 与 Unicode 编码</a> © 允许规范转载 打赏 赞赏作者 微信 赞