⚠️本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

前语

一个优秀的富文本,应该包括优秀的排版算法、丰富的功能和烘托的高性能。在上一篇中,咱们完结了可扩展的、基础的富文本修改器。那么在本文中,让咱们对富文本进行更多功能的扩展。

注:

— 为了在阅览本文时有更好的体验,请先阅览本专栏第一篇,前文涉及到的知识点,本文不再赘述。(摸鱼的朋友请忽略)

— 完整代码太多, 文章只剖析中心代码,需求源码请到 代码库房

文本与图片混排

在有关富文本的业务需求中,或其他文章烘托中,图文混排的功能是非常重要的。在Flutter中,为了处理这个图文混排的问题,有一个很便利的组件:WidgetSpan。而在本专栏的第一篇的文本基础知识中,已经剖析了TextSpan在文本烘托过程中的作用。那么WidgetSpan是怎么被烘托的呢,Flutter又是怎么将TextSpanWidgetSpan混合烘托在一同的呢?

—— 作用图完整代码在库房demo/image_text

Flutter如何将文本与图片混合编辑?(功能扩展篇)

因为Flutter供给了WidgetSpan,所以作用图中的布局非常简单:

Widget _widgetSpan() {
 return Text.rich(TextSpan(
  children: <InlineSpan>[
   const TextSpan(text: 'Hello'),
   WidgetSpan(
    child: 
      ...
     //显现本地图片
     Image.file(
        _image!,
        width: width,
        height: height,
       ),
     ...
    ),
   const TextSpan(text: 'Taxze!'),
   ],
  ));
}

在之前的文章中,咱们已经知道RichText实际上是需求一个InlineSpan,而TextSpanWidgetSpan(中间还有个PlaceholderSpan)都是InlineSpan的子类完结。RichText最终会将InlineSpan传入RenderParagraph中。那么这个InlineSpan是一个什么样的呢?

InlineSpan树的结构

现在将目光先移到Text()Text.rich()的结构函数上,咱们能够看到,在Text()组件中,它的结构函数只要一个必要参数:data,且textSpan = null,而在Text.rich()的结构函数中,也只要一个必要参数:textSpan

const Text(
 String this.data, {
 super.key,
  ...
}) : textSpan = null;
​
const Text.rich(
  InlineSpan this.textSpan, {
  super.key,
   ...
  }) : data = null;

然后将目光移到build上,在其首要逻辑中,咱们能够发现,RichText在结构时传入的text是一个TextSpan,当选用data作为必要参数传入时,text参数才会有值,当选用textSpan作为参数传入时,children才不会为null。

@override
Widget build(BuildContext context) {
 Widget result = RichText(
   ...
  text: TextSpan(
   style: effectiveTextStyle,
   text: data,
   children: textSpan != null ? <InlineSpan>[textSpan!] : null,
   ),
  );
  ...
 return result;
}

经过上面的剖析之后,咱们能够将树的结构总结为两张图:

  • 当选用data作为必要参数传入时,树中只会存在一个根节点

Flutter如何将文本与图片混合编辑?(功能扩展篇)

  • 当选用textSpan作为参数传入时,树中会存在多个子树

Flutter如何将文本与图片混合编辑?(功能扩展篇)

树中的每一个TextSpan都包括text和style,其间的style是文本款式,假如没有设置某一个节点的款式,那么它会承继父节点中的款式。若根节点也没有自界说款式,那么就会选用默许的款式值。

WidgetSpan混入InlineSpan树结构

将目光移到RichTextcreateRenderObject办法上,能够看到RichText创立的烘托对象为RenderParagraph,而且将InlineSpan传入。

@override
RenderParagraph createRenderObject(BuildContext context) {
 return RenderParagraph(
  text, //InlineSpan
    ...
  );
}

再将目光移到RenderParagraphperformLayout函数上,它是RenderParagraph的重要逻辑,用于核算RenderParagraph的尺度和child的制作方位。

@override
void performLayout() {
 final BoxConstraints constraints = this.constraints;
 _placeholderDimensions = _layoutChildren(constraints);
 _layoutTextWithConstraints(constraints);
 _setParentData();
​
 final Size textSize = _textPainter.size;
 final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
 size = constraints.constrain(textSize);
  ...
}

但是,这儿核算的child不是TextSpan,而是PlaceholderSpan。经过_extractPlaceholderSpans挑选出一切的PlaceholderSpanvisitChildrenInlineSpan中的办法,经过该办法能遍历InlineSpan树。

