Bytedance Terminal Technology — Lin Xuebin

The background,

Given that we often have button scenarios in business development, we want the description text to be as vertically centered as possible in terms of UI presentation. However, in the process of development, we often encounter the problem that the text is not vertically centered as shown in the figure below, and need to set additional Padding attribute. However, as the size and screen density of the phone change, the Padding value also needs to be adjusted, which requires our r&d staff to put some effort into adaptation.

Second, font key information

2.1 Font Information

If our Flutter application does not specify a custom font, it will Fallback to the system default font. So what is the default font? For Android, the matching rules are recorded in the /system/etc/fonts. XML file of the device, and the corresponding fonts are stored in /system/fonts. According to the following rules, the Chinese text in our daily application will be matched with the NotoSANSCjk-regular font by default.

<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 get the above information via the ADB command

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

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

The key information about NotoSANscjk-regular 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 see that the information represented by hHEA (Horizontal typesetting Header) is used on Android devices, so the key information can be extracted as follows:

[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? That’s ok. Read the picture below to get a clearer picture.

In the figure above, the three most critical lines are Baseline, Ascent and Descent. Baseline can be understood as our horizontal line. In general, Ascent and Descent represent the upper and lower limits of the glyph drawing area respectively. In notoSANSCjk-regular’s information, we can see that yMax and yMin correspond to the Top and Bottom respectively in the figure, which respectively represent the upper and lower limits of all glyphs contained in this font on the Y-axis. In addition, we see the LineGap parameter, which corresponds to Leading in the figure, which controls the size of the row spacing.

In addition, we have omitted an important parameter, Units per Em, sometimes referred to as Em, that is used to normalize information about fonts.

For example, in Flutter we set the font fontSize to 10 and the device density to 3. How tall is the font?

With fontEditor we can get the following graph:

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

Again, if we’re using NotoSans on a device with screen density of 3, if we set fontSize of “Medium” to 10, then

  • “Middle” height: 10 * 3 * 0.913 = 27.39 ~= 27
  • Text border height: 10 * 3 * (1160 + 320) / 1000= 44

When 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 can’t they be vertically centered

From the above section, LineGap is 0, that is, Leading is 0, so the vertical layout of Flutter Chinese text 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” character is at (837 + -76) / 2 = 380
  • The center of ascent and Descent is (1160 + -320) / 2 = 420

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

So is there any other way to do it besides using Padding? Or let’s look at Flutter another way. Since Flutter is designed in many ways similar to Android, let’s take a look at how Android is currently implemented.

How to achieve vertical center text in Android native

In addition to Padding, we have two feasible solutions in Android at present:

  • Set the TextViewincludeFontPadding 为 false
  • The custom View calls the paint.gettextbounds () method to get the bounds of String

3.1 includeFontPadding Implements text centering

On Android, TextView uses yMax and yMin as the top and edge of the textbox by default. If you set the includeFontPadding of the TextView to false, Before using Ascent and Descent upper and lower edges.

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 both includePad to true if subject to bottom and top / / includePad to false, will be subject to ascent and descent of the if (includePad) {spacing = metrics.bottom - metrics.top; mDesc = metrics.bottom; } else { spacing = metrics.descent - metrics.ascent; mDesc = metrics.descent; } / /... }Copy the code

To verify this further, we exported the notoSANSCjK-regular of our system and put it into the Android project. Then we set the Android :fontFamily property of our TextView to that font, and something unexpected happened.

After setting the includeFontPadding property of the TextView to false, Where the text matches the system’s default NotoSANscjK-regular font (left) and uses the notosanSCjk-regular font specified via Android :fontFamily (right). In theory, the two fonts should be exactly the same, but the result is different.

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 get 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 the font set to 40DP and dPI to 420 note 2: The font Roboto matches for digital English

From the above three experiments, we can see that TextView adopts Roboto information as its layout information by default, while Chinese finally matches NotoSans font. In this case, the text happens to be centered, so this is not what we pursue. \

3.2 Paint.gettextbounds () centers the text

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setColor(0xFF03DAC5); Rect r = new Rect(); Paint. SetTextSize (dip2px(getContext(), fontSize)); 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, and we need to explain a little bit about the Rect value we get. The screen coordinates are the origin of the upper left corner and the positive direction of the Y axis downward. Font drawing is based on the baseline. The baseline is the origin of its Y-axis relative to the whole Rect, so top above the baseline is negative and bottom below the baseline is positive.

