diff --git a/Libraries/Text/TextStylePropTypes.js b/Libraries/Text/TextStylePropTypes.js index d92708683d981e..52db01a30e44d1 100644 --- a/Libraries/Text/TextStylePropTypes.js +++ b/Libraries/Text/TextStylePropTypes.js @@ -49,7 +49,16 @@ const TextStylePropTypes = { textShadowRadius: ReactPropTypes.number, textShadowColor: ColorPropType, /** - * @platform ios + * Increase or decrease the spacing between characters. Based on the platform specific + * rendering this style annotation will be rendered slightly differently on Android and iOS. + * Default is no letter spacing. + * + * Android: Only supported since Android 5+, older versions will will ignore this attribute. + * Please notice that additional space will be added *around* the characters and the space + * is calculated based on your font size and the font family. To left-align a text similar + * to iOS (or in different font sizes) you should add a Platform-specific negative layout attribute. + * + * iOS: The additional space will be added behind the character and is defined in points. */ letterSpacing: ReactPropTypes.number, lineHeight: ReactPropTypes.number, diff --git a/RNTester/js/TextExample.android.js b/RNTester/js/TextExample.android.js index b5a61c00e240b8..8b95f7aac371ca 100644 --- a/RNTester/js/TextExample.android.js +++ b/RNTester/js/TextExample.android.js @@ -322,6 +322,22 @@ class TextExample extends React.Component<{}> { Holisticly formulate inexpensive ideas before best-of-breed benefits. Continually expedite magnetic potentialities rather than client-focused interfaces. + + + + letterSpacing = 0 + + + letterSpacing = 2 + + + letterSpacing = 9 + + + letterSpacing = -1 + + + diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index fe71f1d2dba126..e557c61054ef1d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -80,6 +80,7 @@ public class ViewProps { public static final String FONT_STYLE = "fontStyle"; public static final String FONT_FAMILY = "fontFamily"; public static final String LINE_HEIGHT = "lineHeight"; + public static final String LETTER_SPACING = "letterSpacing"; public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing"; public static final String NUMBER_OF_LINES = "numberOfLines"; public static final String ELLIPSIZE_MODE = "ellipsizeMode"; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java new file mode 100644 index 00000000000000..d4d57847fc77c1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.text; + +import android.annotation.TargetApi; +import android.os.Build; +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +import com.facebook.infer.annotation.Assertions; + +/** + * A {@link MetricAffectingSpan} that allows to set the letter spacing + * on the selected text span. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class CustomLetterSpacingSpan extends MetricAffectingSpan { + + private final float mLetterSpacing; + + public CustomLetterSpacingSpan(float letterSpacing) { + Assertions.assertCondition(!Float.isNaN(letterSpacing) && letterSpacing != 0); + mLetterSpacing = letterSpacing; + } + + @Override + public void updateDrawState(TextPaint paint) { + paint.setLetterSpacing(mLetterSpacing); + } + + @Override + public void updateMeasureState(TextPaint paint) { + paint.setLetterSpacing(mLetterSpacing); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index 4ceee69cf21d0c..bcc3276f95da23 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -180,6 +180,14 @@ private static void buildSpannedFromTextCSSNode( end, new CustomLineHeightSpan(textShadowNode.getEffectiveLineHeight()))); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (textShadowNode.mLetterSpacing != 0) { + ops.add(new SetSpanOperation( + start, + end, + new CustomLetterSpacingSpan(textShadowNode.mLetterSpacing))); + } + } ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textShadowNode.getReactTag()))); } } @@ -329,7 +337,26 @@ private static int parseNumericFontWeight(String fontWeightString) { 100 * (fontWeightString.charAt(0) - '0') : -1; } + /** + * {@link Android TextView#setLetterSpacing} expects the letter spacing value + * in 'EM' unit, where one is defined by the width of the letter 'M'. + * Typical values for slight expansion will be around 0.05. + * Negative values will tighten text. So we use NaN as not defined value. + * + * This method calculates an 'EM' approach based on the input + * and the calculated font size. With common fonts this calculated value + * renders a similar spacing and layout to iOS. + */ + private static float calculateLetterSpacing(float letterSpacingInput, int fontSize) { + if (Float.isNaN(letterSpacingInput) || letterSpacingInput == 0) { + return 0; + } else { + return letterSpacingInput / fontSize * 2f; + } + } + private float mLineHeight = Float.NaN; + protected float mLetterSpacing = 0; private boolean mIsColorSet = false; private boolean mAllowFontScaling = true; private int mColor; @@ -340,6 +367,7 @@ private static int parseNumericFontWeight(String fontWeightString) { protected int mFontSize = UNSET; protected float mFontSizeInput = UNSET; protected float mLineHeightInput = UNSET; + protected float mLetterSpacingInput = Float.NaN; protected int mTextAlign = Gravity.NO_GRAVITY; protected int mTextBreakStrategy = (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY; @@ -454,6 +482,13 @@ public void setLineHeight(float lineHeight) { markUpdated(); } + @ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = Float.NaN) + public void setLetterSpacing(float letterSpacing) { + mLetterSpacingInput = letterSpacing; + mLetterSpacing = calculateLetterSpacing(letterSpacing, mFontSize); + markUpdated(); + } + @ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true) public void setAllowFontScaling(boolean allowFontScaling) { if (allowFontScaling != mAllowFontScaling) { @@ -491,6 +526,7 @@ public void setFontSize(float fontSize) { : (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize)); } mFontSize = (int) fontSize; + mLetterSpacing = calculateLetterSpacing(mLetterSpacingInput, mFontSize); markUpdated(); } @@ -658,6 +694,7 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { getPadding(Spacing.TOP), getPadding(Spacing.END), getPadding(Spacing.BOTTOM), + mLetterSpacing, getTextAlign(), mTextBreakStrategy ); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java index 9f67aec6180dee..3ee797fa531f4f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextUpdate.java @@ -28,6 +28,7 @@ public class ReactTextUpdate { private final float mPaddingBottom; private final int mTextAlign; private final int mTextBreakStrategy; + private final float mLetterSpacing; /** * @deprecated Use a non-deprecated constructor for ReactTextUpdate instead. This one remains @@ -50,6 +51,7 @@ public ReactTextUpdate( paddingTop, paddingEnd, paddingBottom, + 0f, textAlign, Layout.BREAK_STRATEGY_HIGH_QUALITY); } @@ -62,6 +64,7 @@ public ReactTextUpdate( float paddingTop, float paddingEnd, float paddingBottom, + float letterSpacing, int textAlign, int textBreakStrategy) { mText = text; @@ -71,6 +74,7 @@ public ReactTextUpdate( mPaddingTop = paddingTop; mPaddingRight = paddingEnd; mPaddingBottom = paddingBottom; + mLetterSpacing = letterSpacing; mTextAlign = textAlign; mTextBreakStrategy = textBreakStrategy; } @@ -103,6 +107,10 @@ public float getPaddingBottom() { return mPaddingBottom; } + public float getLetterSpacing() { + return mLetterSpacing; + } + public int getTextAlign() { return mTextAlign; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index fa8f87710b1b8f..0a1b64b089b02b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -65,6 +65,16 @@ public void setText(ReactTextUpdate update) { (int) Math.floor(update.getPaddingRight()), (int) Math.floor(update.getPaddingBottom())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + float nextLetterSpacing = update.getLetterSpacing(); + if (Float.isNaN(nextLetterSpacing)) { + nextLetterSpacing = 0; + } + if (getLetterSpacing() != nextLetterSpacing) { + setLetterSpacing(nextLetterSpacing); + } + } + int nextTextAlign = update.getTextAlign(); if (mTextAlign != nextTextAlign) { mTextAlign = nextTextAlign; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java index 2e6c5493c920ed..04227b245de041 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java @@ -146,6 +146,7 @@ public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { getPadding(Spacing.TOP), getPadding(Spacing.RIGHT), getPadding(Spacing.BOTTOM), + mLetterSpacing, mTextAlign, mTextBreakStrategy );