Flutter 文字解读 5 | RichText 富文本的使用 (上)

6,229 阅读6分钟

零、前言

通过前四篇,我们已经了解了 Text 的源码实现和基本使用方式。其本质是使用了 RichText进行构建的,也就是说认识了 Text 就等价于认识了 RichText 。通过 Text.rich 我们也可以方便地构建富文本组件,在第三篇中介绍了一下 Text.rich,本篇就来详细地介绍一下富文本的使用。本篇和之前的几篇关系不大,可单独食用。


一、认识 InlineSpan

1. Text.rich 做了什么

Text 组件内部有一个 InlineSpan 类型的 textSpan 成员。它只能通过 Text.rich 构造进行赋值。使用 Text 普通构造时,该成员为 null。

---->[Text 源码]----
final InlineSpan textSpan;

const Text.rich(
   this.textSpan, {
   //... 略

该成员如果非空,会用于 Text#build 时,作为 RichTextTextSpanchildren ,实现富文本。


2. InlineSpan 是什么

InlineSpan 是一个抽象类,所以我们需要使用其子类,实现类有 TextSpanWidgetSpan 两个,分别用于实现多样文本样式文本中添加组件

如下面的的需求,我们需要使用 TextSpan ,在一个 TextSpan 中可以传入 List<InlineSpan> ,从而可以得到一个树状的结构。实现代码如下:

class HomePage extends StatelessWidget {
  
  final TextStyle linkStyle = const TextStyle(
      color: Colors.blue,
      decoration: TextDecoration.underline,
      decorationColor: Colors.blue);
  
  final TextStyle defaultStyle = const TextStyle(
      color: Colors.black);
  
  @override
  Widget build(BuildContext context) {
    InlineSpan span = TextSpan(children: [
      TextSpan(text: '我已同意 ', style: defaultStyle),
      TextSpan(text: '服务条款', style: linkStyle),
      TextSpan(text: ' 和 ', style: defaultStyle),
      TextSpan(text: '隐私政策', style: linkStyle),
      TextSpan(text: ' 。', style: defaultStyle),
    ]);
    return Text.rich(span);
  }
}

3.InlineSpan 中的点击事件

InlineSpan 中有一个 recognizer 成员,类型为 GestureRecognizer 。它是一个抽象类,有着很多的实现类,我们可以根据不同的手势选择不同的实现类。

其中点击事件可以使用 TapGestureRecognizer,它可以监听到 按下点击抬起取消 等事件。这样我们就可以对一个 InlineSpan 进行点击监听。效果如下:

这样就可以在点击时执行方法,跳转到对应的条款界面。要注意的是 GestureRecognizer 对象需要做 dispose 操作,代码使用如下:

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final TextStyle linkStyle = const TextStyle(
      color: Colors.blue,
      decoration: TextDecoration.underline,
      decorationColor: Colors.blue);

  final TextStyle defaultStyle = const TextStyle(color: Colors.black);

  TapGestureRecognizer _tapServer;
  TapGestureRecognizer _tapPolicy;
  
  @override
  void initState() {
    super.initState();
    _tapServer= TapGestureRecognizer()..onTap=(){
      print('点击 服务条款');
    };

    _tapPolicy= TapGestureRecognizer()..onTap=(){
      print('点击 隐私政策');
    };
  }

  @override
  void dispose() {
    _tapServer.dispose(); // 销毁对象 
    _tapPolicy.dispose(); // 销毁对象 
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    InlineSpan span = TextSpan(children: [
      TextSpan(text: '我已同意 ', style: defaultStyle),
      TextSpan(text: '服务条款', style: linkStyle, recognizer: _tapServer),
      TextSpan(text: ' 和 ', style: defaultStyle, ),
      TextSpan(text: '隐私政策', style: linkStyle,recognizer: _tapPolicy),
      TextSpan(text: ' 。', style: defaultStyle),
    ]);

    return Text.rich(span);
  }
}

4. WidgetSpan

通过 WidgetSpan 可以在文字中添加任何 Widget ,比如下面的图片。

@override
Widget build(BuildContext context) {
  final Image image = Image.asset(
    'assets/images/icon_head.webp',
    width: 20,
    height: 20,
  );
  
  InlineSpan span = TextSpan(children: [
    WidgetSpan(child: image,),
    TextSpan(text: ' 我已同意 ', style: defaultStyle),
    TextSpan(text: '服务条款', style: linkStyle, recognizer: _tapServer),
    TextSpan(text: ' 和 ', style: defaultStyle,),
    TextSpan(text: '隐私政策', style: linkStyle, recognizer: _tapPolicy),
    TextSpan(text: ' 。', style: defaultStyle),
  ]);
  return Text.rich(span);
}

WidgetSpan 中可以设置 PlaceholderAlignment 对齐方式和 基线 TextBaseline ,其中对齐方式含 baseline 字样的,必须设置 TextBaseline 。六种对齐方式如下:

到这里,我们就简单地认识完了 InlineSpan 实现富文本的用法。


二、局部文字高亮

文字很少的时候我们用 InlineSpan 来一个个拼,但是对于大段文本的展示,自己拼装是不切实际的。这时候就需要按照某些规则,进行字符串的解析,然后统一生成 InlineSpan

1.字符串解析

我们先看下面的一段文字,其中有些内容是高亮显示的。可以定义一个规则,然后进行解析。

虽然我们可以自己定义规则,但是在 .md 中已有了规则,最好还是使用共同遵守的规则,如下。


首先我们需要找到被反引号包住的字符串,下面通过写一个 StringParser 类负责文本的解析。其中主要通过 StringScanner 对文本进行扫描,通过下面的正则可以将被包裹的文字位置解析出来。

class StringParser {
  final String content;

  StringParser({this.content});

  StringScanner _scanner;

  parser() {
    _scanner = StringScanner(content);
    parseContent();
  }

  void parseContent() {
    while (!_scanner.isDone) {
      if (_scanner.scan(RegExp('`.*?`'))) {
        print(content.substring(_scanner.lastMatch.start+1,_scanner.lastMatch.end-1));
      }

      if (!_scanner.isDone) {
        _scanner.position++;
      }
    }
  }
}

2.定义高亮数据类型

我们通过有个 SpanBean 来存储 InlineSpan 需要的信息。通过 TextStyleSupport 指定高亮支持的文字样式类型。

class SpanBean {
  SpanBean(this.start, this.end);

  final int start;
  final int end;

  String text(String src) {
    return src.substring(start+1, end-1);
  }

  TextStyle get style => TextStyleSupport.dotWrapStyle;
}

class TextStyleSupport{
  static const defaultStyle = TextStyle(color: Colors.black,fontSize: 14);
  static const dotWrapStyle = TextStyle(color: Colors.purple,fontSize: 14);
}

这样在 parseContent 中,就可以将解析出的有用信息保存到 SpanBean 中,并用集合进行维护。

List<SpanBean> _spans = [];

void parseContent() {
  while (!_scanner.isDone) {
    if (_scanner.scan(RegExp('`.*?`'))) {
      int startIndex = _scanner.lastMatch.start ;
      int endIndex = _scanner.lastMatch.end ;
      _spans.add(SpanBean(startIndex, endIndex));
    }
    if (!_scanner.isDone) {
      _scanner.position++;
    }
  }
}

3.通过 SpanBean 集合生成 InlineSpan

遍历 SpanBean 集合,将其之外的只为默认样式,其区间内的文字设置为指定样式。

InlineSpan parser() {
  _scanner = StringScanner(content);
  parseContent();
  
  final List<TextSpan> spans = <TextSpan>[];
  
  int currentPosition = 0;
  
  for (SpanBean span in _spans) {
    if (currentPosition != span.start)
      spans.add(TextSpan(text: content.substring(currentPosition, span.start)));
    spans.add(TextSpan(style: span.style, text: span.text(content)));
    currentPosition = span.end;
  }
  if (currentPosition != content.length)
    spans.add(TextSpan(text: content.substring(currentPosition, content.length)));
  return TextSpan(style: TextStyleSupport.defaultStyle, children: spans);
}

4.使用

这样通过 StringParser#parser 就可以获取到 InlineSpan,进行显示。这样我们就完成了一个简易的包裹高亮的需求。使用起来也非常方便,有时只是需要高亮一些内容,没有必要用到 markdown 解析的库,这里也就百来行代码。通过自己写出来,可以对内部有更深的了解,想修改样式什么的,也就游刃有余。

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  InlineSpan span;

  final String content = """
可能说起 Flutter 绘制,大家第一反应就是用 `CustomPaint` 组件,自定义 `CustomPainter` 对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是`画出来`的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 `CustomPaint` 组件来画的,其实 `CustomPaint` 组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过`测试`、`调试`及`源码分析`来给出一些在绘制时`被忽略`或`从未知晓`的东西,而有些要点如果被忽略,就很可能出现问题。
  """;

  StringParser parser;

  @override
  void initState() {
    super.initState();
    parser = StringParser(content: content);
    span = parser.parser();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20.0),
      child: Text.rich(span),
    );
  }
}

这样一个简单的包裹高亮文本就实现了,如果想要打造自己的解析规则,也可以自己定制,这就是创造者的自由。本篇就介绍这些,在之后的文章中,将会继续拓展文本解析,比如链接的解析、Markdown 的一些基本语法等。这样 Text 就不仅是文本那么简单,还涉及着字符串的解析、正则的使用等更高阶的技能。

当我们掌握了这些能力,再回看代码的高亮显示的实现,也就会驾轻就熟。换到另一个平台上,web、Android等,我们只需知道解析的方法,整个流程都是类似的,这就是经验和能力,和绘制一样,这些能力并不会随着框架的没落而退散,你会了,它就是你的。本文就到这里,谢谢观看~


@张风捷特烈 2021.01.20 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~