Skip to content

Commit

Permalink
Fix layout width calculation in onTextLayout (#36222)
Browse files Browse the repository at this point in the history
Summary:
There is a rounding issue in layout calculation when using onTextLayout method in Text component on Android.
As you can see in the example below onTextLayout returns 3 lines, but in fact text is rendered in 2 lines:

<img width="775" alt="Screenshot 2023-02-19 at 23 48 53" src="https://user-images.githubusercontent.com/8476339/220177419-de183ccd-a250-4131-ad05-907fdb791c75.png">

This happens because `(int) width` casting works like `Math.floor`, but after changing it to `Math.round` we get correct result:

<img width="775" alt="Screenshot 2023-02-19 at 23 51 11" src="https://user-images.githubusercontent.com/8476339/220177859-93474c43-ed87-4c1b-986c-2817b29b78be.png">

## Changelog

[ANDROID] [FIXED] - Fix layout width calculation in onTextLayout

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->

Pull Request resolved: #36222

Test Plan:
This issue can be tricky to reproduce as width calculation depends on device width. I'm attaching code that I used to reproduce it. You need to tap on the screen to run through different sentences and sooner or later you will get the one with this rounding issue.

<details>
  <summary>Code to reproduce</summary>

```js
import React, { useState, useEffect } from 'react'
import { SafeAreaView, Text, View } from 'react-native'

function App() {
  const [state, setState] = useState({
    index: 0,
    lines: [],
    sentences: [],
  })

  const onTextLayout = (event) => {
    const lines = event.nativeEvent.lines
    console.log(JSON.stringify(lines, null, 2))

    setState(state => ({ ...state, lines }))
  }

  useEffect(() => {
    fetch('https://content.duoreading.io/20-the-adventures-of-tom-sawyer/translations/english.json')
      .then(response => response.text())
      .then(response => {
        setState(state => ({ ...state, sentences: JSON.parse(response) }))
      })
  }, [])

  return (
    <SafeAreaView style={{ flex: 1, padding: 30 }}>
      <View style={{ flex: 1 }} onTouchStart={() => setState(state => ({ ...state, index: state.index + 1 }))}>
        <Text style={{ fontSize: 22 }} onTextLayout={onTextLayout}>
          {state.sentences[state.index]}
        </Text>

        {state.lines.map((line, index) => (
          <View
            key={index}
            style={{
              position: 'absolute',
              top: line.y,
              left: line.x,
              width: line.width,
              height: line.height,
              opacity: 0.3,
              backgroundColor: ['red', 'yellow', 'blue'][index % 3],
            }}>
          </View>
        ))}
      </View>
    </SafeAreaView>
  )
}

export default App
```
</details>

Reviewed By: christophpurrer

Differential Revision: D43907184

Pulled By: javache

fbshipit-source-id: faef757e77e759b5d9ea26da21c9e2b396dc9ff1
  • Loading branch information
reepush authored and facebook-github-bot committed Mar 14, 2023
1 parent 19cf5c9 commit ccbbcaa
Showing 1 changed file with 7 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ private Layout measureSpannedText(Spannable text, float width, YogaMeasureMode w
new StaticLayout(
text, textPaint, (int) width, alignment, 1.f, 0.f, mIncludeFontPadding);
} else {
// Android 11+ introduces changes in text width calculation which leads to cases
// where the container is measured smaller than text. Math.ceil prevents it
// See T136756103 for investigation
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.Q) {
width = (float) Math.ceil(width);
}

StaticLayout.Builder builder =
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
.setAlignment(alignment)
Expand Down

0 comments on commit ccbbcaa

Please sign in to comment.