Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android Text elements that have adjustsFontSizeToFit do not have their font size adjusted when the height of a parent element is changed. #30717

Closed
kdo1234 opened this issue Jan 10, 2021 · 6 comments
Labels
Bug Component: Text Impact: Regression Describes a behavior that used to work on a prior release, but stopped working recently. Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Platform: Android Android applications. Resolution: Locked This issue was locked by the bot.

Comments

@kdo1234
Copy link

kdo1234 commented Jan 10, 2021

Description

On Android, Text elements that have adjustsFontSizeToFit={true} do not have their font size adjusted when the height of a parent element is changed. This works as desired on iOS. This seems to be a defect in the excellent PR #26389 by @janicduplessis.

React Native version:

info Fetching system and libraries information...
System:
    OS: Windows 10 10.0.18362
    CPU: (12) x64 Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
    Memory: 4.60 GB / 15.81 GB
  Binaries:
    Node: 10.17.0 - C:\Program Files\nodejs\node.EXE
    Yarn: Not Found
    npm: 6.11.3 - C:\Program Files\nodejs\npm.CMD
    Watchman: Not Found
  SDKs:
    Android SDK:
      API Levels: 27, 28, 29
      Build Tools: 28.0.3, 29.0.2
      System Images: android-27 | Google Play Intel x86 Atom, android-28 | Wear OS Intel x86 Atom, android-28 | Intel x86 Atom_64, android-28 | Google APIs Intel x86 Atom, android-28 | Google Play Intel x86 Atom, android-29 | Google APIs Intel x86 Atom, android-30 | Google APIs Intel x86 Atom
      Android NDK: Not Found
    Windows SDK: Not Found
  IDEs:
    Android Studio: Version  3.5.0.0 AI-191.8026.42.35.5791312
    Visual Studio: Not Found
  Languages:
    Java: 1.8.0_221
    Python: 2.7.16
  npmPackages:
    @react-native-community/cli: Not Found
    react: 16.13.1 => 16.13.1
    react-native: 0.63.4 => 0.63.4
    react-native-windows: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

Steps To Reproduce

  1. Run the following app:
import * as React from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
export default class App extends React.Component {
  state = {
    rowHeight: 60,
  };

  render() {
    return (
      <View style={styles.container}>
        <View style={[styles.row, {height:this.state.rowHeight}]}>
          <Text adjustsFontSizeToFit numberOfLines={1} style={styles.paragraph}>
            Text text text text text text
          </Text>
        </View>
        <Text>{this.state.version}</Text>
        <View style={styles.button}>
         <Button onPress={() => this.setState({rowHeight: 10})} title="Set row height to 10"/>
        </View>
        <View style={styles.button}>
         <Button onPress={() => this.setState({rowHeight: 60})} title="Set row height to 60"/>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    paddingTop: 100,

  },
  row: {
    backgroundColor: 'lightblue',
    marginBottom: 20,
  },
  button: {
    margin: 10
  },
  paragraph: {
    fontSize: 100,
    fontWeight: 'bold',
    textAlign: 'center',
    backgroundColor: "yellow"
  },
});
  1. Press the button labeled "Set row height to 10". Observe that on iOS the text's font size is reduced but is unchanged on Android.

Expected Results

The font size of the Text element should be changed when the height of a parent element is changed on Android, consistent with iOS.

Snack, code example, screenshot, or link to a repository:

https://snack.expo.io/TAJyBmNdv

@ydalmia
Copy link

ydalmia commented Jan 10, 2021

Would this be a good first issue? I a newcomer and would love to try working on it

@chrisglein chrisglein added Component: Text Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Bug Impact: Regression Describes a behavior that used to work on a prior release, but stopped working recently. and removed Needs: Triage 🔍 labels Jan 12, 2021
@chrisglein
Copy link

Thanks for the Snack! Confirmed the repro on my end. Downsizes on iOS but not on Android. And does nothing on Web, I assume it's just not implemented there.

The PR to implement this on Android is from September 2019, so more than a year ago. I assume it worked then so this is likely a regression?

@ydalmia I don't know if this is a good first issue but it does look like a legit bug and I'm sure it would be helpful for you to take a look.

@fabOnReact

This comment has been minimized.

@janicduplessis
Copy link
Contributor

janicduplessis commented Feb 22, 2021

I don't think I tested changing the layout dynamically in the original PR. If someone can figure out a solution feel free to open a PR and I can have a look.

@fabOnReact
Copy link
Contributor

fabOnReact commented Feb 24, 2021

Thanks Janic Duplessis, I summarize my findings here, I'll try my best to fix this issue.

