Skip to content

Commit 3a33e75

Browse files
janicduplessisfacebook-github-bot
authored andcommitted
Fix textTransform when used with other text styles on Android (#22670)
Summary: On Android `textTransform` breaks other styles applied to the text. It seems related to the usage of `ReplacementSpan` which allows drawing the text manually but seems to throw away some changes made by other span applied to the text. To fix it I removed the usage of `ReplacementSpan` and simply transform the text before appending it to the `Spannable` string. To make sure textTransform is inherited correctly I added it to TextAttributes which handles this. Pull Request resolved: #22670 Differential Revision: D13494819 Pulled By: cpojer fbshipit-source-id: 1c69591084aa906c2d3b10153b354d39c0936340
1 parent 27617be commit 3a33e75

File tree

6 files changed

+92
-109
lines changed

6 files changed

+92
-109
lines changed

RNTester/js/TextExample.android.js

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* @flow
99
*/
1010

11+
/* eslint-disable react-native/no-inline-styles */
12+
1113
'use strict';
1214

1315
const React = require('react');
@@ -600,6 +602,18 @@ class TextExample extends React.Component<{}> {
600602
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
601603
}
602604
</Text>
605+
<Text
606+
style={{
607+
textTransform: 'uppercase',
608+
fontSize: 16,
609+
color: 'turquoise',
610+
backgroundColor: 'blue',
611+
lineHeight: 32,
612+
letterSpacing: 2,
613+
alignSelf: 'flex-start',
614+
}}>
615+
Works with other text styles
616+
</Text>
603617
</RNTesterBlock>
604618
</RNTesterPage>
605619
);

ReactAndroid/src/main/java/com/facebook/react/views/text/CustomTextTransformSpan.java

-83
This file was deleted.

ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java

