Flutter 中的图文混排与原理解析

在移动开发中图文混排是十分常见的业务需求,以下图效果所示,本篇将介绍在 Flutter 中的图文混排效果与实现原理。git

事实上,针对如上所示的图文混排需求,Flutter 官方提供了十分便捷的实现方式: WidgetSpangithub

以下代码所示,经过 Text.rich 接入 TextSpanWidgetSpan 就能够快速实现图文混排的需求,而且能够看出 WidgetSpan 不止支持图片控件,它能够接入任何你须要的 Widget ,好比 CardInkWell 等等。bash

Text.rich(TextSpan(
      children: <InlineSpan>[
        TextSpan(text: 'Flutter is'),
        WidgetSpan(
            child: SizedBox(
          width: 120,
          height: 50,
          child: Card(
              color: Colors.blue,
              child: Center(child: Text('Hello World!'))),
        )),
        WidgetSpan(
            child: SizedBox(
          width: size > 0 ? size : 0,
          height: size > 0 ? size : 0,
          child: new Image.asset(
            "static/gsy_cat.png",
            fit: BoxFit.cover,
          ),
        )),
        TextSpan(text: 'the best!'),
      ],
    )
复制代码

也就是说 WidgetSpan 支持在文本中插入任意控件,这大大提高了 Flutter 中富文本的自定义效果,好比上述演示效果中随意改变图片的大小。布局

那为何 WidgetSpan 能够如何方便地实现文本和 Widget 混合效果呢?这就要从 Text 的实现提及学习

实现原理

咱们经常使用的 Text 控件其实只是 RichText 的封装,而 RichText 的实现以下图所示,主要能够分为三部分:MultiChildRenderObjectWidgetMultiChildRenderObjectElementRenderParagraphui

正如咱们知道的, Flutter 控件通常是由 WidgetElementRenderObeject 三部分组成,而在 RichText 中也是如此,其中:spa

  • RenderParagraph 主要是负责文本绘制、布局相关;
  • RichText 继承 MultiChildRenderObjectWidget 主要是须要经过 MultiChildRenderObjectElement 来处理 WidgetSpan 中 children 控件的插入和管理。

WidgetSpan 到底是如何混入在文本绘制中呢?

在前面的使用中,咱们首先是传入了一个 TextSpanRichText ,并在 TextSpanchildren 中拼接咱们须要的内容,那就从 RichText 开始挖掘其中的原理。3d

如上代码所示,这里咱们首先看 RichText 的入口,能够看到 RichText 开始是有一个 _extractChildren 方法,这个方法主要是将传入 TextSpanchildren 里,全部的 WidgetSpan 经过 visitChildren 方法给递归筛选出来,而后传入给父类 MultiChildRenderObjectWidgetcode

为何须要这么作?在 《十6、详解自定义布局实战》 中介绍过,MultiChildRenderObjectWidget 的 children 最终会经过 MultiChildRenderObjectElement 做为桥梁,而后被插入到须要管理和绘制的 child 链表结构中,这样在 RenderObject 中方便管理和访问。cdn

另外咱们知道 RichText 传入的 text 实际上是一个 InlineSpan ,而 TextSpan 就是 InlineSpan 的子类,WidgetSpan 也是 InlineSpan 的子类实现,它们的关系以下图所示:

对于 InlineSpan 系列咱们主要关注两个方法:visitChildrenbuild 方法,它的子类 TextSpanWidgetSpan 都对这两个方法有本身对应的实现。

void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions });

  bool visitChildren(InlineSpanVisitor visitor);
复制代码

接着看 RenderParagraph ,如上代码所示,RichText 中的 textInlineSpan) 会继续被传入到 RenderParagraph 中,RenderParagraph 继承了 RenderBox 并混入的 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin 等。

混入的对象这部分在内容在 《十6、详解自定义布局实战》 也介绍过,这里只须要知道经过混入它们, RenderParagraph 就能够得到前面经过 WidgetSpan 传入到 MultiChildRenderObjectElement 的 children 链表,而且布局计算大小等。

以后 RenderParagraph 中的 text 以后会被放置到 TextPainter 中使用,而且经过 _extractPlaceholderSpans 方法将全部的 PlaceholderSpans 筛选出来。

