Flutter Analysis and Practice: RichText Best Practices
The detail pages are an essential part of an e-commerce app. On the page, the major technological challenge is the hybrid text layout. Xianyu has complex and varying text layout requirements. However, in several earlier versions of Flutter, there were just simple text styles with a few configurable attributes. Even rich text implemented through TextSpan links can only display text in multiple styles, for example, a basic text segment and a link segment. These could not meet the design requirements. Therefore, development of a hybrid text layout component with more powerful features was urgently needed.
2.4.1 How RichText Works
This article describes how RichText works.
1) Creation
Figure 2–14 shows the objects that are created with RichText.
- Create a
LeafRenderObjectElement
instance. - The
ComponentElement
method calls theCreateRenderObject
method of theRichText
instance to generate theRenderParagraph
instance. - The
RenderParagraph
instance creates aTextPainter
, which is a proxy class responsible for width and height calculation and text-to-canvas rendering. In addition,TextPainter
also has aTextSpan
text structure.
Finally, the RenderParagraph
instance registers itself with Dirty Nodes of the rendering module. The rendering module traverses Dirty Nodes to start RenderParagraph
rendering.
2) Rendering
As shown in Figure 2–15, the RenderParagraph
method encapsulates the logic of rendering text to the canvas by using the TextPainter
module. The calling process of RenderParagraph
is the same as RenderObject
.
During the PerfromLayout
process, TextPaint
Layout is called and AddText
is used to add text for each stage using the TextSpan
structure tree. Finally, Paragraph Layout is called to calculate the text height.
During the Paint process, clipRect
is rendered first. Then, the Paint function of TextPaint
is called to render the text through Paint of Paragraph. Finally, the drawRect
is rendered.
2.4.2 Design
Based on the text rendering principle of RichText
, we can see that TextSpan
records the text information of each text segment. TextPaint
calls a native API and calculates the width and height based on the recorded information, and renders the text to the canvas. In the traditional solutions, complex hybrid layouts are rendered using HTML WebView rich text. The performance of WebView is worse than the native implementation. To solve the problem, we tried to design a native solution for hybrid image-text layouts. The initial design was to use special spans, such as ImageSpan
and EmojiSpan
to record information and use that to calculate layouts in TextPaint
Layout. Then, special widgets were rendered separately in the Paint process. However, this solution does considerable damage to class encapsulations and RichText
and RenderParagraph
source code must be replicated and modified. The final solution is to use special characters (such as an empty string) as placeholders first and move the special spans to the positions, as shown in Figure 2-16.
This solution has two challenges:
1) How can we fill the desired positions in the text and customize for the width and height we want.
u200B represents a zero width space. According to the test for TextPainter
, the width of the layout is always 0, and fontSize
only determines the height. Therefore, we use the font size with letterSpacing
in TextStyle
to control the width and height of the special characters.
/// The amount of space (in logical pixels) to add between each letter
/// A negative value can be used to bring the letters closer.
final double letterSpacing;
2) How can we move special spans to the desired positions
From the above test, it is not hard to discern that special spans are independent of Widget and RichText
. Therefore, we need to know the relative positions of the current widgets to the RichText
spans and use Stack to integrate the widgets and RichText
. We can use the getOffsetForCaret
method of TextPaint
to obtain the relative position of the current placeholder.
/// Returns the offset at which to paint the caret.
///
/// Valid only after [layout] has been called.
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype)
2.4.3 Key Code Implementation
- Unified placeholder SpaceSpans.
SpaceSpan({
this.contentWidth,
this.contentHeight,
this.widgetChild,
GestureRecognizer recognizer,
}) : super(
style: TextStyle(
color: Colors.transparent,
letterSpacing: contentWidth,
height: 1.0,
fontSize:
contentHeight),
text: '\u200B',
recognizer: recognizer);
- Acquisition of
SpaceSpan
relative positions.
for (TextSpan textSpan in widget.text.children) {
if (textSpan is SpaceSpan) {
final SpaceSpan targetSpan = textSpan;
Offset offsetForCaret = painter.getOffsetForCaret(
TextPosition(offset: textIndex),
Rect.fromLTRB(
0.0, targetSpan.contentHeight, targetSpan.contentWidth, 0.0),
);
........
}
textIndex += textSpan.toPlainText().length;
}
- Integration of
RichText
andSpaceSpans
.
Stack(
children: <Widget>[
RichText(),
Positioned(left: position.dx, top: position.dy, child: child),
],
);
}
2.4.4 Results
As shown in Figure 2–17, the advantage of this solution is that any widget can be integrated with RichText
through SpaceSpans
, including images, custom tags, and buttons, without affecting the encapsulation of RichText
too much.
This article only discussed rich text display, which still has limitations and places that need optimization. For example, the width and height must be specified through SpaceSpans
. In addition, neither text selection nor custom text backgrounds are supported. The rich text editor needs to support image and currency formatting during text editing.