how is the text size decreased to fit height of the view?

  1. the layout variable is first calculated/instanciated
    Layout layout = measureSpannedText(text, width, widthMode);
  2. the while loop runs iteration based on conditions layout.getHeight() > height and layout.getLineCount() > mNumberOfLines
    && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines
    || heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) {
  3. each loop iteration decreases currentFontSize by 1dip
    currentFontSize = currentFontSize - (int) PixelUtil.toPixelFromDIP(1);

    then re-calculates the layout using method measureSpannedText
    layout = measureSpannedText(text, width, widthMode);
  4. measureSpannedText computes the layout based on the new text with updated fontSize
    4a. verifies if the BoringLayout can be used for this text, in this case the BoringLayout is used.
    BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);

    4b. isBoring returns BoringLayout.Metrics (sourcecode at the bottom) which is used to generate the layout (boring.width) in the step below
    4c. the layout is built using BoringLayout.make
    layout =
    BoringLayout.make(
    text, textPaint, boring.width, alignment, 1.f, 0.f, boring, mIncludeFontPadding);
  5. we return to step 2 and check condition layout.getHeight() > height and layout.getLineCount() > mNumberOfLines
POINT 4b. BoringLayout isBoring()

https://github.com/aosp-mirror/platform_frameworks_base/blob/33b19a2507bbc6fd1ff41224139a39dea0876728/core/java/android/text/BoringLayout.java#L330-L370

    /**
     * Returns null if not boring; the width, ascent, and descent in the
     * provided Metrics object (or a new one if the provided one was null)
     * if boring.
     * @hide
     */
    @UnsupportedAppUsage
    public static Metrics isBoring(CharSequence text, TextPaint paint,
            TextDirectionHeuristic textDir, Metrics metrics) {
        final int textLength = text.length();
        if (hasAnyInterestingChars(text, textLength)) {
           return null;  // There are some interesting characters. Not boring.
        }
        if (textDir != null && textDir.isRtl(text, 0, textLength)) {
           return null;  // The heuristic considers the whole text RTL. Not boring.
        }
        if (text instanceof Spanned) {
            Spanned sp = (Spanned) text;
            Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
            if (styles.length > 0) {
                return null;  // There are some ParagraphStyle spans. Not boring.
            }
        }


        Metrics fm = metrics;
        if (fm == null) {
            fm = new Metrics();
        } else {
            fm.reset();
        }


        TextLine line = TextLine.obtain();
        line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
                Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,
                0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);
        fm.width = (int) Math.ceil(line.metrics(fm));
        TextLine.recycle(line);


        return fm;
    }

The first time we render the text boring.width reaches value of 81 and layout.getHeight > height returns false stopping the iteration

02-24 13:18:47.277  6134  6186 W unknown:TESTING::: layout.getHeight() > height:  false
02-24 13:18:47.277  6134  6186 W unknown:TESTING::: boring.width: 223
02-24 13:18:47.278  6134  6186 W unknown:TESTING::: boring.width: 81

The second time we render the text after updating the height, boring.width reaches minimum value of 443 and layout.getHeight > height stops the iteration.

@fabOnReact
Copy link
Contributor

fabOnReact commented Feb 24, 2021

how is layout.getHeight() calculated and why does it stop the iteration?

layout.getHeight() is the difference between

spacing = metrics.bottom - metrics.top;

First Time we render

boring: FontMetricsInt: top=-87 ascent=-76 descent=20 bottom=23 leading=0 width=971
layout.getHeight(): 110 

Second Time we render

boring: FontMetricsInt: top=-40 ascent=-34 descent=9 bottom=11 leading=0 width=443
layout.getHeight(): 51

the value getHeight() is responsible for stopping the iteration which reduces the font weight

&& (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines
|| heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) {

how do we compute the new size of the text?

we add to the TextView a Span with the new font size and then measure the TextView

ReactAbsoluteSizeSpan[] sizeSpans =
text.getSpans(0, text.length(), ReactAbsoluteSizeSpan.class);
for (ReactAbsoluteSizeSpan span : sizeSpans) {
text.setSpan(
new ReactAbsoluteSizeSpan(
(int) Math.max((span.getSize() * ratio), minimumFontSize)),
text.getSpanStart(span),
text.getSpanEnd(span),
text.getSpanFlags(span));
text.removeSpan(span);
}
layout = measureSpannedText(text, width, widthMode);

how does `BoringLayout` compute the height/width of the text?

The computation is done in init

    /* package */ void init(CharSequence source, TextPaint paint, Alignment align,
            BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {
        int spacing;


        if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
            mDirect = source.toString();
        } else {
            mDirect = null;
        }


        mPaint = paint;


        if (includePad) {
            spacing = metrics.bottom - metrics.top;
            mDesc = metrics.bottom;
        } else {
            spacing = metrics.descent - metrics.ascent;
            mDesc = metrics.descent;
        }


        mBottom = spacing;


        if (trustWidth) {
            mMax = metrics.width;
        } else {
            /*
             * If we have ellipsized, we have to actually calculate the
             * width because the width that was passed in was for the
             * full text, not the ellipsized form.
             */
            TextLine line = TextLine.obtain();
            line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                    mEllipsizedStart, mEllipsizedStart + mEllipsizedCount);
            mMax = (int) Math.ceil(line.metrics(null));
            TextLine.recycle(line);
        }


        if (includePad) {
            mTopPadding = metrics.top - metrics.ascent;
            mBottomPadding = metrics.bottom - metrics.descent;
        }
    }

shergin pushed a commit to shergin/react-native that referenced this issue May 17, 2021
This PR fixes facebook#30717, a bug in `<Text adjustsFontSizeToFit={true}>` implementation that prevents it from adjusting text size dynamically on Android.

