The purpose of this series of articles is to share some things that I would like to continue to share, but due to the limitations of space and topic in The Journey of Flutter Development from South to North, readers can learn to expand here if they can.

The topic of this article is text rendering in Flutter, and it is assumed that you have a general understanding of Flutter concepts such as widgets, Elements, and RenderObjects.

The body of the

As mentioned in a previous article, there are other alternative widgets in the Flutter source code besides stateless widgets and state flow Widgets, which are directly inherited from widgets. For example, inheritedWidgets, RenderObjectWidget, StatelessWidgets and StateFluWidgets can be used to uniformly manage state. I’m sure most readers have had enough, so today we’re going to take a look at RenderObjectWidget and see how it helps us render components.

RenderObjectWidget (RenderObjectWidget, RenderObjectWidget, RenderObjectWidget, RenderObjectWidget, RenderObjectWidget) SingleChildRenderObjectWidget and MultiChildRenderObjectWidget can respectively to customize the layout of the components with a single or multiple child component.

Like StatelessWidget and StatefluWidget, RenderObjectWidget is not responsible for rendering components. It is still a member of the component tree of the Flutter tree and only holds configuration information about the Flutter rendering component. It implements the createRenderObject method to createRenderObject objects directly, which gives us a way to render custom components.

Figure 2 shows all of the subclasses of RenderObject, of which RenderBox is the most commonly used. RenderBox represents a rendering object that passes box constraints between components to render a rectangular block on the screen. In a previous article, I used it to implement a custom centered layout component. RenderParagraph, a subclass of RenderBox, is the render object that Flutter uses specifically to render text.

According to the description above, if we use RenderParagraph in RenderObjectWidget, the RenderObject object can customize a text control of its own. This is just as easy as customizinga center layout component. Custom text components may use LeafRenderObjectWidget with no children.

Before Flutter 1.7, the RichText component specifically for rendering text did inherit from the LeafRenderObjectWidget with no children as we expected. And Flutter after 1.7 RichText also needs to support internally embedded WidgetSpan components, realize the graphic function of mixed, so changed to MultiChildRenderObjectWidget to implement:

Class RichText extends LeafRenderObjectWidget {//... } / / 1.7 after class RichText extends MultiChildRenderObjectWidget {/ /... }Copy the code

This doesn’t stop us from figuring out how to render text. Finally, within the RichText, RenderParagraph this rendering object is then generated in the MultiChildRenderObjectWidget createRenderObject method, the code is as follows:

@override
RenderParagraph createRenderObject(BuildContext context) {
  assert(textDirection ! =null || debugCheckHasDirectionality(context));
  return RenderParagraph(text,
    textAlign: textAlign,
    textDirection: textDirection ?? Directionality.of(context),
    softWrap: softWrap,
    overflow: overflow,
    textScaleFactor: textScaleFactor,
    maxLines: maxLines,
    strutStyle: strutStyle,
    textWidthBasis: textWidthBasis,
    textHeightBehavior: textHeightBehavior,
    locale: locale ?? Localizations.localeOf(context, nullOk: true)); }Copy the code

RenderParagraph

After introducing the general process, we will further explore the overall structure of Flutter as follows:

RenderObjectWidget, as an ordinary widget, is definitely in the widget layer of the framework in the architecture diagram. However, its Rendering object RenderParagraph is already at the Rendering layer. RenderParagraph is a direct descendant of the RenderBox class:

class RenderParagraph extends RenderBox
    with ContainerRenderObjectMixin<RenderBox.TextParentData>,
             RenderBoxContainerDefaultsMixin<RenderBox.TextParentData>,
                  RelayoutWhenSystemFontsChangeMixin
Copy the code

That is, it still has box constraints on its width and height, and it still renders a rectangle, but the subcomponents inside the rectangle are text.

Of course, RenderParagraph is not omnipotent as a rendering object, and its internal task of rendering text mainly relies on a TextPainter type object _textPainter. The _textPainter object is used in the performLayout and paint methods of RenderParagraph to render the final text. The code is as follows:

// Take charge of layout
@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  _layoutTextWithConstraints(constraints);

  // Get the text size and lay out the child components
  final Size textSize = _textPainter.size;
  size = constraints.constrain(textSize);
  // ...
}

// Take charge of drawing
@override
void paint(PaintingContext context, Offset offset) {
  _textPainter.paint(context.canvas, offset);
  // ...
}
Copy the code

So TextPainter is what we’re going to dig into next.

TextPainter

