Skip to content

fix(Android) drawing additional empty line when 'textAlign' is set to 'justify'#47122

Closed
coado wants to merge 8 commits into
facebook:mainfrom
coado:multiline-issue
Closed

fix(Android) drawing additional empty line when 'textAlign' is set to 'justify'#47122
coado wants to merge 8 commits into
facebook:mainfrom
coado:multiline-issue

Conversation

@coado
Copy link
Copy Markdown
Contributor

@coado coado commented Oct 18, 2024

Summary:

Fixes #46908

The justificationMode is not set for multiline text without unicode characters with known width on both architectures. This caused the issue of drawing additional empty line at the end of TextView because Yoga thought that text takes 5 lines and falsely calculated it's height.

Currently, on the old architecture, the justificationMode is set only on text that is not boring (contains unicode characters) with unknown width. I am not sure why is that, so I am opening this as a draft for now as I am still checking if it doesn't break anything.

Changelog:

[ANDROID] [FIXED] - fix generating empty line at the end of multiline text view when textAlign is set to justify

Test Plan:

I've tested on both architectures on repro provided in the issue.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 18, 2024
@react-native-bot
Copy link
Copy Markdown
Collaborator

react-native-bot commented Oct 18, 2024

Fails
🚫

Danger failed to run dangerfile.js.

Error ReferenceError

validateChangelog is not defined
ReferenceError: validateChangelog is not defined
    at Object.<anonymous> (dangerfile.js:48:18)
    at Module._compile (node:internal/modules/cjs/loader:1364:14)
    at requireFromString (/home/runner/work/react-native/react-native/node_modules/require-from-string/index.js:28:4)
    at /home/runner/work/react-native/react-native/node_modules/danger/distribution/runner/runners/inline.js:161:68
    at step (/home/runner/work/react-native/react-native/node_modules/danger/distribution/runner/runners/inline.js:52:23)
    at Object.next (/home/runner/work/react-native/react-native/node_modules/danger/distribution/runner/runners/inline.js:33:53)
    at /home/runner/work/react-native/react-native/node_modules/danger/distribution/runner/runners/inline.js:27:71
    at new Promise (<anonymous>)
    at __awaiter (/home/runner/work/react-native/react-native/node_modules/danger/distribution/runner/runners/inline.js:23:12)
    at runDangerfileEnvironment (/home/runner/work/react-native/react-native/node_modules/danger/distribution/runner/runners/inline.js:118:132)
    at /home/runner/work/react-native/react-native/node_modules/danger/distribution/platforms/GitHub.js:181:38
    at step (/home/runner/work/react-native/react-native/node_modules/danger/distribution/platforms/GitHub.js:44:23)
    at Object.next (/home/runner/work/react-native/react-native/node_modules/danger/distribution/platforms/GitHub.js:25:53)
    at /home/runner/work/react-native/react-native/node_modules/danger/distribution/platforms/GitHub.js:19:71
    at new Promise (<anonymous>)
    at __awaiter (/home/runner/work/react-native/react-native/node_modules/danger/distribution/platforms/GitHub.js:15:12)
    at Object.executeRuntimeEnvironment (/home/runner/work/react-native/react-native/node_modules/danger/distribution/platforms/GitHub.js:144:88)
    at /home/runner/work/react-native/react-native/node_modules/danger/distribution/commands/danger-runner.js:101:47
    at step (/home/runner/work/react-native/react-native/node_modules/danger/distribution/commands/danger-runner.js:34:23)
    at Object.next (/home/runner/work/react-native/react-native/node_modules/danger/distribution/commands/danger-runner.js:15:53)
    at fulfilled (/home/runner/work/react-native/react-native/node_modules/danger/distribution/commands/danger-runner.js:6:58)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

Dangerfile

