Bytedance Terminal Technology by Lin Xuebin

The background,

Since we often have button scenarios in business development, we require that the description copy in the UI presentation be as vertically centered as possible. However, during development, we often encounter the problem of text being vertical and not centered, as shown in the figure below, requiring the additional Padding property to be set. However, as the size, screen density and other factors change, the Padding value also needs to be adjusted, which requires us to put some effort into adapting.

Ii. Key information of font

2.1 Key Font information

If our Flutter application does not specify a custom font, it will Fallback to the system default font. What is the default font?

In the case of Android, the relevant matching rules are recorded in the /system/etc/fonts.xml file of the device, and the corresponding fonts are stored in/System /fonts.xml.

Chinese text in our usual application will be matched with NotosanSCjK-Regular (si Yuan bold) font by default according to the following rules.

<family lang="zh-Hans">
    <font weight="400" style="normal" index="2">NotoSansCJK-Regular.ttc</font>
    <font weight="400" style="normal" index="2" fallbackFor="serif">NotoSerifCJK-Regular.ttc</font>
</family>
Copy the code

Note: We can create an Android emulator and then get the above information from the ADB command

Then we use the font line tool to get information about the font.

pip3 install font-line # install
font-line report ttf_path # get ttf font info
Copy the code

The key information of Notosanscjk-regular obtained is as follows:

[head] Units per Em:  1000
[head] yMax:      1808
[head] yMin:     -1048
[OS/2] CapHeight:   733
[OS/2] xHeight:    543
[OS/2] TypoAscender:  880
[OS/2] TypoDescender: -120
[OS/2] WinAscent:   1160
[OS/2] WinDescent:   320
[hhea] Ascent:     1160
[hhea] Descent:    -320
[hhea] LineGap:    0
[OS/2] TypoLineGap:  0
Copy the code

There are a lot of entries in the log, by looking at glyphsapp.com/learn/verti… We can know that the information represented by HHEA (Horizontal typesetting header) is adopted on Android devices, so the key information can be extracted as

[head] Units per Em:  1000
[head] yMax:      1808
[head] yMin:     -1048
[hhea] Ascent:     1160
[hhea] Descent:    -320
[hhea] LineGap:    0
Copy the code

Are you still confused? No matter, by reading the picture below can be more clear understanding.

In the figure above, the three most critical lines are baseline, Ascent and Descent. Baseline is our horizontal line. Generally, Ascent and Descent represent the upper and lower bounds of a glyph, respectively. In the information of Notosanscjk-Regular, we can see that yMax and yMin correspond to Top and Bottom respectively in the figure, which respectively represent the upper limit and lower limit of y axis among all glyphs contained in this font. In addition, we see the LineGap parameter, which corresponds to Leading in the figure to control the size of the line spacing.

In addition, we haven’t mentioned one important parameter Units per Em sometimes referred to simply as Em, which is used to normalize information about fonts.

For example, in Flutter we set the fontSize to 10 and the density of the device to 3, so how high is the font?

Through the fontEditor (github.com/ecomfe/font…). We can get the following figure:

As can be seen from the figure above, the coordinate of the upper vertex of “zhong” is (459, 837), and the coordinate of the lower vertex is (459, -76), so the height of “zhong” is (837 + 76) = 913. As can be seen from the above NotoSans font information, the Em value is 1000. Thus the “medium” height of each unit is 0.913 and the ascent and Descent are 1160 and -320 as described above.

Again, if we use NotoSans on a device with a screen density of 3, if we set the fontSize of “medium” to 10, then

  • The height of the middle font is 27.39 to 27:10 * 3 * 0.913
  • The text border height is: 10 * 3 * (1160 + 320) / 1000= 44

That is, when the fontSize is set to 30 pixels, the height of the “middle” word is 27 pixels and the height of the text box is 44 pixels.

2.2 Why cannot the Center be vertical

As can be seen from the previous section, the LineGap is 0, that is, Leading is 0. Then the vertical layout of the Chinese text of Flutter is only related to ascent and descent, that is:

height = (accent – descent) / em * fontSize

According to the “middle” subgraph in Section 2.1, it can be seen that:

  • The center of the “middle” font is at (837 + -76) / 2 = 380
  • The ascent and Descent centre for “Chinese” is (1160 + -320) / 2 = 420

If the fontSize is 10, then on a device with a density of 3, 10 * 3 * (420-380) / 1000= 1.2 ~= 1, the center point is already off by 1 pixel, and the deviation increases as the fontSize increases. Therefore, if NotoSans information is directly used for vertical layout, it is impossible to achieve vertical centering of text.

So what’s the alternative to using the Padding? Or let’s look at it the other way, because a lot of the design principles of Flutter are very similar to Android, so let’s look at how Android is currently implemented.

3. How to achieve vertical text center in Android native

In addition to Padding, there are currently two options available on Android:

  • Set the TextViewincludeFontPaddingfalse
  • The custom View calls the paint.gettextBounds () method to get the bounds of the String

3.1 includeFontPadding implements text center