+13-15
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.facebook.react.uimanager.ViewProps;
2323
import com.facebook.react.uimanager.annotations.ReactProp;
2424
import com.facebook.yoga.YogaDirection;
25+
2526
import java.util.ArrayList;
2627
import java.util.List;
2728
import javax.annotation.Nullable;
@@ -94,7 +95,10 @@ private static void buildSpannedFromShadowNode(
9495
ReactShadowNode child = textShadowNode.getChildAt(i);
9596

9697
if (child instanceof ReactRawTextShadowNode) {
97-
sb.append(((ReactRawTextShadowNode) child).getText());
98+
sb.append(
99+
TextTransform.apply(
100+
((ReactRawTextShadowNode) child).getText(),
101+
textAttributes.getTextTransform()));
98102
} else if (child instanceof ReactBaseTextShadowNode) {
99103
buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops, textAttributes, sb.length());
100104
} else if (child instanceof ReactTextInlineImageShadowNode) {
@@ -182,13 +186,6 @@ private static void buildSpannedFromShadowNode(
182186
new SetSpanOperation(
183187
start, end, new CustomLineHeightSpan(effectiveLineHeight)));
184188
}
185-
if (textShadowNode.mTextTransform != TextTransform.UNSET) {
186-
ops.add(
187-
new SetSpanOperation(
188-
start,
189-
end,
190-
new CustomTextTransformSpan(textShadowNode.mTextTransform)));
191-
}
192189
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
193190
}
194191
}
@@ -207,7 +204,7 @@ protected static Spannable spannedFromShadowNode(
207204
if (text != null) {
208205
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
209206
// TextInput).
210-
sb.append(text);
207+
sb.append(TextTransform.apply(text, textShadowNode.mTextAttributes.getTextTransform()));
211208
}
212209

213210
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, 0);
@@ -266,7 +263,6 @@ private static int parseNumericFontWeight(String fontWeightString) {
266263
protected int mTextAlign = Gravity.NO_GRAVITY;
267264
protected int mTextBreakStrategy =
268265
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
269-
protected TextTransform mTextTransform = TextTransform.UNSET;
270266

271267
protected float mTextShadowOffsetDx = 0;
272268
protected float mTextShadowOffsetDy = 0;
@@ -528,14 +524,16 @@ public void setTextShadowColor(int textShadowColor) {
528524

529525
@ReactProp(name = PROP_TEXT_TRANSFORM)
530526
public void setTextTransform(@Nullable String textTransform) {
531-
if (textTransform == null || "none".equals(textTransform)) {
532-
mTextTransform = TextTransform.NONE;
527+
if (textTransform == null) {
528+
mTextAttributes.setTextTransform(TextTransform.UNSET);
529+
} else if ("none".equals(textTransform)) {
530+
mTextAttributes.setTextTransform(TextTransform.NONE);
533531
} else if ("uppercase".equals(textTransform)) {
534-
mTextTransform = TextTransform.UPPERCASE;
532+
mTextAttributes.setTextTransform(TextTransform.UPPERCASE);
535533
} else if ("lowercase".equals(textTransform)) {
536-
mTextTransform = TextTransform.LOWERCASE;
534+
mTextAttributes.setTextTransform(TextTransform.LOWERCASE);
537535
} else if ("capitalize".equals(textTransform)) {
538-
mTextTransform = TextTransform.CAPITALIZE;
536+
mTextAttributes.setTextTransform(TextTransform.CAPITALIZE);
539537
} else {
540538
throw new JSApplicationIllegalArgumentException("Invalid textTransform: " + textTransform);
541539
}

ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class TextAttributes {
2828
private float mLetterSpacing = Float.NaN;
2929
private float mMaxFontSizeMultiplier = Float.NaN;
3030
private float mHeightOfTallestInlineImage = Float.NaN;
31+
private TextTransform mTextTransform = TextTransform.UNSET;
3132

3233
public TextAttributes() {
3334
}
@@ -44,6 +45,7 @@ public TextAttributes applyChild(TextAttributes child) {
4445
result.mLetterSpacing = !Float.isNaN(child.mLetterSpacing) ? child.mLetterSpacing : mLetterSpacing;
4546
result.mMaxFontSizeMultiplier = !Float.isNaN(child.mMaxFontSizeMultiplier) ? child.mMaxFontSizeMultiplier : mMaxFontSizeMultiplier;
4647
result.mHeightOfTallestInlineImage = !Float.isNaN(child.mHeightOfTallestInlineImage) ? child.mHeightOfTallestInlineImage : mHeightOfTallestInlineImage;
48+
result.mTextTransform = child.mTextTransform != TextTransform.UNSET ? child.mTextTransform : mTextTransform;
4749

4850
return result;
4951
}
@@ -102,6 +104,14 @@ public void setHeightOfTallestInlineImage(float value) {
102104
mHeightOfTallestInlineImage = value;
103105
}
104106

107+
public TextTransform getTextTransform() {
108+
return mTextTransform;
109+
}
110+
111+
public void setTextTransform(TextTransform textTransform) {
112+
mTextTransform = textTransform;
113+
}
114+
105115
// Getters for effective values
106116
//
107117
// In general, these return `Float.NaN` if the property doesn't have a value.
@@ -164,6 +174,7 @@ public String toString() {
164174
+ "\n getEffectiveLetterSpacing(): " + getEffectiveLetterSpacing()
165175
+ "\n getLineHeight(): " + getLineHeight()
166176
+ "\n getEffectiveLineHeight(): " + getEffectiveLineHeight()
177+
+ "\n getTextTransform(): " + getTextTransform()
167178
+ "\n getMaxFontSizeMultiplier(): " + getMaxFontSizeMultiplier()
168179
+ "\n getEffectiveMaxFontSizeMultiplier(): " + getEffectiveMaxFontSizeMultiplier()
169180
+ "\n}"

ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java

+6-10
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ private static void buildSpannableFromFragment(
5757
ReadableMap fragment = fragments.getMap(i);
5858
int start = sb.length();
5959

60-
//ReactRawText
61-
sb.append(fragment.getString("string"));
60+
// ReactRawText
61+
TextAttributeProps textAttributes = new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes")));
62+
63+
sb.append(TextTransform.apply(
64+
fragment.getString("string"),
65+
textAttributes.mTextTransform));
6266

6367
// TODO: add support for TextInlineImage and BaseText
6468
// if (child instanceof ReactRawTextShadowNode) {
@@ -79,7 +83,6 @@ private static void buildSpannableFromFragment(
7983
// "Unexpected view type nested under text node: " + child.getClass());
8084
// }
8185

82-
TextAttributeProps textAttributes = new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes")));
8386
int end = sb.length();
8487
if (end >= start) {
8588
if (textAttributes.mIsColorSet) {
@@ -136,13 +139,6 @@ private static void buildSpannableFromFragment(
136139
new SetSpanOperation(
137140
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
138141
}
139-
if (textAttributes.mTextTransform != TextTransform.UNSET && textAttributes.mTextTransform != TextTransform.NONE) {
140-
ops.add(
141-
new SetSpanOperation(
142-
start,
143-
end,
144-
new CustomTextTransformSpan(textAttributes.mTextTransform)));
145-
}
146142

147143
int reactTag = fragment.getInt("reactTag");
148144
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));

ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.java

+48-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,54 @@
77

88
package com.facebook.react.views.text;
99

10+
import java.text.BreakIterator;
11+
1012
/**
1113
* Types of text transforms for CustomTextTransformSpan
1214
*/
13-
public enum TextTransform { NONE, UPPERCASE, LOWERCASE, CAPITALIZE, UNSET };
15+
public enum TextTransform {
16+
NONE, UPPERCASE, LOWERCASE, CAPITALIZE, UNSET;
17+
18+
public static String apply(String text, TextTransform textTransform) {
19+
if (text == null) {
20+
return null;
21+
}
22+
23+
String transformed;
24+
switch(textTransform) {
25+
case UPPERCASE:
26+
transformed = text.toUpperCase();
27+
break;
28+
case LOWERCASE:
29+
transformed = text.toLowerCase();
30+
break;
31+
case CAPITALIZE:
32+
transformed = capitalize(text);
33+
break;
34+
default:
35+
transformed = text;
36+
}
37+
38+
return transformed;
39+
}
40+
41+
private static String capitalize(String text) {
42+
BreakIterator wordIterator = BreakIterator.getWordInstance();
43+
wordIterator.setText(text);
44+
45+
StringBuilder res = new StringBuilder(text.length());
46+
int start = wordIterator.first();
47+
for (int end = wordIterator.next(); end != BreakIterator.DONE; end = wordIterator.next()) {
48+
String word = text.substring(start, end);
49+
if (Character.isLetterOrDigit(word.charAt(0))) {
50+
res.append(Character.toUpperCase(word.charAt(0)));
51+
res.append(word.substring(1).toLowerCase());
52+
} else {
53+
res.append(word);
54+
}
55+
start = end;
56+
}
57+
58+
return res.toString();
59+
}
60+
};

0 commit comments

Comments
 (0)