late List<PlaceholderSpan> _placeholderSpans;
void _extractPlaceholderSpans(InlineSpan span) {
 _placeholderSpans = <PlaceholderSpan>[];
 span.visitChildren((InlineSpan span) {
  //判别是否为PlaceholderSpan
  if (span is PlaceholderSpan) {
   _placeholderSpans.add(span);
   }
  return true;
  });
}

到这儿,关于InlineSpan树的结构已经明晰了,在树中,除了TextSpan,还存在着PlaceholderSpan类型的节点,而WidgetSpan又是承继于PlaceholderSpan的。

Flutter如何将文本与图片混合编辑?(功能扩展篇)

不过,PlaceholderSpan只是一个占位节点,RenderParagraph并不会对其进行制作,RenderParagraph只负责确定它的巨细和需求制作的方位。RenderParagraph只需在布局的时分,将这个制作的区域预留给WidgetSpan,这样制作时就不会改动树的结构。

核算WidgetSpan的制作区域

performLayoutRenderParagraph的布局函数,performLayout内部首要调用了三个函数:

final BoxConstraints constraints = this.constraints;
_placeholderDimensions = _layoutChildren(constraints);
_layoutTextWithConstraints(constraints);
_setParentData();
  • _layoutChildren函数首要是用于核算承认PlaceholderSpan占位节点的巨细。

    List<PlaceholderDimensions> _layoutChildren(BoxConstraints constraints, {bool dry = false}) {
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty);
     while (child != null) {
      if (!dry) {
        ...
       childSize = child.size;
       } else {
       childSize = child.getDryLayout(boxConstraints);
       }
      placeholderDimensions[childIndex] = PlaceholderDimensions(
       size: childSize,
       alignment: _placeholderSpans[childIndex].alignment,
       baseline: _placeholderSpans[childIndex].baseline,
       baselineOffset: baselineOffset,
       );
      child = childAfter(child);
      childIndex += 1;
      }
     return placeholderDimensions;
    }
    
  • _setParentData此函数用于将父节点的设置给子节点,详细的核算(尺度核算、偏移核算)都在_layoutTextWithConstraints函数中完结。

    void _setParentData() {
      ...
     while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
      final TextParentData textParentData = child.parentData! as TextParentData;
      textParentData.offset = Offset(
       _textPainter.inlinePlaceholderBoxes![childIndex].left,
       _textPainter.inlinePlaceholderBoxes![childIndex].top,
       );
      textParentData.scale = _textPainter.inlinePlaceholderScales![childIndex];
      child = childAfter(child);
      childIndex += 1;
      }
    }
    
  • _layoutTextWithConstraints此函数包括首要的布局逻辑。其间的_textPainterRichTexttext传入RenderParagraph时,RenderParagraphtext保存在_textPainter中。setPlaceholderDimensions办法用于设置InlineSpan树中每个占位符的尺度。

    void _layoutTextWithConstraints(BoxConstraints constraints) {
     _textPainter.setPlaceholderDimensions(_placeholderDimensions);
     _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
    }
    

    setPlaceholderDimensions将各占位节点尺度设置完结之后,会调用_layoutText来进行 布局。

    void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
     final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
     //_textPainter包括节点的尺度。
     _textPainter.layout(
      minWidth: minWidth,
      maxWidth: widthMatters ?
       maxWidth :
       double.infinity,
      );
    }
    

    调用layout办法,就代表着进入了TextPainter,开端创立ParagraphBuilder,然后进入引擎层开端制作。

到这儿,咱们已经了解了图文混排中的图,是怎么被混入文本一同烘托的了。下面让咱们开端探索,怎么将文本与图片放在一同修改。

文本与图片混合修改

要想将文本与图片混合修改,就要在构建InlineSpan树时,在Image()外嵌套一层WidgetSpan,并将其混入InlineSpan树。而其间较为杂乱的是对TextRange的方位改动的核算(增加图片、删除图片)。接下让咱们一同探索,文本与图片混合修改的秘密。

Flutter如何将文本与图片混合编辑?(功能扩展篇)

输入为图像时的Style处理

若用户操作为刺进图片,则该操作不存入Style,若为文本的刺进,依据TextRange,判别所需求的Style

