Skip to content

Commit

Permalink
Android textTransform style support (#20572)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Stephen Cook authored and facebook-github-bot committed Aug 14, 2018
1 parent 1081560 commit 22cf5dc
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 3 deletions.
3 changes: 0 additions & 3 deletions Libraries/Text/TextStylePropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,6 @@ const TextStylePropTypes = {
* @platform ios
*/
textDecorationColor: ColorPropType,
/**
* @platform ios
*/
textTransform: ReactPropTypes.oneOf([
'none' /*default*/,
'capitalize',
Expand Down
50 changes: 50 additions & 0 deletions RNTester/js/TextExample.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,56 @@ class TextExample extends React.Component<{}> {
make text look slightly misaligned when centered vertically.
</Text>
</RNTesterBlock>
<RNTesterBlock title="Text transform">
<Text style={{textTransform: 'uppercase'}}>
This text should be uppercased.
</Text>
<Text style={{textTransform: 'lowercase'}}>
This TEXT SHOULD be lowercased.
</Text>
<Text style={{textTransform: 'capitalize'}}>
This text should be CAPITALIZED.
</Text>
<Text style={{textTransform: 'capitalize'}}>
Mixed: <Text style={{textTransform: 'uppercase'}}>uppercase </Text>
<Text style={{textTransform: 'lowercase'}}>LoWeRcAsE </Text>
<Text style={{textTransform: 'capitalize'}}>
capitalize each word
</Text>
</Text>
<Text>
Should be "ABC":
<Text style={{textTransform: 'uppercase'}}>
a<Text>b</Text>c
</Text>
</Text>
<Text>
Should be "AbC":
<Text style={{textTransform: 'uppercase'}}>
a<Text style={{textTransform: 'none'}}>b</Text>c
</Text>
</Text>
<Text style={{textTransform: 'none'}}>
{
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
}
</Text>
<Text style={{textTransform: 'uppercase'}}>
{
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
}
</Text>
<Text style={{textTransform: 'lowercase'}}>
{
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
}
</Text>
<Text style={{textTransform: 'capitalize'}}>
{
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
}
</Text>
</RNTesterBlock>
</RNTesterPage>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.text;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.style.ReplacementSpan;
import java.text.BreakIterator;

public class CustomTextTransformSpan extends ReplacementSpan {

/**
* A {@link ReplacementSpan} that allows declarative changing of text casing.
* CustomTextTransformSpan will change e.g. "foo" to "FOO", when passed UPPERCASE.
*
* This needs to be a Span in order to achieve correctly nested transforms
* (for Text nodes within Text nodes, each with separate needed transforms)
*/

private final TextTransform mTransform;

public CustomTextTransformSpan(TextTransform transform) {
mTransform = transform;
}

@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
CharSequence transformedText = transformText(text);
canvas.drawText(transformedText, start, end, x, y, paint);
}

@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
CharSequence transformedText = transformText(text);
return Math.round(paint.measureText(transformedText, start, end));
}

private CharSequence transformText(CharSequence text) {
CharSequence transformed;

switch(mTransform) {
case UPPERCASE:
transformed = (CharSequence) text.toString().toUpperCase();
break;
case LOWERCASE:
transformed = (CharSequence) text.toString().toLowerCase();
break;
case CAPITALIZE:
transformed = (CharSequence) capitalize(text.toString());
break;
default:
transformed = text;
}

return transformed;
}

private String capitalize(String text) {
BreakIterator wordIterator = BreakIterator.getWordInstance();
wordIterator.setText(text);

StringBuilder res = new StringBuilder(text.length());
int start = wordIterator.first();
for (int end = wordIterator.next(); end != BreakIterator.DONE; end = wordIterator.next()) {
String word = text.substring(start, end);
if (Character.isLetterOrDigit(word.charAt(0))) {
res.append(Character.toUpperCase(word.charAt(0)));
res.append(word.substring(1).toLowerCase());
} else {
res.append(word);
}
start = end;
}

return res.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
public static final String PROP_SHADOW_RADIUS = "textShadowRadius";
public static final String PROP_SHADOW_COLOR = "textShadowColor";

public static final String PROP_TEXT_TRANSFORM = "textTransform";

public static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;

private static class SetSpanOperation {
Expand Down Expand Up @@ -164,6 +166,13 @@ private static void buildSpannedFromShadowNode(
new SetSpanOperation(
start, end, new CustomLineHeightSpan(textShadowNode.getEffectiveLineHeight())));
}
if (textShadowNode.mTextTransform != TextTransform.UNSET) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomTextTransformSpan(textShadowNode.mTextTransform)));
}
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag())));
}
}
Expand Down Expand Up @@ -251,6 +260,7 @@ private static int parseNumericFontWeight(String fontWeightString) {
protected int mTextAlign = Gravity.NO_GRAVITY;
protected int mTextBreakStrategy =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY;
protected TextTransform mTextTransform = TextTransform.UNSET;

protected float mTextShadowOffsetDx = 0;
protected float mTextShadowOffsetDy = 0;
Expand Down Expand Up @@ -307,6 +317,7 @@ public ReactBaseTextShadowNode(ReactBaseTextShadowNode node) {
mLineHeightInput = node.mLineHeightInput;
mTextAlign = node.mTextAlign;
mTextBreakStrategy = node.mTextBreakStrategy;
mTextTransform = node.mTextTransform;

mTextShadowOffsetDx = node.mTextShadowOffsetDx;
mTextShadowOffsetDy = node.mTextShadowOffsetDy;
Expand Down Expand Up @@ -561,4 +572,20 @@ public void setTextShadowColor(int textShadowColor) {
markUpdated();
}
}

@ReactProp(name = PROP_TEXT_TRANSFORM)
public void setTextTransform(@Nullable String textTransform) {
if (textTransform == null || "none".equals(textTransform)) {
mTextTransform = TextTransform.NONE;
} else if ("uppercase".equals(textTransform)) {
mTextTransform = TextTransform.UPPERCASE;
} else if ("lowercase".equals(textTransform)) {
mTextTransform = TextTransform.LOWERCASE;
} else if ("capitalize".equals(textTransform)) {
mTextTransform = TextTransform.CAPITALIZE;
} else {
throw new JSApplicationIllegalArgumentException("Invalid textTransform: " + textTransform);
}
markUpdated();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.text;

/**
* Types of text transforms for CustomTextTransformSpan
*/
public enum TextTransform { NONE, UPPERCASE, LOWERCASE, CAPITALIZE, UNSET };
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.views.text.ReactRawTextShadowNode;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
import com.facebook.react.views.text.CustomTextTransformSpan;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -341,6 +342,70 @@ public void testBackgroundColorStyleApplied() {
assertThat(((ReactViewBackgroundDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE);
}

@Test
public void testTextTransformNoneApplied() {
UIManagerModule uiManager = getUIManagerModule();

String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
String testTextTransformed = testTextEntered;

ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of("textTransform", "none"),
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));

TextView textView = (TextView) rootView.getChildAt(0);
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
}

@Test
public void testTextTransformUppercaseApplied() {
UIManagerModule uiManager = getUIManagerModule();

String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
String testTextTransformed = ".AA\tBB\t\tCC DD EE \r\nZZ I LIKE TO EAT APPLES. \n中文ÉÉ 我喜欢吃苹果。AWDAWD ";

ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of("textTransform", "uppercase"),
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));

TextView textView = (TextView) rootView.getChildAt(0);
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
}

@Test
public void testTextTransformLowercaseApplied() {
UIManagerModule uiManager = getUIManagerModule();

String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
String testTextTransformed = ".aa\tbb\t\tcc dd ee \r\nzz i like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";

ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of("textTransform", "lowercase"),
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));

TextView textView = (TextView) rootView.getChildAt(0);
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
}

@Test
public void testTextTransformCapitalizeApplied() {
UIManagerModule uiManager = getUIManagerModule();

String testTextEntered = ".aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd ";
String testTextTransformed = ".Aa\tBb\t\tCc Dd Ee \r\nZz I Like To Eat Apples. \n中文Éé 我喜欢吃苹果。Awdawd ";

ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of("textTransform", "capitalize"),
JavaOnlyMap.of(ReactRawTextShadowNode.PROP_TEXT, testTextEntered));

TextView textView = (TextView) rootView.getChildAt(0);
assertThat(textView.getText().toString()).isEqualTo(testTextTransformed);
}

// JELLY_BEAN is needed for TextView#getMaxLines(), which is OK, because in the actual code we
// only use TextView#setMaxLines() which exists since API Level 1.
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Expand Down

1 comment on commit 22cf5dc

@ziyafenn
Copy link

Choose a reason for hiding this comment

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

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.