mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-01-12 22:50:10 +08:00
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: https://github.com/facebook/react-native/pull/22670 Differential Revision: D13494819 Pulled By: cpojer fbshipit-source-id: 1c69591084aa906c2d3b10153b354d39c0936340
This commit is contained in:
committed by
Facebook Github Bot
parent
27617be9bb
commit
3a33e75183
@@ -8,6 +8,8 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
/* eslint-disable react-native/no-inline-styles */
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
@@ -600,6 +602,18 @@ class TextExample extends React.Component<{}> {
|
||||
'.aa\tbb\t\tcc dd EE \r\nZZ I like to eat apples. \n中文éé 我喜欢吃苹果。awdawd '
|
||||
}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 16,
|
||||
color: 'turquoise',
|
||||
backgroundColor: 'blue',
|
||||
lineHeight: 32,
|
||||
letterSpacing: 2,
|
||||
alignSelf: 'flex-start',
|
||||
}}>
|
||||
Works with other text styles
|
||||
</Text>
|
||||
</RNTesterBlock>
|
||||
</RNTesterPage>
|
||||
);
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* 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 implements ReactSpan {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.facebook.react.uimanager.ReactShadowNode;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.yoga.YogaDirection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -94,7 +95,10 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
ReactShadowNode child = textShadowNode.getChildAt(i);
|
||||
|
||||
if (child instanceof ReactRawTextShadowNode) {
|
||||
sb.append(((ReactRawTextShadowNode) child).getText());
|
||||
sb.append(
|
||||
TextTransform.apply(
|
||||
((ReactRawTextShadowNode) child).getText(),
|
||||
textAttributes.getTextTransform()));
|
||||
} else if (child instanceof ReactBaseTextShadowNode) {
|
||||
buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops, textAttributes, sb.length());
|
||||
} else if (child instanceof ReactTextInlineImageShadowNode) {
|
||||
@@ -182,13 +186,6 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
new SetSpanOperation(
|
||||
start, end, new CustomLineHeightSpan(effectiveLineHeight)));
|
||||
}
|
||||
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())));
|
||||
}
|
||||
}
|
||||
@@ -207,7 +204,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
if (text != null) {
|
||||
// Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on
|
||||
// TextInput).
|
||||
sb.append(text);
|
||||
sb.append(TextTransform.apply(text, textShadowNode.mTextAttributes.getTextTransform()));
|
||||
}
|
||||
|
||||
buildSpannedFromShadowNode(textShadowNode, sb, ops, null, 0);
|
||||
@@ -266,7 +263,6 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
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;
|
||||
@@ -528,14 +524,16 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
@ReactProp(name = PROP_TEXT_TRANSFORM)
|
||||
public void setTextTransform(@Nullable String textTransform) {
|
||||
if (textTransform == null || "none".equals(textTransform)) {
|
||||
mTextTransform = TextTransform.NONE;
|
||||
if (textTransform == null) {
|
||||
mTextAttributes.setTextTransform(TextTransform.UNSET);
|
||||
} else if ("none".equals(textTransform)) {
|
||||
mTextAttributes.setTextTransform(TextTransform.NONE);
|
||||
} else if ("uppercase".equals(textTransform)) {
|
||||
mTextTransform = TextTransform.UPPERCASE;
|
||||
mTextAttributes.setTextTransform(TextTransform.UPPERCASE);
|
||||
} else if ("lowercase".equals(textTransform)) {
|
||||
mTextTransform = TextTransform.LOWERCASE;
|
||||
mTextAttributes.setTextTransform(TextTransform.LOWERCASE);
|
||||
} else if ("capitalize".equals(textTransform)) {
|
||||
mTextTransform = TextTransform.CAPITALIZE;
|
||||
mTextAttributes.setTextTransform(TextTransform.CAPITALIZE);
|
||||
} else {
|
||||
throw new JSApplicationIllegalArgumentException("Invalid textTransform: " + textTransform);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class TextAttributes {
|
||||
private float mLetterSpacing = Float.NaN;
|
||||
private float mMaxFontSizeMultiplier = Float.NaN;
|
||||
private float mHeightOfTallestInlineImage = Float.NaN;
|
||||
private TextTransform mTextTransform = TextTransform.UNSET;
|
||||
|
||||
public TextAttributes() {
|
||||
}
|
||||
@@ -44,6 +45,7 @@ public class TextAttributes {
|
||||
result.mLetterSpacing = !Float.isNaN(child.mLetterSpacing) ? child.mLetterSpacing : mLetterSpacing;
|
||||
result.mMaxFontSizeMultiplier = !Float.isNaN(child.mMaxFontSizeMultiplier) ? child.mMaxFontSizeMultiplier : mMaxFontSizeMultiplier;
|
||||
result.mHeightOfTallestInlineImage = !Float.isNaN(child.mHeightOfTallestInlineImage) ? child.mHeightOfTallestInlineImage : mHeightOfTallestInlineImage;
|
||||
result.mTextTransform = child.mTextTransform != TextTransform.UNSET ? child.mTextTransform : mTextTransform;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -102,6 +104,14 @@ public class TextAttributes {
|
||||
mHeightOfTallestInlineImage = value;
|
||||
}
|
||||
|
||||
public TextTransform getTextTransform() {
|
||||
return mTextTransform;
|
||||
}
|
||||
|
||||
public void setTextTransform(TextTransform textTransform) {
|
||||
mTextTransform = textTransform;
|
||||
}
|
||||
|
||||
// Getters for effective values
|
||||
//
|
||||
// In general, these return `Float.NaN` if the property doesn't have a value.
|
||||
@@ -164,6 +174,7 @@ public class TextAttributes {
|
||||
+ "\n getEffectiveLetterSpacing(): " + getEffectiveLetterSpacing()
|
||||
+ "\n getLineHeight(): " + getLineHeight()
|
||||
+ "\n getEffectiveLineHeight(): " + getEffectiveLineHeight()
|
||||
+ "\n getTextTransform(): " + getTextTransform()
|
||||
+ "\n getMaxFontSizeMultiplier(): " + getMaxFontSizeMultiplier()
|
||||
+ "\n getEffectiveMaxFontSizeMultiplier(): " + getEffectiveMaxFontSizeMultiplier()
|
||||
+ "\n}"
|
||||
|
||||
@@ -57,8 +57,12 @@ public class TextLayoutManager {
|
||||
ReadableMap fragment = fragments.getMap(i);
|
||||
int start = sb.length();
|
||||
|
||||
//ReactRawText
|
||||
sb.append(fragment.getString("string"));
|
||||
// ReactRawText
|
||||
TextAttributeProps textAttributes = new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes")));
|
||||
|
||||
sb.append(TextTransform.apply(
|
||||
fragment.getString("string"),
|
||||
textAttributes.mTextTransform));
|
||||
|
||||
// TODO: add support for TextInlineImage and BaseText
|
||||
// if (child instanceof ReactRawTextShadowNode) {
|
||||
@@ -79,7 +83,6 @@ public class TextLayoutManager {
|
||||
// "Unexpected view type nested under text node: " + child.getClass());
|
||||
// }
|
||||
|
||||
TextAttributeProps textAttributes = new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes")));
|
||||
int end = sb.length();
|
||||
if (end >= start) {
|
||||
if (textAttributes.mIsColorSet) {
|
||||
@@ -136,13 +139,6 @@ public class TextLayoutManager {
|
||||
new SetSpanOperation(
|
||||
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
|
||||
}
|
||||
if (textAttributes.mTextTransform != TextTransform.UNSET && textAttributes.mTextTransform != TextTransform.NONE) {
|
||||
ops.add(
|
||||
new SetSpanOperation(
|
||||
start,
|
||||
end,
|
||||
new CustomTextTransformSpan(textAttributes.mTextTransform)));
|
||||
}
|
||||
|
||||
int reactTag = fragment.getInt("reactTag");
|
||||
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));
|
||||
|
||||
@@ -7,7 +7,54 @@
|
||||
|
||||
package com.facebook.react.views.text;
|
||||
|
||||
import java.text.BreakIterator;
|
||||
|
||||
/**
|
||||
* Types of text transforms for CustomTextTransformSpan
|
||||
*/
|
||||
public enum TextTransform { NONE, UPPERCASE, LOWERCASE, CAPITALIZE, UNSET };
|
||||
public enum TextTransform {
|
||||
NONE, UPPERCASE, LOWERCASE, CAPITALIZE, UNSET;
|
||||
|
||||
public static String apply(String text, TextTransform textTransform) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String transformed;
|
||||
switch(textTransform) {
|
||||
case UPPERCASE:
|
||||
transformed = text.toUpperCase();
|
||||
break;
|
||||
case LOWERCASE:
|
||||
transformed = text.toLowerCase();
|
||||
break;
|
||||
case CAPITALIZE:
|
||||
transformed = capitalize(text);
|
||||
break;
|
||||
default:
|
||||
transformed = text;
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
private static 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();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user