React-native: adjustsFontSizeToFit not working

Created on 29 Aug 2018  路  8Comments  路  Source: facebook/react-native

Environment

  React Native Environment Info:
    System:
      OS: macOS High Sierra 10.13.6
      CPU: x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
      Memory: 184.31 MB / 16.00 GB
      Shell: 5.3 - /bin/zsh
    Binaries:
      Node: 8.11.3 - /usr/local/bin/node
      Yarn: 1.9.4 - /usr/local/bin/yarn
      npm: 5.6.0 - /usr/local/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    SDKs:
      iOS SDK:
        Platforms: iOS 11.4, macOS 10.13, tvOS 11.4, watchOS 4.3
      Android SDK:
        Build Tools: 23.0.1, 26.0.1, 26.0.3, 27.0.3, 28.0.0
        API Levels: 23, 25, 26, 27, 28
    IDEs:
      Android Studio: 3.1 AI-173.4819257
      Xcode: 9.4.1/9F2000 - /usr/bin/xcodebuild
    npmPackages:
      react: 16.4.1 => 16.4.1
      react-native: 0.56.0 => 0.56.0
    npmGlobalPackages:
      react-native-cli: 2.0.1

Description

Since 0.54.x, adjustsFontSizeToFit option in Text has not been working. It worked as expected on 0.53.x

Reproducible Demo

import React, {Component} from 'react';
import {StyleSheet, Text, View} from 'react-native';

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text} allowFontScaling>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: 30,
    color: '#333333',
    marginBottom: 5,
  },
});
Bug Stale

Most helpful comment

@pabloga265 I took your code and took out the constants. TLDR: you're code breaks for some font-families, mine should fix that. See below.

Basically, I took figured if we have to render the text twice, we might as well get some useful information, so I'm rendering the first pass at fontSize 5 (if we start out too small it gets less accurate because the letters are so small). I did some testing and found the following:
monospace width = length * fontSize * 3 / 5
where length is the number of characters in the text. We can also get the width at fontSize 5:
monospace width at fontSize 5 = length * 5 * 3 / 5 = length * 3
Now, we can calculate how wide our actual text would be in myFontFamily after render by applying the ratio between these two monospace widths to the reported width at fontSize 5 from onLayout:

myFontFamily width = myFontFamily width at 5 (reported width ) * ( monospace width / monospace width at 5)
myFontFamily width = reported width * length * fontSize * 3 / (length * 3 * 5)
myFontFamily width = reported width * fontSize / 5

We can use also use this equation to calculate an appropriate fontSize to get the desired width of the text element.

I was also running into some problems with multiple adjustments so I created a switch that would only allow it to be adjusted one time.

I also made sure to use the smaller of the two fontSize values, as your code adjusts no matter what, while the adjustsFontSizeToFit property only shrinks text that is clipping.

export default class ResponsiveText extends React.Component {
  /*
    This is an extension of text that handles implements the adjustsFontSizeToFit for android. The bug that it's
    patching is documented here:

    https://github.com/facebook/react-native/issues/20906

   */
  state = {
    _style: {
      fontSize: 5,
      width: null,
    },
    _adjusted: false
  }

  onLayout = (event) => {
    const { adjustsFontSizeToFit, style, children } = this.props
    if (adjustsFontSizeToFit && style.width && typeof children === 'string') {
      if (!this.state._adjusted) {
        const {width} = event.nativeEvent.layout
        this.setState({
          _style: {
            fontSize: Math.min(style.fontSize, Math.floor(5 * style.width / width)),
            width: style.width,
          },
          _adjusted: true,
        })
      }
    }
    else {
      this.setState({
        _style: {
          fontSize: this.props.style.fontSize,
          width: this.props.style.width,
        },
        _adjusted: true,
      })
    }
  }

  render() {
    return (
      Platform.OS === 'ios'
        ? <Text {...this.props}/>
        : <Text onLayout={this.onLayout} {...this.props}
                style={[this.props.style, this.state._style]}
          />
    )
  }
}

All 8 comments

This is still happening in 0.56 and 0.57, adjustsFontSizeToFit works fine on iOS but does nothing on Android

Yep, still happening on 0.57. Is there any workaround?