43|     'This will require a manual import by a Facebook employee.';
44|   warn(`${title} - <i>${idea}</i>`);
45| }
46| 
47| // Provides advice if a test plan is missing.
--------------------^
48| const includesTestPlan =
49|   danger.github.pr.body &&
50|   danger.github.pr.body.toLowerCase().includes('## test plan');
51| if (!includesTestPlan && !isFromPhabricator) {

Generated by 🚫 dangerJS against 232ed3f

@coado
Copy link
Copy Markdown
Contributor Author

coado commented Oct 22, 2024

These three test cases enter all specified layout builders in createLayout (new arch) and measureSpannedText (old arch) and it seems to work

code
import {
  SafeAreaView,
  StyleSheet,
  Text,
} from 'react-native';

function App(): React.JSX.Element {
  return (
    <SafeAreaView style={{ backgroundColor: 'white', flex: 1, justifyContent: 'center'}}>

      {/* boring layout */}
      <Text style={[styles.textJustify, styles.containerWidth]}>
        asdasjand akjsnd snd jsdnjkdn js s
      </Text>

      {/* multiline, boring layout */}
      <Text style={[styles.textJustify, styles.containerWidth]}>
        asdasd dajdoij sdnk aosjn dojna s dnjasjdkn dnsjdnjnkdaajs sjdnjn sdjn
        jdns ssjkndjkansd s s s s s s s njnd sjkdnajdn sndjand sdjnaojdnsjdnsnd
        jsdn sdn a s kk sdnjsjdn jsnd sjdn dddd ajksnd jad nd a d sakjnd skjnd
        sjdnaj ksjand akjsnd snd jsdnjkdn js s
      </Text>

      {/* not boring, single line */}
      <Text style={[styles.textJustify]}>
      تخطيط الاختبارتخطيط الاختبارتخطيط الاختبرا تخطيط تخطيط تخطيط تخطيط ا ا
      </Text>

    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  containerWidth: {
    width: 400,
  },
  textJustify: {
    backgroundColor: 'red',
    textAlign: 'justify',
    margin: 16,
  },
});

export default App;

Comment on lines +149 to +171
private static int getTextJustificationMode(MapBuffer attributedString) {
int justificationMode = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
if (!attributedString.contains(AS_KEY_FRAGMENTS)) {
return justificationMode;
}

MapBuffer fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS);
if (fragments.getCount() != 0) {
MapBuffer fragment = fragments.getMapBuffer(0);
MapBuffer textAttributes = fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES);

if (textAttributes.contains(TextAttributeProps.TA_KEY_ALIGNMENT)) {
String alignmentAttr = textAttributes.getString(TextAttributeProps.TA_KEY_ALIGNMENT);

if (alignmentAttr.equals("justified")) {
justificationMode = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 1 : Layout.JUSTIFICATION_MODE_INTER_WORD;
}
}
}

return justificationMode;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could merge retrieving alignmentAttr into one function as similar code is used in getTextAlignment

@coado coado marked this pull request as ready for review October 22, 2024 10:47
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 22, 2024
@Nullable
private static String getTextAlignmentAttr(MapBuffer attributedString) {
// TODO: Don't read AS_KEY_FRAGMENTS, which may be expensive, and is not present when using
// cached Spannable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: looks like we chopped off this part of the comment

int justificationMode = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;

if (alignmentAttr != null && alignmentAttr.equals("justified")) {
justificationMode = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 1 : Layout.JUSTIFICATION_MODE_INTER_WORD;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the significance of 1 here? We will never use this value on the old versions, right? Maybe we should just early return something invalid like -1;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I somehow missed that it won't be even used 😅



private static int getTextJustificationMode(@Nullable String alignmentAttr) {
int justificationMode = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cortinico more code that will go away once we bump to minSdk 26

MapBuffer attributedString,
Spannable spanned,
@Nullable String alignmentAttr) {
// cached Spannable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where that other part of the comment ended up 😅


private static Layout.Alignment getTextAlignment(MapBuffer attributedString, Spannable spanned) {
@Nullable
private static String getTextAlignmentAttr(MapBuffer attributedString) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is splitting the alignment methods related to core change, or just for consistency?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just for consistency and to not retrieve alignment twice from MapBuffer (first from getTextAlignment and second from getTextJustificationMode)

// always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on
// how exacly lines are aligned, just their width
// always passing ALIGN_NORMAL and JUSTIFICATION_MODE_NONE here should be fine, since this method doesn't depend on
// how exactly lines are aligned, just their width
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct, since justification can change line width.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we could get current justification mode or pass -1 as well I think.

Copy link
Copy Markdown
Contributor

@NickGerleman NickGerleman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@facebook-github-bot
Copy link
Copy Markdown
Contributor

@NickGerleman has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Oct 29, 2024
@facebook-github-bot
Copy link
Copy Markdown
Contributor

@NickGerleman merged this pull request in 08e8f6a.

@react-native-bot
Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @coado in 08e8f6a

When will my fix make it into a release? | How to file a pick request?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] React native 0.75.4 text number of lines not updating

4 participants