List<TextStyle> getReplacementsAtSelection(TextSelection selection) {
 // 只要[left replacement]才会被记载
 final List<TextStyle> stylesAtSelection = <TextStyle>[];
​
 for (final TextEditingInlineSpanReplacement replacement in replacements!) {
  if (replacement.isWidget == true) {
        //若为非修改文本操作,则暂不处理。
   } else {
     ...
    ///保存style
      stylesAtSelection
      .add(replacement.generator('', replacement.range).style!);
     ...
  }
 return stylesAtSelection;
}

构建InlineSpan树

  • 界说行为增加函数,将用户行为经过该函数保存。

    void applyReplacement(TextEditingInlineSpanReplacement replacement) {
     if (replacements == null) {
      replacements = [];
      replacements!.add(replacement);
      } else {
      replacements!.add(replacement);
      }
    }
    
  • 将用户行为映射到生成的InlineSpan

    static void _addToMappingWithOverlaps(
      InlineSpanGenerator generator,
      TextRange matchedRange,
      Map<TextRange, InlineSpan> rangeSpanMapping,
      String text,
      //非文本修改行为
       {bool? isWidget}) {
     // 在某些情况下,应该答应重叠。
     // 例如在两个TextSpan匹配相同的替换规模的情况下,
     // 测验合并到一个TextStyle的风格,并建立一个新的TextSpan。
     bool overlap = false;
     List<TextRange> overlapRanges = <TextRange>[];
     //遍历索引
     for (final TextRange range in rangeSpanMapping.keys) {
      if (math.max(matchedRange.start, range.start) <=
        math.min(matchedRange.end, range.end)) {
       overlap = true;
       overlapRanges.add(range);
       }
      }
      ...
     //更新TextRanges到InlineSpan的映射。
     rangeSpanMapping[uniqueRange] =
           TextSpan(text: uniqueRange.textInside(text), style: mergedStyles);
      ...
    }
    
  • 构建InlineSpan树

    @override
    TextSpan buildTextSpan({
     required BuildContext context,
     TextStyle? style,
     required bool withComposing,
    }) {
        //该函数其他逻辑在上一篇文章中已剖析
    }
    

经过image_picker插件,完结刺进图片

getImage(BuildContext context) async {
 //获取Editable的controller
 final ReplacementTextEditingController controller =
   _data.replacementsController;
 //界说当时行为TextRange
 final TextRange replacementRange = TextRange(
  start: controller.selection.start,
  end: controller.selection.end,
  );
 File? image;
 //默许尺度
 double width = 100.0;
 double height = 100.0;
 //从相册获取图片
 var getImage = await ImagePicker().pickImage(source: ImageSource.gallery);
 image = File(getImage!.path);
 //调用applyReplacement函数,保存用户行为
 controller.applyReplacement(
  TextEditingInlineSpanReplacement(
    replacementRange,
     (string, range) => WidgetSpan(
        child: GestureDetector(
       onTap: () {
         ...
        },
       child: Image.file(
        image!,
        width: width,
        height: height,
        ),
       )),
    true,
    isWidget: true),
  );
 _data = _data.copyWith(replacementsController: controller);
 setState(() {});
}

尾述

在这篇文章中,咱们完结了将文本与图片混合修改的功能,其他需求刺进的模块也能触类旁通完结,例如刺进视频。本专栏完结的富文本修改器关于真实的杂乱需求也只是一个小玩意,也有着较多的缺点,依托我一个人的力气也是很难完结标题中说的《高性能、多功能的富文本修改器》,本专栏旨在于引领我们走入Flutter富文本修改器的国际,而不单单只是学会使用已有的插件,却不了解其间的完结原理,当然这是一个超级大坑。例如文本与图片的排版问题…这些缺点都需求许多的时刻一点点处理处理,也期望在将来能有更多的朋友与我一同探索文本的国际。而在后续的系列文章中,将会把富文本更加的完善,完结一个笔记的Demo,也会有对富文本性能的优化与剖析。期望这篇文章能对你有所协助,有问题欢迎在谈论区留言讨论~

参阅

flutter_quill

zefyrka

关于我

Hello,我是Taxze,假如您觉得文章对您有价值,期望您能给我的文章点个❤️,有问题需求联系我的话:我在这儿 ,也能够经过的新的私信功能联系到我。假如您觉得文章还差了那么点东西,也请经过关注催促我写出更好的文章——万一哪天我进步了呢?