By the time TextPainter is at the Painting layer of the framework in the architecture diagram, we are getting closer and closer to the root, where a Flutter will fragment each style of text into a UI. Each UI.Paragraph object, in turn, is generated by ParagraphBuilder, which can accept a UI.ParagraphStyle object, It is mainly used to configure the maximum number of lines, text direction, truncation method and other information of each Paragraph (we can define TextStyle in the upper layer). The _createParagraphStyle method in the TextPainter class is specifically used to generate a UI.ParagraphStyle object, which goes as follows:

ui.ParagraphStyle _createParagraphStyle([ TextDirection defaultTextDirection ]) {
  // ...
  return_text.style? .getParagraphStyle( textAlign: textAlign, textDirection: textDirection ?? defaultTextDirection, textScaleFactor: textScaleFactor, maxLines: _maxLines, textHeightBehavior: _textHeightBehavior, ellipsis: _ellipsis, locale: _locale, strutStyle: _strutStyle, ) ?? ui.ParagraphStyle( textAlign: textAlign, textDirection: textDirection ?? defaultTextDirection, maxLines: maxLines, textHeightBehavior: _textHeightBehavior, ellipsis: ellipsis, locale: locale, ); }Copy the code

As you can see from this code, TextPainter will also set a default style for the text if the user does not have a custom style. The UI.Paragraph object _paragraph is generated from this, and here is part of the layout method in TextPainter (which is called when RenderParagraph lays out the internal text) :

void layout({ double minWidth = 0.0.double maxWidth = double.infinity }) {
  if (_paragraph == null) {
    // Create a ParagraphBuilder with a feature style
    final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
    _text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
    // Create the UI.Paragraph object
    _paragraph = builder.build();
  }
  // ...
}
Copy the code

Finally, TextPainter passes the generated _Paragraph object directly into the Canvas. DrawParagraph method to render the text on the canvas:

void paint(Canvas canvas, Offset offset) {
  // ...
  canvas.drawParagraph(_paragraph, offset);
}
Copy the code

Here, two classes, ParagraphBuilder and Paragraph, actually go to Foundation, the lowest level of the framework, and readers can see that most of the functions ParagraphBuilder and Paragraph have become empty functions implemented at the engine layer.

Text rendering engine

Since the engine code of Flutter is mainly written in C/C++, it cannot be read directly in Android Studio/VSCode. Interested readers can compile their own Flutter Engine code locally or directly to the official repository (github.com/flutter/eng…

The engine used by the Flutter engine layer to render text is called LibTxt. The code is concentrated in engine/third_party/ TXT /. This library relies on several other libraries, including Minikin, ICU, HarfBuzz and Skia. We won’t go into that for the moment.

Best practices

After all, the description of the theory is a bit abstract, so let’s define a component for rendering text, which involves rewriting TextPainter and Paragraph.

The text component that we want to make, Flutter, is not provided by the official. It can be used to display the incoming text vertically, because it can be used to display our Chinese poetry, so I named it PoetryText, and the usage method is as follows:

PoetryText(
  text: TextSpan(
    text: "In front of the bed, the moon is bright, like frost on the ground. I look up at the moon and lower my head to think of home.",
    style: TextStyle(
      color: Colors.black,
      fontSize: 30,),),)Copy the code

Overall effect:

As shown above, the PoetryText component is very simple to use by taking a text argument and passing in a TextSpan of a particular style:

class PoetryText extends LeafRenderObjectWidget {
  const PoetryText({
    Key key,
    this.text,
  }) : super(key: key);

  final TextSpan text;

  @override
  RenderVerticalText createRenderObject(BuildContext context) {
    return RenderVerticalText(text);
  }

  @override
  voidupdateRenderObject( BuildContext context, RenderVerticalText renderObject) { renderObject.text = text; }}Copy the code

PoetryText inherits LeafRenderObjectWidget, and its createRenderObject method also returns a custom render object, RenderVerticalText, The updateRenderObject method here is mainly used for configuration updates to widgets.

The code for RenderVerticalText is also simple:

class RenderVerticalText extends RenderBox {
  RenderVerticalText(TextSpan text)
      : _textPainter = VerticalTextPainter(text: text);

  final VerticalTextPainter _textPainter;

  TextSpan get text => _textPainter.text;

  // Sets the text content to render
  set text(TextSpan value) {
    // Compare the old and new text
    switch (_textPainter.text.compareTo(value)) {
      case RenderComparison.identical:
      case RenderComparison.metadata:
        return;
      case RenderComparison.paint:
        _textPainter.text = value;
        markNeedsPaint();
        break;
      case RenderComparison.layout:
        _textPainter.text = value;
        markNeedsLayout();
        break; }}// Layout component size
  void _layoutText({
    double minHeight = 0.0.double maxHeight = double.infinity,
  }) {
    _textPainter.layout(
      minHeight: minHeight,
      maxHeight: maxHeight,
    );
  }

  void _layoutTextWithConstraints(BoxConstraints constraints) {
    _layoutText(
      minHeight: constraints.minHeight,
      maxHeight: constraints.maxHeight,
    );
  }

  // Calculate the minimum height of the box
  @override
  double computeMinIntrinsicHeight(double width) {
    _layoutText();
    return _textPainter.minIntrinsicHeight;
  }

  // Calculate the maximum height of the box
  @override
  double computeMaxIntrinsicHeight(double width) {
    _layoutText();
    return _textPainter.maxIntrinsicHeight;
  }

  double _computeIntrinsicWidth(double height) {
    _layoutText(minHeight: height, maxHeight: height);
    return _textPainter.width;
  }

  // Calculate the minimum box width
  @override
  double computeMinIntrinsicWidth(double height) {
    return _computeIntrinsicWidth(height);
  }

  // Calculate the maximum width of the box
  @override
  double computeMaxIntrinsicWidth(double height) {
    return _computeIntrinsicWidth(height);
  }

  // Returns the distance from the top of the text to the first baseline
  @override
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return _textPainter.height;
  }

  / / layout
  @override
  void performLayout() {
    _layoutTextWithConstraints(constraints);
    final Size textSize = _textPainter.size;
    size = constraints.constrain(textSize);
  }

  / / rendering
  @override
  voidpaint(PaintingContext context, Offset offset) { _textPainter.paint(context.canvas, offset); }}Copy the code