At the heart of the custom View above is the getTextBounds function, which can be used to crack the scheme as long as we can read the information inside. 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

To sum up, the core of the solution is to get Bounds by calling the Minikin Layout interface. The logic of Flutter is very similar to Android, so this solution can be applied to Flutter.

4. Center text in Flutter

4.1 Related principles and modification description

As we can see from Section 3.2, the core of a Flutter is to call the minikin:Layout method to center text in a flutter as Android’s getTextBounds does. We found the following call link in flutter’s existing layout logic:

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 is 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}; SkSpan<const SkGlyph*> glyphs = metrics. Glyphs (SkMakeSpan(glyphIDs, count)); SkScalar scale = strikeSpec.strikeToSourceRatio(); if (bounds) { SkMatrix scaleMat = SkMatrix::Scale(scale, scale); SkRect* cursor = bounds; For (auto glyph: glyphs) {/ / attention glyph - > the rect () inside of values are int scaleMat. MapRectScaleTranslate (cursor++, glyph - > the rect ()); } } if (widths) { SkScalar* cursor = widths; for (auto glyph : glyphs) { *cursor++ = glyph->advanceX() * scale; }}}Copy the code

So there’s no additional layout cost to getTextBounds, we just pass the data stored on the above link

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

The following points of note are encountered during implementation:

  • The Size used for Flutter mapping is exactly the fontSize set by Dart layer. Compared with Android fontSize x density, the accuracy will be lost. Resulting in a deviation of 1 to density – hence the need for corresponding amplification processing
  • In terms of ParagraphTxt::Layout, height is calculated as round(max_accent + max_descent), and there is a loss of precision
  • In ParagraphTxt::Layout, there is also a loss of precision for Y_offset, which is the Y-axis position of the baseline when it is drawn
  • The Paragraph uses _applyFloatingPointHack (value. CeilToDouble (), such as 0.0001 -> 1.0, to obtain the height interface in the Dart layer

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

4.2 Verification

The difference between the internal version and the official PR is that we provide the drawMinHeight parameter, because in order to achieve this part of the function of modification is relatively large, we do not plan to propose PR to the official.

In Text, we add two parameters:

  • DrawMinHeight: Draws the minimum height
  • ForceVerticalCenter: Forces text to be vertically centered in this line, leaving other existing related logic unchanged

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

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

Five, the summary

This paper gives readers a general impression of the vertical layout of the font by interpreting the key information of the font. Taking the character “zhong” 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 programs, analysis of the principle. Finally, based on the getTextBounds scheme of Android, the Function of forceVerticalCenter is implemented on Flutter.

The Bytedance team on Flutter Infra is working to solve these problems. This article mainly addresses the problem of the center alignment of the text of Flutter. Please pay attention to the upcoming series of articles on Flutter governance.

The resources

  • Android font:

    www.jianshu.com/p/35328f7ac…

  • Meaning of Top, ascent, baseline, Descent, bottom, and leading in Android’s FontMetrics:

    Stackoverflow.com/questions/2…

  • FontEditor:

    Github.com/ecomfe/font…

  • Source boldface:

    Baike.baidu.com/item/%E6%80…

  • Typography: \

    Zh.wikipedia.org/wiki/Em_ (% E…

  • The Android source code:

    source.bytedance.net/android/

  • Glyphsapp.com/learn/verti…

About the Byte Terminal technology team

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

Now! Client/front-end/server/side intelligent algorithm/test development for global recruitment! Let’s change the world with technology. If you are interested, please contact [[email protected]] with the subject line resume – name – Job intention – desired city – Phone number.

Bytes to beat application suite MARS is byte to beat terminal technology team in trill, today’s headlines over the past nine years, watermelon video, books, understand car such as emperor App development practice, for mobile research and development, the front-end development, QA, operations, product managers, project managers and operating roles, one-stop research and development of the overall solution, We will help enterprises upgrade their R&D models and reduce their overall r&d costs. Click the link to enter the official website for more product information.