Flutter-富文本框架SuperText-Overflow效果

767 阅读4分钟

# Flutter-富文本框架SuperText

在上节文章中,通过解析拆分text,构建List<InlineSpan>的方式,实现了富文本的效果。而在常用的text功能中,还有一个收缩文本的功能经常会被使用到,无奈Flutter只能默认使用,而不能使用其他自定义的文案,且无法做到点击再展开的效果。因此这里考虑为SuperText控件加上可定制化的overflow效果。

实现原理

系统Text控件虽然无法做到自定义ellipsis文案,但是其中判定是否需要ellipsize的方法,却是可以直接拿来使用的,其实就是使用TextPainter对文本进行测量,也就能知道在有限的行数内,能否显示下全文。

但光有测量还没有用,我们还得知道,如果出现ellipsize效果了,是在哪里出现的,这个才是本文的重点。

只要知道在哪里被ellipsize了,那么只需要截取0到被ellipsize位置的文本,然后拼接上自定义的ellipsis文案即可。当然过程中还需要部分的修正。

TextPainter

这里简单介绍TextPainter的用法:

TextPainter painter = TextPainter(
    text: textSpan,
    locale: WidgetsBinding.instance?.window.locale,
    textDirection: TextDirection.ltr,
    textAlign: widget.textAlign ?? TextAlign.start,
    textScaleFactor: 1.0,
    maxLines: widget.maxLine,
);
painter.layout(maxWidth: widget.maxWidth);
print('textSize: ${painter.size}');

Text的参数差不多,主要是调用layout方法后,就能获取到绘制对应文本,所需要的宽高了。

除了最基本的绘制宽高可以获取到之外,还能够通过painter.didExceedMaxLines来获取,文本是否被ellipsize了。

painter.getPositionForOffset

getPositionForOffset方法,是对于我们知晓何处被ellipsize非常重要的方法。它是接收一个Offset参数,返回一个TextPosition对象。这个方法的作用就是传入一个Offset表示的坐标(相对于文字绘制区域),返回在这个坐标上,绘制的文字的下标(需要注意,如果用WidgetSpan替换了一段文字,那么这个WidgetSpan的文字长度只能算1,这里返回的下标,并不是原有文字的下标,而是被WidgetSpan替换过的)。

既然已经有了文字被ellipsize时的绘制区域,又有通过坐标换取文字下标的方法,自然也就能够知道文字被截断的位置。

计算文本截断位置

因为我们需要自定义ellipsis文案,那么最终的文本截断位置,是需要根据这个ellipsis文案来计算的。

所以先使用另一个TextPainter来测量ellipsis文案所需要的宽高:

TextPainter ellipsisPainter = TextPainter(
          text: TextSpan(text: ellipsisText, style: widget.ellipsisTextStyle ?? widget.style),
          locale: WidgetsBinding.instance?.window.locale,
          textDirection: TextDirection.ltr,
          textAlign: widget.textAlign ?? TextAlign.start,
          textScaleFactor: 1.0,
        );
ellipsisPainter.layout();

然后使用getPositionForOffset方法,来获取预留了用于显示ellipsis文案的空间后,文字被截断的位置:

TextPosition pos = painter.getPositionForOffset(Offset(
            max(painter.size.width - _ellipsisTextWidth! - ellipsisFontSize, 0),
            painter.size.height));

因为文字拼接实际上还需要考虑间距等问题,因此这里再减去一个ellipsisFontSize来做保险。这样就能获取到一个合适的截断位置了。

根据截断位置,裁剪List

在之前的文章中,通过富文本SpanBuilder解析后,我们已经有了一个包含全文的List的textSpan,然后现在又有了截断位置,那么我们只需要裁剪这个textSpan的内容,然后在最后追加上一个显示ellipsis文案的textSpan,就是我们最终要显示的效果。

        int c = 0;
        //从头开始遍历,查找最终被截断的位置在哪个子span中
        for (int i = 0; i < (textSpan.children?.length ?? 0); i++) {
          int c2 = c;
          InlineSpan span = textSpan.children![i];
          if (span is TextSpan) {
            c2 += span.text!.length;
          } else if (span is WidgetSpan) {
            c2 += 1;
          }
          if (c2 - 1 >= pos.offset && c - 1 <= pos.offset) {
            //找到了被截断的文字位置所在的span
            //下面就进行裁剪
            List<InlineSpan> realSpans = [];
            //先加之前从头开始的
            realSpans.addAll(textSpan.children!.sublist(0, i));
            //检查是否要拆分当前的
            if (span is TextSpan) {
              if (c2 - 1 > pos.offset) {
                //当前的span包含的文字很多,超过了最后的限制
                realSpans.add(TextSpan(
                  text: span.text!.substring(0, pos.offset - c),
                  style: span.style,
                  recognizer: span.recognizer,
                ));
              } else {
                realSpans.add(textSpan.children![i]);
              }
            }
            //追加ellipsis的TextSpan
            TapGestureRecognizer? recognizer;
            if (widget.enableExpand) {
              recognizer = TapGestureRecognizer();
              recognizer.onTap = () {
                setState(() {
                  _mExpanded = true;
                  _textSpan = _oriTextSpan;
                });
              };
            }
            realSpans.add(TextSpan(
              text: ellipsisText,
              style: widget.ellipsisTextStyle ?? widget.style,
              recognizer: recognizer,
            ));
            //返回最终裁剪完的textSpan
            return TextSpan(
              children: realSpans,
              style: widget.style,
              recognizer: textSpan.recognizer,
            );
          }
          c = c2;
        }

效果预览

下面的Emoji并不是通过unicode实现的,而是使用WidgetSpan替换的。

        Container(
            width: 160,
            child: SuperText(
              '[微笑]123456[微笑][微笑][微笑]11[微笑][微笑][微笑]123456[微笑][微笑][微笑][微笑]',
              style: const TextStyle(color: Colors.black, fontSize: 22),
              maxLine: 3,
              spanBuilders: [
                EmojiSpanBuilder(),
              ],
              maxWidth: 160,
              ellipsis: '...查看',
              ellipsisTextStyle:
                  const TextStyle(color: Colors.blue, fontSize: 18),
              overflow: TextOverflow.ellipsis,
            ),
          )

收缩状态下:

image.png

展示时:

image.png