Each RenderObject goes through both layout and paint. For example, RenderVerticalText, inherited from RenderBox, overwrites a series of methods PerformLayout () and Paint () are used for layout and rendering, respectively.

Of course, rendering on the screen is mostly left to the VerticalTextPainter type _textPainter, which has the following code:

class VerticalTextPainter {
  VerticalTextPainter({TextSpan text}) : _text = text;

  VerticalParagraph _paragraph;
  bool _needsLayout = true;

  TextSpan get text => _text;
  TextSpan _text;

  // ...

  // called when RenderVerticalText is laid out
  void layout({double minHeight = 0.0.double maxHeight = double.infinity}) {
    if(! _needsLayout && minHeight == _lastMinHeight && maxHeight == _lastMaxHeight)return;
    _needsLayout = false;
    if (_paragraph == null) {
      final VerticalParagraphBuilder builder = VerticalParagraphBuilder(null);
      _applyTextSpan(builder, _text);
      _paragraph = builder.build();
    }
    _lastMinHeight = minHeight;
    _lastMaxHeight = maxHeight;
    // Call the layout method of _Paragraph
    _paragraph.layout(VerticalParagraphConstraints(height: maxHeight));
    if(minHeight ! = maxHeight) {final double newHeight = maxIntrinsicHeight.clamp(minHeight, maxHeight);
      if (newHeight != height)
        _paragraph.layout(VerticalParagraphConstraints(height: newHeight));
    }
  }

  // Sets VerticalParagraphBuilder parameter
  void _applyTextSpan(VerticalParagraphBuilder builder, TextSpan textSpan) {
    final style = textSpan.style;
    final text = textSpan.text;
    final boolhasStyle = style ! =null;
    if (hasStyle) {
      builder.textStyle = style;
    }
    if(text ! =null) { builder.text = text; }}// Called when RenderVerticalText is drawn
  voidpaint(Canvas canvas, Offset offset) { _paragraph.draw(canvas, offset); }}Copy the code

This class comes from TextPainter. The default implementation of TextPainter is to draw text horizontally. Here we can modify part of the logic to define the width and height of our own text components through the incoming text to achieve the vertical display of the text components.

In keeping with the Flutter source, we then delegate the task to the VerticalParagraph corresponding to the Flutter Paragraph:

class VerticalParagraph {
  VerticalParagraph(this._paragraphStyle, this._textStyle, this._text);

  ui.ParagraphStyle _paragraphStyle;
  ui.TextStyle _textStyle;
  String _text;

  // ...

  List<Word> _words = [];

  void layout(VerticalParagraphConstraints constraints) =>
      _layout(constraints.height);