TextPainter 主要用于实现文本的绘制,这里咱们暂时很少分析,_extractPlaceholderSpans 挑选出来的全部 PlaceholderSpans ,其实就是 WidgetSpan

WidgetSpan 是经过继承 PlaceholderSpans 从而实现了 InlineSpan,而目前暂时 PlaceholderSpans 实现的类只有 WidgetSpan

挑选出来的 List<PlaceholderSpan> 们会在 RenderParagraph 计算宽高等方法中被用到,好比 computeMaxIntrinsicWidth 方法等,其中主要有 _canComputeIntrinsics_computeChildrenWidthWithMaxIntrinsics_layoutText 三个关键方法,这三个方法结合处理了 RenderParagraph 中 Span 的尺寸和布局等。

  • _canComputeIntrinsics_canComputeIntrinsics 主要判断了 PlaceholderSpan 只支持的 baseline 配置。

  • _computeChildrenWidthWithMaxIntrinsics_computeChildrenWidthWithMaxIntrinsics 中会经过 PlaceholderSpan 去对应获得 PlaceholderDimensions,获得的 PlaceholderDimensions 会用于后续如 WidgetSpan 的大小绘制信息。

这个 PlaceholderDimensions 会经过 setPlaceholderDimensions 方法设置到 TextPainter 里面, 这样 TextPainterlayout 的时候,就会将 PlaceholderDimensions 赋予 WidgetSpan 大小信息。

  • _layoutText: _layoutText 方法会调用 _textPainter.layout, 从而执行 _text.build 方法,这个方法就会触发 children 中的 WidgetSpan 去执行 build

因此以下代码所示,_textPainter.layout 会执行 Span 的 build 方法,将 PlaceholderDimensions 设置到 WidgetSpan 里面,而后还有经过 _paragraph.getBoxesForPlaceholders() 方法获取到控件绘制须要的 leftright 等信息,这些信息来源是基于上面 text.build 的执行。

_paragraph.getBoxesForPlaceholders() 获取到的 TextBox 信息,是基于后面咱们介绍在 Span 里提交的 addPlaceholder 方法获取。

这些信息会在 setParentData 方法中被设置到 TextParentData 里,关于 ParentData 及其子类的做用,在《十6、详解自定义布局实战》 一样有所介绍,这里就不赘述了,简单理解就是 WidgetSpan 绘制的时候所须要的 offset 位置信息会由它们提供。

以后以下代码所示, WidgetSpanbuild 方法被执行,这里会有一个 placeholderCountplaceholderCount 默认是从 0 开始,而在执行 addPlaceholder 方法时会经过 _placeholderCount++ 自增,这样下一个 WidgetSpan 就会拿到下一个 PlaceholderDimensions 用于设置大小。

addPlaceholder 以后会执行到 Flutter Engine 中的流程了。

最终 RenderParagrashpaint 方法会执行 _textPainter.paint 并把肯定了大小和位置的 child 提交绘制。

是否是有点晕,结合下图所示,总结起来其实就是:

  • RichText 中传入 TextSpan , 在 TextSpan 的 children 中使用 WidgetSpanWidgetSpan 里的 Widget 们会转成 MultiChildRenderObjectElementchildren, 处理后获得一个 child 链表结构;
  • 以后 TextSpan 进入 RenderParagrash ,会抽取出对应 PlaceholderSpanWidgetSpan),而后经过转化为 PlaceholderDimensions 保存大小等信息;
  • 以后进去 TextPainter 会触发 InlineSpanbuild 方法,从而将前面获得的 PlaceholderDimensions 传递到 WidgetSpan 中;
  • WidgetSpan 中的控件信息经过 addPlaceholder 会被传递到 Paragraph
  • 以后 TextPainter 中经过 addPlaceholder 的信息获取,调用 _paragraph.getBoxesForPlaceholders() 获取去控件绘制须要的 offset
  • 有了大小和位置,最终文本中插入的控件,会在 RenderParagrashpaint 方法被绘制。

RichText 中插入控件的管理巧妙的依托到 MultiChildRenderObjectWidget 中,从而复用了本来控件的管理逻辑,以后依托引擎计算位置从而绘制完成。

至此,简简单单的 WidgetSpan 的实现原理解析完成~

资源推荐

相关文章
相关标签/搜索