Skip to content

Commit 22cf5dc

Browse files
Stephen Cookfacebook-github-bot
Stephen Cook
authored andcommittedAug 14, 2018
Android textTransform style support (#20572)
Summary: Issue #2088 (closed, but a bit pre-emptively imo, since Android support was skipped) Related (merged) iOS PR #18387 Related documentation PR facebook/react-native-website#500 The basic desire is to have a declarative mechanism to transform text content to uppercase or lowercase or titlecase ("capitalized"). Pull Request resolved: #20572 Differential Revision: D9311716 Pulled By: hramos fbshipit-source-id: dfbb855117196958e7ae5e980700d31be07a448d
1 parent 1081560 commit 22cf5dc

File tree

6 files changed

+238
-3
lines changed

6 files changed

+238
-3
lines changed
 

‎Libraries/Text/TextStylePropTypes.js

-3
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,6 @@ const TextStylePropTypes = {
108108
* @platform ios
109109
*/
110110
textDecorationColor: ColorPropType,
111-
/**
112-
* @platform ios
113-
*/
114111
textTransform: ReactPropTypes.oneOf([
115112
'none' /*default*/,
116113
'capitalize',

‎RNTester/js/TextExample.android.js

+50
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,56 @@ class TextExample extends React.Component<{}> {
531531
make text look slightly misaligned when centered vertically.
532532
</Text>
533533
</RNTesterBlock>
534+
<RNTesterBlock title="Text transform">
535+
<Text style={{textTransform: 'uppercase'}}>
536+
This text should be uppercased.
537+
</Text>
538+
<Text style={{textTransform: 'lowercase'}}>
539+
This TEXT SHOULD be lowercased.
540+
</Text>
541+
<Text style={{textTransform: 'capitalize'}}>
542+
This text should be CAPITALIZED.
543+
</Text>
544+
<Text style={{textTransform: 'capitalize'}}>
545+
Mixed: <Text style={{textTransform: 'uppercase'}}>uppercase </Text>
546+
<Text style={{textTransform: 'lowercase'}}>LoWeRcAsE </Text>
547+
<Text style={{textTransform: 'capitalize'}}>
548+
capitalize each word
549+
</Text>
550+
</Text>
551+
<Text>
552+
Should be "ABC":
553+
<Text style={{textTransform: 'uppercase'}}>
554+
a<Text>b</Text>c
555+
</Text>
556+
</Text>
557+
<Text>
558+
Should be "AbC":
559+
<Text style={{textTransform: 'uppercase'}}>
560+
a<Text style={{textTransform: 'none'}}>b</Text>c
561+
</Text>
562+
</Text>
563+
<Text style={{textTransform: 'none'}}>
564+
{
565+
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
566+
}
567+
</Text>
568+
<Text style={{textTransform: 'uppercase'}}>
569+
{
570+
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
571+
}
572+
</Text>
573+
<Text style={{textTransform: 'lowercase'}}>
574+
{
575+
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
576+
}
577+
</Text>
578+
<Text style={{textTransform: 'capitalize'}}>
579+
{
580+
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
581+
}
582+
</Text>
583+
</RNTesterBlock>
534584
</RNTesterPage>
535585
);
536586
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text;
9+
10+
import android.graphics.Canvas;
11+
import android.graphics.Paint;
12+
import android.text.style.ReplacementSpan;
13+
import java.text.BreakIterator;
14+
15+
public class CustomTextTransformSpan extends ReplacementSpan {
16+
17+
/**
18+
* A {@link ReplacementSpan} that allows declarative changing of text casing.
19+
* CustomTextTransformSpan will change e.g. "foo" to "FOO", when passed UPPERCASE.
20+
*
21+
* This needs to be a Span in order to achieve correctly nested transforms
22+
* (for Text nodes within Text nodes, each with separate needed transforms)
23+
*/
24+
25+
private final TextTransform mTransform;
26+
27+
public CustomTextTransformSpan(TextTransform transform) {
28+
mTransform = transform;
29+
}
30+
31+
@Override
32+
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
33+
CharSequence transformedText = transformText(text);
34+
canvas.drawText(transformedText, start, end, x, y, paint);
35+
}
36+
37+
@Override
38+
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
39+
CharSequence transformedText = transformText(text);
40+
return Math.round(paint.measureText(transformedText, start, end));
41+
}
42+
43+
private CharSequence transformText(CharSequence text) {
44+
CharSequence transformed;
45+
46+
switch(mTransform) {
47+
case UPPERCASE:
48+
transformed = (CharSequence) text.toString().toUpperCase();
49+
break;
50+
case LOWERCASE:
51+
transformed = (CharSequence) text.toString().toLowerCase();
52+
break;
53+
case CAPITALIZE:
54+
transformed = (CharSequence) capitalize(text.toString());
55+
break;
56+
default:
57+
transformed = text;
58+
}
59+
60+
return transformed;
61+
}
62+
63+
private String capitalize(String text) {
64+
BreakIterator wordIterator = BreakIterator.getWordInstance();
65+
wordIterator.setText(text);
66+
67+
StringBuilder res = new StringBuilder(text.length());
68+
int start = wordIterator.first();
69+
for (int end = wordIterator.next(); end != BreakIterator.DONE; end = wordIterator.next()) {
70+
String word = text.substring(start, end);
71+
if (Character.isLetterOrDigit(word.charAt(0))) {
72+
res.append(Character.toUpperCase(word.charAt(0)));
73+
res.append(word.substring(1).toLowerCase());
74+
} else {
75+
res.append(word);
76+
}
77+
start = end;
78+
}
79+
80+
return res.toString();
81+
}
82+
83+
}

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

+27
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
5252
public static final String PROP_SHADOW_RADIUS = "textShadowRadius";
5353
public static final String PROP_SHADOW_COLOR = "textShadowColor";
5454

55+
public static final String PROP_TEXT_TRANSFORM = "textTransform";
56+
5557
public static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
5658

5759
private static class SetSpanOperation {
@@ -164,6 +166,13 @@ private static void buildSpannedFromShadowNode(
164166
new SetSpanOperation(
165167
start, end, new CustomLineHeightSpan(textShadowNode.getEffectiveLineHeight())));
166168
}
169+
if (textShadowNode.mTextTransform != TextTransform.UNSET) {
170+
ops.add(
171+
new SetSpanOperation(
172+
start,
173+
end,
174+
new CustomTextTransformSpan(textShadowNode.mTextTransform)));
175+
}
167176
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
168177
}
169178
}
@@ -251,6 +260,7 @@ private static int parseNumericFontWeight(String fontWeightString) {
251260
protected int mTextAlign = Gravity.NO_GRAVITY;
252261
protected int mTextBreakStrategy =
253262
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
263+
protected TextTransform mTextTransform = TextTransform.UNSET;
254264

255265
protected float mTextShadowOffsetDx = 0;
256266
protected float mTextShadowOffsetDy = 0;
@@ -307,6 +317,7 @@ public ReactBaseTextShadowNode(ReactBaseTextShadowNode node) {
307317
mLineHeightInput = node.mLineHeightInput;
308318
mTextAlign = node.mTextAlign;
309319
mTextBreakStrategy = node.mTextBreakStrategy;
320+
mTextTransform = node.mTextTransform;
310321

311322
mTextShadowOffsetDx = node.mTextShadowOffsetDx;
312323
mTextShadowOffsetDy = node.mTextShadowOffsetDy;
@@ -561,4 +572,20 @@ public void setTextShadowColor(int textShadowColor) {
561572
markUpdated();
562573
}
563574
}
575+
576+
@ReactProp(name = PROP_TEXT_TRANSFORM)
577+
public void setTextTransform(@Nullable String textTransform) {
578+
if (textTransform == null || "none".equals(textTransform)) {
579+
mTextTransform = TextTransform.NONE;
580+
} else if ("uppercase".equals(textTransform)) {
581+
mTextTransform = TextTransform.UPPERCASE;
582+
} else if ("lowercase".equals(textTransform)) {
583+
mTextTransform = TextTransform.LOWERCASE;
584+
} else if ("capitalize".equals(textTransform)) {
585+
mTextTransform = TextTransform.CAPITALIZE;
586+
} else {
587+
throw new JSApplicationIllegalArgumentException("Invalid textTransform: " + textTransform);
588+
}
589+
markUpdated();
590+
}
564591
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.text;
9+
10+
/**
11+
* Types of text transforms for CustomTextTransformSpan
12+
*/
13+
public enum TextTransform { NONE, UPPERCASE, LOWERCASE, CAPITALIZE, UNSET };

‎ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java

+65
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.facebook.react.uimanager.ViewProps;
3737
import com.facebook.react.views.text.ReactRawTextShadowNode;
3838
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
39+
import com.facebook.react.views.text.CustomTextTransformSpan;
3940
import java.util.ArrayList;
4041
import java.util.Arrays;
4142
import java.util.List;
@@ -341,6 +342,70 @@ public void testBackgroundColorStyleApplied() {
341342
assertThat(((ReactViewBackgroundDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
342343
}
343344

345+
@Test
346+
public void testTextTransformNoneApplied() {
347+
UIManagerModule uiManager = getUIManagerModule();
348+
349+
String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
350+
String testTextTransformed = testTextEntered;
351+
352+
ReactRootView rootView = createText(
353+
uiManager,
354+
JavaOnlyMap.of("textTransform", "none"),
355+
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));
356+
357+
TextView textView = (TextView) rootView.getChildAt(0);
358+
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
359+
}
360+
361+
@Test
362+
public void testTextTransformUppercaseApplied() {
363+
UIManagerModule uiManager = getUIManagerModule();
364+
365+
String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
366+
String testTextTransformed = ".AA\tBB\t\tCC DD EE \r\nZZ I LIKE TO EAT APPLES. \n中文ÉÉ 我喜欢吃苹果。AWDAWD ";
367+
368+
ReactRootView rootView = createText(
369+
uiManager,
370+
JavaOnlyMap.of("textTransform", "uppercase"),
371+
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));
372+
373+
TextView textView = (TextView) rootView.getChildAt(0);
374+
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
375+
}
376+
377+
@Test
378+
public void testTextTransformLowercaseApplied() {
379+
UIManagerModule uiManager = getUIManagerModule();
380+
381+
String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
382+
String testTextTransformed = ".aa\tbb\t\tcc dd ee \r\nzz i like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
383+
384+
ReactRootView rootView = createText(
385+
uiManager,
386+
JavaOnlyMap.of("textTransform", "lowercase"),
387+
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));
388+
389+
TextView textView = (TextView) rootView.getChildAt(0);
390+
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
391+
}
392+
393+
@Test
394+
public void testTextTransformCapitalizeApplied() {
395+
UIManagerModule uiManager = getUIManagerModule();
396+
397+
String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
398+
String testTextTransformed = ".Aa\tBb\t\tCc Dd Ee \r\nZz I Like To Eat Apples. \n中文Éé 我喜欢吃苹果。Awdawd ";
399+
400+
ReactRootView rootView = createText(
401+
uiManager,
402+
JavaOnlyMap.of("textTransform", "capitalize"),
403+
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));
404+
405+
TextView textView = (TextView) rootView.getChildAt(0);
406+
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
407+
}
408+
344409
// JELLY_BEAN is needed for TextView#getMaxLines(), which is OK, because in the actual code we
345410
// only use TextView#setMaxLines() which exists since API Level 1.
346411
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)

1 commit comments

Comments
 (1)

ziyafenn commented on Nov 8, 2018

@ziyafenn

enabling textTransform on android with RN0.57.4 breaks the styling of the text and renders the text very weirdly.

Please sign in to comment.