Flutter Analysis and Practice: RichText Best Practices

Image for post
Image for post

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.

Image for post
Image for post
Figure 2–14
  • Create a instance.
  • The method calls the method of the instance to generate the instance.
  • The instance creates a , which is a proxy class responsible for width and height calculation and text-to-canvas rendering. In addition, also has a text structure.

Finally, the instance registers itself with Dirty Nodes of the rendering module. The rendering module traverses Dirty Nodes to start rendering.

2) Rendering

As shown in Figure 2–15, the method encapsulates the logic of rendering text to the canvas by using the module. The calling process of is the same as .

Image for post
Image for post
Figure 2–15

During the process, Layout is called and is used to add text for each stage using the structure tree. Finally, Paragraph Layout is called to calculate the text height.

During the Paint process, is rendered first. Then, the Paint function of is called to render the text through Paint of Paragraph. Finally, the is rendered.

2.4.2 Design

Based on the text rendering principle of , we can see that records the text information of each text segment. 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 and to record information and use that to calculate layouts in Layout. Then, special widgets were rendered separately in the Paint process. However, this solution does considerable damage to class encapsulations and and 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.

Image for post
Image for post
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 , the width of the layout is always 0, and only determines the height. Therefore, we use the font size with in 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 . Therefore, we need to know the relative positions of the current widgets to the spans and use Stack to integrate the widgets and . We can use the method of 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 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 and .
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 through , including images, custom tags, and buttons, without affecting the encapsulation of too much.

Image for post
Image for post
Figure 2–17

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 . 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.

Original Source:

Written by

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store