  void _layout(double height) {
    if (height == _height) {
      return;
    }
    int count = _text.length;
    for (int i=0; i<count; i++) {
      _addWord(i);									// Save each word in the text
    }
    _calculateLineBreaks(height);		// Compute a newline
    _calculateWidth();							// Calculate the width
    _height = height;
    _calculateIntrinsicHeight();		// Calculate the height
  }

  void _addWord(int index) {
    finalbuilder = ui.ParagraphBuilder(_paragraphStyle) .. pushStyle(_textStyle) .. addText(_text.substring(index, index +1));
    final paragraph = builder.build();
    paragraph.layout(ui.ParagraphConstraints(width: double.infinity));
    // Save each Word in a UI.Paragraph object and wrap it in Word into the _words list
    final run = Word(index, paragraph);
    _words.add(run);
  }

  List<LineInfo> _lines = [];

  void _calculateLineBreaks(double maxLineLength) {
    if (_words.isEmpty) {
      return;
    }
    if (_lines.isNotEmpty) {
      _lines.clear();
    }

    int start = 0;
    int end;
    double lineWidth = 0;
    double lineHeight = 0;
    // Iterate over each Word object saved previously
    for (int i=0; i<_words.length; i++) {
      end = i;
      final word = _words[i];
      final wordWidth = word.paragraph.maxIntrinsicWidth;
      final wordHeight = word.paragraph.height;
      // Meet ", ", ". Line wrap saves the width and height of each line and calls _addLine to the _lines list
      if (_text.substring(i, i + 1) = ="," || _text.substring(i, i + 1) = ="。") {
        lineWidth += math.max(lineWidth, wordWidth);
        _addLine(start, end+1, lineWidth, lineHeight);
        start = end + 1;
        lineWidth = 0;
        lineHeight = 0;
      } else {
        // Before the end of a line, the total height of the line should be added to the height of the text
        lineHeight += wordHeight;
      }
    }
    end = _words.length;
    if(start < end) { _addLine(start, end, lineWidth, lineHeight); }}void _addLine(int start, int end, double width, double height) {
    final bounds = Rect.fromLTRB(0.0, width, height);
    final LineInfo lineInfo = LineInfo(start, end, bounds);
    _lines.add(lineInfo);
  }

  // The width is the sum of the width of each line
  void _calculateWidth() {
    double sum = 0;
    for (LineInfo line in _lines) {
      sum += line.bounds.width;
    }
    _width = sum;
  }

  // Take the longest line in the poem (vertical height)
  void _calculateIntrinsicHeight() {
    double sum = 0;
    double maxRunHeight = 0;
    for (LineInfo line in _lines) {
      sum += line.bounds.width;
      maxRunHeight = math.max(line.bounds.height, maxRunHeight);
    }
    _minIntrinsicHeight = maxRunHeight;
    _maxIntrinsicHeight = maxRunHeight;
  }

  // After calculating the width and height of each text and line,
  // This is where you can draw text to canvas.
  void draw(Canvas canvas, Offset offset) {
    canvas.save();
    // Move to the starting position
    canvas.translate(offset.dx, offset.dy);
    // Draw each line
    for (LineInfo line in _lines) {
      // Move to the beginning of the drawing line
      canvas.translate(line.bounds.width + 20.0);
      // Iterate over each word
      double dy = 0;
      for (int i = line.textRunStart; i < line.textRunEnd; i++) {
        // Draw the text ui.paragraph in each line, with the offset being the position of the word on the line
        canvas.drawParagraph(_words[i].paragraph, Offset(0, dy)); dy += _words[i].paragraph.height; } } canvas.restore(); }}Copy the code

As the code above shows, you can really draw the passed text into the canvas that the system provides us, where all we need to do for the layer below is wrap the passed text in a UI.ParagraphBuilder in a UI. Paragraphobject. Then draw it and pass it to Canvas.drawParagraph ().

This completes our custom PoetryText component, which contains the following important parts:

  • PoetryText, Widget that inherits LeafRenderObjectWidget.
  • RenderVerticalText, the RenderObject of a component that inherits RenderBox.
  • VerticalTextPainter, rewritten from TextPainter.
  • VerticalParagraph, adapted from Paragraph.

For the complete code, see: github.com/MeandNi/flu…

read

Flutter. Cn: API. Flutter – IO. Cn/Flutter/dar…


My new book, The Journey of Flutter Development from South to North, is finally available! (Book raffle)

The drawing is in progress….