Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

code-review PR 41770 - single line text not fully rendered #30

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 30 additions & 22 deletions android/src/main/java/com/text/ReactTextViewImprovedShadowNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
*/
package com.text;

import static android.text.Layout.BREAK_STRATEGY_HIGH_QUALITY;
import static android.text.Layout.BREAK_STRATEGY_SIMPLE;
import static android.text.Layout.HYPHENATION_FREQUENCY_NONE;

import android.graphics.text.LineBreaker;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.Gravity;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
Expand Down Expand Up @@ -42,6 +50,9 @@
import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.yoga.YogaNode;
import java.util.ArrayList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReactTextViewImprovedShadowNode extends ReactTextShadowNode {
// It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it
Expand Down Expand Up @@ -128,13 +139,17 @@ public long measure(
if (widthMode == YogaMeasureMode.EXACTLY) {
layoutWidth = width;
} else {
for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) {
boolean endsWithNewLine =
text.length() > 0 && text.charAt(layout.getLineEnd(lineIndex) - 1) == '\n';
float lineWidth =
endsWithNewLine ? layout.getLineMax(lineIndex) : layout.getLineWidth(lineIndex);
if (lineWidth > layoutWidth) {
layoutWidth = lineWidth;
if (lineCount == 1) {
layoutWidth = (int) layout.getEllipsizedWidth();
} else {
for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) {
boolean endsWithNewLine =
text.length() > 0 && text.charAt(layout.getLineEnd(lineIndex) - 1) == '\n';
float lineWidth =
endsWithNewLine ? layout.getLineMax(lineIndex) : layout.getLineWidth(lineIndex);
if (lineWidth > layoutWidth) {
layoutWidth = lineWidth;
}
}
}
if (widthMode == YogaMeasureMode.AT_MOST && layoutWidth > width) {
Expand Down Expand Up @@ -195,11 +210,6 @@ private Layout measureSpannedText(Spannable text, float width, YogaMeasureMode w
Layout layout;
BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN;
// StaticLayout#getLineWidth does not work with single-line text.
boolean overrideTextBreakStrategySingleLine =
boring == null
? false
: mNumberOfLines == 1 && !mAdjustsFontSizeToFit && boring.width > width;

// technically, width should never be negative, but there is currently a bug in
boolean unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0;
Expand Down Expand Up @@ -240,7 +250,7 @@ private Layout measureSpannedText(Spannable text, float width, YogaMeasureMode w
layout = builder.build();

} else if (boring != null
&& (unconstrainedWidth || boring.width <= width || overrideTextBreakStrategySingleLine)) {
&& (unconstrainedWidth || boring.width <= width)) {
// Is used for single-line, boring text when the width is either unknown or bigger
// than the width of the text.
layout =
Expand All @@ -261,15 +271,13 @@ private Layout measureSpannedText(Spannable text, float width, YogaMeasureMode w
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
width = (float) Math.ceil(width);
}

StaticLayout.Builder builder =
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
.setAlignment(alignment)
.setLineSpacing(0.f, 1.f)
.setIncludePad(mIncludeFontPadding)
.setBreakStrategy(mTextBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);

StaticLayout.Builder builder =
StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, (int) width)
.setAlignment(alignment)
.setLineSpacing(0.f, 1.f)
.setIncludePad(mIncludeFontPadding)
.setBreakStrategy(mTextBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(true);
}
Expand Down
210 changes: 24 additions & 186 deletions example/src/RNTesterAppShared.js
Original file line number Diff line number Diff line change
@@ -1,200 +1,38 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

import {RNTesterEmptyBookmarksState} from './components/RNTesterEmptyBookmarksState';
import RNTesterModuleContainer from './components/RNTesterModuleContainer';
import RNTesterModuleList from './components/RNTesterModuleList';
import RNTesterNavBar, {navBarHeight} from './components/RNTesterNavbar';
import {RNTesterThemeContext, themes} from './components/RNTesterTheme';
import RNTTitleBar from './components/RNTTitleBar';
import RNTesterList from './utils/RNTesterList';
import {
RNTesterNavigationActionsType,
RNTesterNavigationReducer,
} from './utils/RNTesterNavigationReducer';
import {
Screens,
getExamplesListWithBookmarksAndRecentlyUsed,
initialNavigationState,
} from './utils/testerStateUtils';
import * as React from 'react';
import {BackHandler, StyleSheet, View, useColorScheme} from 'react-native';

// RNTester App currently uses in memory storage for storing navigation state

const RNTesterApp = (): React.Node => {
const [state, dispatch] = React.useReducer(
RNTesterNavigationReducer,
initialNavigationState,
);
const colorScheme = useColorScheme();

const {
activeModuleKey,
activeModuleTitle,
activeModuleExampleKey,
screen,
bookmarks,
recentlyUsed,
} = state;

const examplesList = React.useMemo(
() =>
getExamplesListWithBookmarksAndRecentlyUsed({bookmarks, recentlyUsed}),
[bookmarks, recentlyUsed],
);

const handleBackPress = React.useCallback(() => {
if (activeModuleKey != null) {
dispatch({type: RNTesterNavigationActionsType.BACK_BUTTON_PRESS});
}
}, [dispatch, activeModuleKey]);

// Setup hardware back button press listener
React.useEffect(() => {
const handleHardwareBackPress = () => {
if (activeModuleKey) {
handleBackPress();
return true;
}
return false;
};

BackHandler.addEventListener('hardwareBackPress', handleHardwareBackPress);

return () => {
BackHandler.removeEventListener(
'hardwareBackPress',
handleHardwareBackPress,
);
};
}, [activeModuleKey, handleBackPress]);

const handleModuleCardPress = React.useCallback(
({exampleType, key, title}: any) => {
dispatch({
type: RNTesterNavigationActionsType.MODULE_CARD_PRESS,
data: {exampleType, key, title},
});
},
[dispatch],
);

const handleModuleExampleCardPress = React.useCallback(
(exampleName: string) => {
dispatch({
type: RNTesterNavigationActionsType.EXAMPLE_CARD_PRESS,
data: {key: exampleName},
});
},
[dispatch],
);

const toggleBookmark = React.useCallback(
({exampleType, key}: any) => {
dispatch({
type: RNTesterNavigationActionsType.BOOKMARK_PRESS,
data: {exampleType, key},
});
},
[dispatch],
);

const handleNavBarPress = React.useCallback(
(args: {screen: string}) => {
dispatch({
type: RNTesterNavigationActionsType.NAVBAR_PRESS,
data: {screen: args.screen},
});
},
[dispatch],
);

const theme = colorScheme === 'dark' ? themes.dark : themes.light;

if (examplesList === null) {
return null;
}

const activeModule =
activeModuleKey != null ? RNTesterList.Modules[activeModuleKey] : null;
const activeModuleExample =
activeModuleExampleKey != null
? activeModule?.examples.find(e => e.name === activeModuleExampleKey)
: null;
const title =
activeModuleTitle != null
? activeModuleTitle
: screen === Screens.COMPONENTS
? 'Components'
: screen === Screens.APIS
? 'APIs'
: 'Bookmarks';

const activeExampleList =
screen === Screens.COMPONENTS
? examplesList.components
: screen === Screens.APIS
? examplesList.apis
: examplesList.bookmarks;
import { View, StyleSheet, Text, TextInput } from 'react-native';

export default function App() {
const email =
'From [email protected] From [email protected]';
return (
<RNTesterThemeContext.Provider value={theme}>
<RNTTitleBar
title={title}
theme={theme}
onBack={activeModule ? handleBackPress : null}
documentationURL={activeModule?.documentationURL}
/>
<View
style={StyleSheet.compose(styles.container, {
backgroundColor: theme.GroupedBackgroundColor,
})}>
{activeModule != null ? (
<RNTesterModuleContainer
module={activeModule}
example={activeModuleExample}
onExampleCardPress={handleModuleExampleCardPress}
/>
) : screen === Screens.BOOKMARKS &&
examplesList.bookmarks.length === 0 ? (
<RNTesterEmptyBookmarksState />
) : (
<RNTesterModuleList
sections={activeExampleList}
toggleBookmark={toggleBookmark}
handleModuleCardPress={handleModuleCardPress}
/>
)}
</View>
<View style={styles.bottomNavbar}>
<RNTesterNavBar
screen={screen || Screens.COMPONENTS}
isExamplePageOpen={!!activeModule}
handleNavBarPress={handleNavBarPress}
/>
<>
<View style={styles.container}>
<View style={styles.flexBrokenStyle}>
<Text
textBreakStrategy="simple"
style={styles.parentText}
numberOfLines={1}
>
{email}
</Text>
</View>
</View>
</RNTesterThemeContext.Provider>
</>
);
};

export default RNTesterApp;
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 8,
backgroundColor: 'yellow',
},
bottomNavbar: {
height: navBarHeight,
flexBrokenStyle: {
flexDirection: 'row',
},
hidden: {
display: 'none',
parentText: {
backgroundColor: 'red',
},
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"prepare": "bob build",
"release": "release-it",
"patch": "node src/patch.js",
"patch-dev": "node src/patch-dev.js"
"patch-dev": "node src/patch-dev.js",
"postinstall": "yarn patch-dev"
},
"keywords": [
"react-native",
Expand Down