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

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

coado
Copy link
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
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
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
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
// TODO: Don't read AS_KEY_FRAGMENTS, which may be expensive, and is not present when using
// cached Spannable
Copy link
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
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
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
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
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 😅

@@ -146,13 +146,42 @@ public static boolean isRTL(MapBuffer attributedString) {
== LayoutDirection.RTL;
}

private static Layout.Alignment getTextAlignment(MapBuffer attributedString, Spannable spanned) {
@Nullable
private static String getTextAlignmentAttr(MapBuffer attributedString) {
Copy link
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
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
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
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
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
Contributor

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

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. 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