In Android, TextView by default uses yMax and yMin as the top edge and edge of the text box. If you set includeFontPadding to false, Use the upper and lower edges of Ascent and Descent.

We can in the android/text/BoringLayout. Java the init method, find the logic.

void init(CharSequence source, TextPaint paint, Alignment align,
        BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {
    // ...
    // If includePad is true, bottom and top shall prevail
    // If includePad is false, ascent and Descent will prevail
    if (includePad) {
        spacing = metrics.bottom - metrics.top;
        mDesc = metrics.bottom;
    } else {
        spacing = metrics.descent - metrics.ascent;
        mDesc = metrics.descent;
    }
    // ...
 }
Copy the code

To further verify, we exported the system’s NotosanscjK-Regular and put it into the Android project, then set the TextView’s Android :fontFamily property to that font, and then something unexpected happened.

The figure above shows that after setting the includeFontPadding property of TextView to false, The text matching is the difference between the default NotosanSCjK-Regular font (left) and the notosanSCjK-Regular font (right) specified by Android :fontFamily. The two should theoretically be identical with the same font, but they are not.

Through the breakpoint debugging in our android/graphics/Paint. Java found getFontMetricsInt method, can retrieve information contained in the font Metrics:

public int getFontMetricsInt(FontMetricsInt fmi) {
    return nGetFontMetricsInt(mNativePaint, fmi);
}
Copy the code

Experiment 1. By default, we obtain the following information

FontMetricsInt: top=-111 ascent=-97 descent=26 bottom=29 leading=0 width=0
Copy the code

Experiment 2: After setting Android :fontFamliy to NotoSans, we get the following results:

FontMetricsInt: top=-190 ascent=-122 descent=30 bottom=111 leading=0 width=0
Copy the code

Experiment 3. After setting Android :fontFamliy as Roboto, we get the following results:

FontMetricsInt: top=-111 ascent=-97 descent=26 bottom=29 leading=0 width=0
Copy the code

Note 1: The above data is in the Pixel emulator with font set to 40dp and DPI set to 420

Note 2: Roboto is the font used in numeric English

From the above three experiments, we can see that TextView uses Roboto information as its layout information by default, while Chinese finally matches NotoSans font, which happens to make the text centered, so this is not the scheme we are pursuing.

3.2 Paint. GetTextBounds () implements text center

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
  
    Paint paint = new Paint();
    paint.setColor(0xFF03DAC5);
    Rect r = new Rect();

    // Set the font size
    paint.setTextSize(dip2px(getContext(), fontSize));
    // Get the font bounds
    paint.getTextBounds(str, 0, str.length(), r);
    float offsetTop = -r.top;
    float offsetLeft = -r.left;
    r.offset(-r.left, -r.top);
    paint.setAntiAlias(true);
    canvas.drawRect(r, paint);
    paint.setColor(0xFF000000);
    canvas.drawText(str, offsetLeft, offsetTop, paint);

}
Copy the code

The above code is the logic of our operation, here we need to explain a little bit about the value of the Rect obtained. The screen coordinates are the upper-left corner as the origin, and the downward direction is the positive direction of the Y-axis. C: the bottom line of the baseline is positive, the bottom line is negative, the bottom line is negative, the bottom line is positive.

At the heart of the custom View is the getTextBounds function, and as long as we can read the information inside, we can decode the solution. But Android is open source, we in the frameworks/base/core/jni/Android/graphics/Paint. Found in a CPP implementation as follows:

static void getStringBounds(JNIEnv* env, jobject, jlong paintHandle, jstring text, jint start, jint end, jint bidiFlags, jobject bounds) {
    // Omit some code...
    doTextBounds(env, textArray + start, end - start, bounds, *paint, typeface, bidiFlags);
    env->ReleaseStringChars(text, textArray);
}

static void doTextBounds(JNIEnv* env, const jchar* text, int count, jobject bounds,
        const Paint& paint, const Typeface* typeface, jint bidiFlags) {
    // Omit some code...
    minikin::Layout layout = MinikinUtils::doLayout(&paint,
            static_cast<minikin::Bidi>(bidiFlags), typeface,
            text, count,  // text buffer
            0, count,  // draw range
            0, count,  // context range
            nullptr);
    minikin::MinikinRect rect;
    layout.getBounds(&rect);
    // Omit some code...
}
Copy the code

Let’s look at the frameworks/base/libs/hwui hwui/MinikinUtils. CPP

minikin::Layout MinikinUtils::doLayout(const Paint* paint, minikin::Bidi bidiFlags,
                                    const Typeface* typeface, const uint16_t* buf,
                                    size_t bufSize, size_t start, size_t count,
                                    size_t contextStart, size_t contextCount,
                                    minikin::MeasuredText* mt) {
    minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface);
    // Omit some code...
    return minikin::Layout(textBuf.substr(contextRange), range - contextStart, bidiFlags,
}
Copy the code

In summary, the core is to get Bounds by calling the Layout interface of Minikin. The logic associated with the Flutter is very similar to that of Android, so this solution can be applied to the Flutter.