Extremely hacky and extremely no-no. Full combat solution until I find something better

I created a text component that for Ios basically returns Text and in android returns a Text with this onLayout thing on top of it.

Please I can't encourage anyone enough to not use this in their projects, and find a real solution but if you are in a hurry this patched my problem

<Text
  onLayout={(event) => {
    if (!this.props.adjustsFontSizeToFit || typeof this.props.children !== 'string') return
    let {width} = event.nativeEvent.layout
    this.setState({fontSize: Math.floor(1.7 * (width / this.props.children.length))})
  }}
  {...this.props}
  style={[this.props.style, this.state.fontSize && {fontSize: this.state.fontSize}]}
>
  {this.props.children}
</Text>)

This is not a fix of course 1.7 is a random number that seems to fit my project properly in all resolutions I have tested so far, onLayout fires after render so there is a bit of a visual glitch etc... etc...

I will try to find a decent fix if it doesn't get fixed eventually but for now, I'm going with this shameful hack.

I can confirm that this is still happening.

@pabloga265 I _think_ the above might work better if you can use Dimensions to determine screen width and calculate the correct fontSize before the first render.

@pabloga265 I took your code and took out the constants. TLDR: you're code breaks for some font-families, mine should fix that. See below.

Basically, I took figured if we have to render the text twice, we might as well get some useful information, so I'm rendering the first pass at fontSize 5 (if we start out too small it gets less accurate because the letters are so small). I did some testing and found the following:
monospace width = length * fontSize * 3 / 5
where length is the number of characters in the text. We can also get the width at fontSize 5:
monospace width at fontSize 5 = length * 5 * 3 / 5 = length * 3
Now, we can calculate how wide our actual text would be in myFontFamily after render by applying the ratio between these two monospace widths to the reported width at fontSize 5 from onLayout:

myFontFamily width = myFontFamily width at 5 (reported width ) * ( monospace width / monospace width at 5)
myFontFamily width = reported width * length * fontSize * 3 / (length * 3 * 5)
myFontFamily width = reported width * fontSize / 5

We can use also use this equation to calculate an appropriate fontSize to get the desired width of the text element.

I was also running into some problems with multiple adjustments so I created a switch that would only allow it to be adjusted one time.

I also made sure to use the smaller of the two fontSize values, as your code adjusts no matter what, while the adjustsFontSizeToFit property only shrinks text that is clipping.

export default class ResponsiveText extends React.Component {
  /*
    This is an extension of text that handles implements the adjustsFontSizeToFit for android. The bug that it's
    patching is documented here:

    https://github.com/facebook/react-native/issues/20906

   */
  state = {
    _style: {
      fontSize: 5,
      width: null,
    },
    _adjusted: false
  }

  onLayout = (event) => {
    const { adjustsFontSizeToFit, style, children } = this.props
    if (adjustsFontSizeToFit && style.width && typeof children === 'string') {
      if (!this.state._adjusted) {
        const {width} = event.nativeEvent.layout
        this.setState({
          _style: {
            fontSize: Math.min(style.fontSize, Math.floor(5 * style.width / width)),
            width: style.width,
          },
          _adjusted: true,
        })
      }
    }
    else {
      this.setState({
        _style: {
          fontSize: this.props.style.fontSize,
          width: this.props.style.width,
        },
        _adjusted: true,
      })
    }
  }

  render() {
    return (
      Platform.OS === 'ios'
        ? <Text {...this.props}/>
        : <Text onLayout={this.onLayout} {...this.props}
                style={[this.props.style, this.state._style]}
          />
    )
  }
}

If you want to use a value of "numberOfLines" >= 1 you have to modify a little this component, because how it's written it will always calculate a fontSize that will fit in just 1 line. I needed a numberOfLines = 2.

changing

const { adjustsFontSizeToFit, style, children } = this.props

in

const { adjustsFontSizeToFit, style, children,numberOfLines } = this.props

and

fontSize: Math.min(style.fontSize, Math.floor(5 * style.width / width))

in

fontSize: Math.min(style.fontSize, Math.floor(5 * style.width / width * numberOfLines))

did the trick for me.

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.

Was this page helpful?
0 / 5 - 0 ratings