Mobile Development 10 min read

Implementing Text and Image Mixed Editing in a Flutter Rich Text Editor

This article explains how to extend a Flutter rich‑text editor to support mixed text‑image layout by using WidgetSpan, InlineSpan trees, and the image_picker plugin, providing detailed code examples and a deep dive into the rendering pipeline.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Text and Image Mixed Editing in a Flutter Rich Text Editor

A high‑quality rich‑text editor requires a solid layout algorithm, rich features, and high performance. Building on a previous basic editor, this article shows how to add mixed text‑image capabilities in Flutter.

Flutter provides the WidgetSpan component for embedding widgets such as images inside text. Combined with TextSpan , it enables seamless rendering of text and images together.

Example usage:

Widget _widgetSpan() {
  return Text.rich(TextSpan(
    children:
[
      const TextSpan(text: 'Hello'),
      WidgetSpan(
        child: 
          ...
          // display local image
          Image.file(
            _image!,
            width: width,
            height: height,
          ),
        ...
      ),
      const TextSpan(text: 'Taxze!'),
    ],
  ));
}

The rendering flow starts with RichText , which receives an InlineSpan (either a TextSpan or a WidgetSpan ) and passes it to RenderParagraph . The build method of RichText constructs the appropriate TextSpan or WidgetSpan based on whether the data or textSpan parameter is provided.

Constructor signatures:

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

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

Both TextSpan and WidgetSpan inherit from InlineSpan . WidgetSpan itself extends PlaceholderSpan , which represents a placeholder node in the layout tree.

RenderParagraph creates its render object via createRenderObject and performs layout in performLayout . During layout it extracts PlaceholderSpan dimensions, lays out the text, and assigns parent data to each child.

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

Key layout functions:

List
_layoutChildren(BoxConstraints constraints, {bool dry = false}) {
  final List
placeholderDimensions = List
.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;
}

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;
  }
}

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

To insert an image, the article uses the image_picker plugin. The following function obtains an image from the gallery, creates a WidgetSpan wrapping an Image.file , and records the replacement in the editor controller.

getImage(BuildContext context) async {
  final ReplacementTextEditingController controller = _data.replacementsController;
  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);
  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(() {});
}

The article concludes that while the current implementation enables mixed text‑image editing, further work is needed to handle complex layout issues, performance optimization, and additional media types such as video.

FlutterRichTextTextSpanRich Text EditorWidgetSpanImageInlineSpan
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.