4. Text centralization in Flutter

4.1 Related Principles and Modifications

According to section 3.2, if you want to center text in the Flutter the way Android’s getTextBounds do, the core is to call the minikin:Layout method.

We find the following call link in the existing layout logic of the Flutter:

ParagraphTxt::Layout()
    -> Layout::doLayout()
        -> Layout::doLayoutRunCached()
            -> Layout::doLayoutWord()
                ->LayoutCacheKey::doLayout()
                    -> Layout::doLayoutRun()
                        -> MinikinFont::GetBounds()
                            -> FontSkia::GetBounds()
                                -> SkFont::getWidths()
                                    -> SkFont::getWidthsBounds()
Copy the code

Where SkFont::getWidthsBounds are as follows

void SkFont::getWidthsBounds(const SkGlyphID glyphIDs[],
                             int count,
                             SkScalar widths[],
                             SkRect bounds[],
                             const SkPaint* paint) const {
    SkStrikeSpec strikeSpec = SkStrikeSpec::MakeCanonicalized(*this, paint);
    SkBulkGlyphMetrics metrics{strikeSpec};
    // Get the corresponding glyph
    SkSpan<const SkGlyph*> glyphs = metrics.glyphs(SkMakeSpan(glyphIDs, count));
    SkScalar scale = strikeSpec.strikeToSourceRatio(a);if (bounds) {
        SkMatrix scaleMat = SkMatrix::Scale(scale, scale);
        SkRect* cursor = bounds;
        for (auto glyph : glyphs) {
            Glyph ->rect()
            scaleMat.mapRectScaleTranslate(cursor++, glyph->rect()); }}if (widths) {
        SkScalar* cursor = widths;
        for (auto glyph : glyphs) {
            *cursor++ = glyph->advanceX() * scale; }}}Copy the code

So in the getTextBounds way of thinking, there is no additional layout cost, just the data stored in the above link passing through

Layout::getBounds(MinikinRect* bounds) function call to obtain and can.

During the implementation, the following points are encountered:

  • The Size used to map the Flutter is the fontSize set by the Dart layer, which causes a loss of accuracy compared to the Android fontSize x density. This results in a deviation of 1 ~ density pixels — hence the need for corresponding amplification
  • In ParagraphTxt::Layout, when height is calculated as round(max_accent + max_descent), there can be a loss of accuracy
  • In the ParagraphTxt::Layout, there is also a problem of accuracy loss for Y_offset, which is the Y-axis position of the baseline when drawing
  • Paragraph Gets the height interface in the Dart layer and calls _applyFloatingPointHack, which is value-ceiltodouble (), such as 0.0001 -> 1.0

We also mentioned the corresponding PR to the official to implement the forceVerticalCenter function. For details, see github.com/flutter/eng…

4.2 Verification

The difference between the official PR and the internal version is that we provide the drawMinHeight parameter outside, because the amount of modification to implement this part of the function is relatively large, so we are not going to provide the official PR.

In the Text, we add two parameters:

  • DrawMinHeight: Draws the minimum height
  • ForceVerticalCenter: Forces text to be vertically centered in a row, leaving all other relevant logic unchanged

Figure 4-1 Comparison of normal mode (left) and drawMinHeight (right) of FontSize from 8 to 26 on Android

Figure 4-2 Comparison between the normal mode of FontSize from 8 to 26 (left) and forceVerticalCenter (right) on Android

Five, the summary

This paper makes readers have a general impression of the vertical layout of fonts by interpreting the key information of fonts. Taking the word “middle” as an example, this paper analyzes the information of NotoSans and points out the root problem of not being in the middle. Then explore the Android native two solutions, analysis of the principle. Finally, based on the principle of Android getTextBounds scheme, the forceVerticalCenter function was implemented on the Flutter.

The team at The Flutter Infra team is working on solving these problems. This article addresses the problem of centering the text of the Flutter. There will be a series of articles on the management of Flutter problems to follow.

The resources

[1] Android Font

[2] Meaning of top, ascent, baseline, descent, bottom, and leading in Android’s FontMetrics

[3] Si Yuan Black body

[4] Typography

[5] the Android source code

[6] glyphsapp.com/learn/verti…

About the Byte Terminal Technology team

Bytedance Client Infrastructure is a global r&d team (with r&d teams in Beijing, Shanghai, Hangzhou, Shenzhen, Guangzhou, Singapore and Mountain View, USA) responsible for the construction of the entire big front-end Infrastructure of Bytedance. Improve performance, stability and engineering efficiency across the company’s product line; The supported products include but are not limited to Douyin, Toutiao, Watermelon Video, Feishu, Guagualong, etc., which have been deeply researched on mobile terminal, Web, Desktop and other terminals.

Now is the time! Client/front-end/server/intelligent algorithm/test development for global recruitment! Let’s use technology to change the world. If you are interested, please contact [email protected]. Email subject: Resume – Name – Job Objective – Expected city – Phone number.