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 kelset committed Oct 23, 2018
1 parent 05a036d commit ef835b5
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 @@ -535,6 +535,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 @@ -53,6 +53,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 @@ -168,6 +170,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 @@ -257,6 +266,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 @@ -313,6 +323,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 @@ -567,4 +578,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

0 comments on commit ef835b5

Please sign in to comment.