The way `adjustsFontSizeToFit` was implemented in facebook#26389 (and the design of ReactTextShadowNode) implies that Yoga will call `onMeasure` on every size change of a `<Text>` component, which is actually not the case (Yoga can cache the results of the measures, call the function multiple times or do not call at all inferring the size from the size constraints). The implementation of `adjustsFontSizeToFit` computes the adjusted string inside the measure function and then eventually passes that to the view layer where it's being rendered.

The proper fix of this issue requires the full redesign of the measure and rendering pipelines and separating them, and that... would be too invasive. And, I believe, this issue is already fixed in Fabric where this part is already designed this way.

Instead, this diff implements a small workaround: if `adjustsFontSizeToFit` is enabled, we manually dirty the Yoga node and mark the shadow node updated to force remeasuring.
facebook-github-bot pushed a commit that referenced this issue Feb 17, 2022
…tely (#33135)

Summary:
Fixes the infinite loop explained in the issue #33129 by reverting commit  5902152. PR #31538.

`onCollectExtraUpdates` is part of the node update cycle. By marking the node as updated `markUpdated()` in `onCollectExtraUpdates` we are restarting the update infinitely.

Unfortunately, reverting this PR also reintroduces the original issue #30717 which IMO is minor compared to the infinite loop.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[Android] [Fixed] - Text with adjustsFontSizeToFit changes the text layout infinitely

Pull Request resolved: #33135

Test Plan:
I added a console.log to the Text `onTextLayout` in [packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js ](https://github.com/facebook/react-native/blob/main/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js) to see if the infinite loop is gone.

![image](https://user-images.githubusercontent.com/3791120/154523914-e6aa7cf5-7a1c-488f-a392-898f4c85a833.png)

![Screen Shot 2022-02-17 at 11 20 31 AM](https://user-images.githubusercontent.com/3791120/154524274-880c3bed-d2c6-456b-8947-42e75793c424.jpg)

```

Reviewed By: ShikaSD

Differential Revision: D34310218

Pulled By: lunaleaps

fbshipit-source-id: 0d40f49d15c562ec25983145897bd95dc182f897
ShikaSD pushed a commit that referenced this issue Feb 22, 2022
…tely (#33135)

Summary:
Fixes the infinite loop explained in the issue #33129 by reverting commit  5902152. PR #31538.

`onCollectExtraUpdates` is part of the node update cycle. By marking the node as updated `markUpdated()` in `onCollectExtraUpdates` we are restarting the update infinitely.

Unfortunately, reverting this PR also reintroduces the original issue #30717 which IMO is minor compared to the infinite loop.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[Android] [Fixed] - Text with adjustsFontSizeToFit changes the text layout infinitely

Pull Request resolved: #33135

Test Plan:
I added a console.log to the Text `onTextLayout` in [packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js ](https://github.com/facebook/react-native/blob/main/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js) to see if the infinite loop is gone.

![image](https://user-images.githubusercontent.com/3791120/154523914-e6aa7cf5-7a1c-488f-a392-898f4c85a833.png)

![Screen Shot 2022-02-17 at 11 20 31 AM](https://user-images.githubusercontent.com/3791120/154524274-880c3bed-d2c6-456b-8947-42e75793c424.jpg)

```

Reviewed By: ShikaSD

Differential Revision: D34310218

Pulled By: lunaleaps

fbshipit-source-id: 0d40f49d15c562ec25983145897bd95dc182f897
ShikaSD pushed a commit that referenced this issue Feb 24, 2022
…tely (#33135)

Summary:
Fixes the infinite loop explained in the issue #33129 by reverting commit  5902152. PR #31538.

`onCollectExtraUpdates` is part of the node update cycle. By marking the node as updated `markUpdated()` in `onCollectExtraUpdates` we are restarting the update infinitely.

Unfortunately, reverting this PR also reintroduces the original issue #30717 which IMO is minor compared to the infinite loop.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[Android] [Fixed] - Text with adjustsFontSizeToFit changes the text layout infinitely

Pull Request resolved: #33135

Test Plan:
I added a console.log to the Text `onTextLayout` in [packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js ](https://github.com/facebook/react-native/blob/main/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js) to see if the infinite loop is gone.

![image](https://user-images.githubusercontent.com/3791120/154523914-e6aa7cf5-7a1c-488f-a392-898f4c85a833.png)

![Screen Shot 2022-02-17 at 11 20 31 AM](https://user-images.githubusercontent.com/3791120/154524274-880c3bed-d2c6-456b-8947-42e75793c424.jpg)

```

Reviewed By: ShikaSD

Differential Revision: D34310218

Pulled By: lunaleaps

fbshipit-source-id: 0d40f49d15c562ec25983145897bd95dc182f897
@facebook facebook locked as resolved and limited conversation to collaborators Aug 25, 2022
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Aug 25, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug Component: Text Impact: Regression Describes a behavior that used to work on a prior release, but stopped working recently. Issue: Author Provided Repro This issue can be reproduced in Snack or an attached project. Platform: Android Android applications. Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants