diff --git a/RNTester/js/TextExample.android.js b/RNTester/js/TextExample.android.js
index a4579d9d931cce..0b0bf195f68fb7 100644
--- a/RNTester/js/TextExample.android.js
+++ b/RNTester/js/TextExample.android.js
@@ -341,6 +341,36 @@ 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
+
+
+
+ With size and background color
+
+
+
+ letterSpacing = -1
+
+
+ [letterSpacing = 3]
+
+ [Nested letterSpacing = 0]
+
+
+ [Nested letterSpacing = 6]
+
+
+
+
diff --git a/RNTester/js/TextExample.ios.js b/RNTester/js/TextExample.ios.js
index 4afcc78668e5ea..e5510390d0a353 100644
--- a/RNTester/js/TextExample.ios.js
+++ b/RNTester/js/TextExample.ios.js
@@ -575,9 +575,23 @@ exports.examples = [
letterSpacing = 9
+
+
+ With size and background color
+
+
letterSpacing = -1
+
+ [letterSpacing = 3]
+
+ [Nested letterSpacing = 0]
+
+
+ [Nested letterSpacing = 6]
+
+
);
},
diff --git a/RNTester/js/TextInputExample.android.js b/RNTester/js/TextInputExample.android.js
index 92843e3459f7fe..358d3fd043b02d 100644
--- a/RNTester/js/TextInputExample.android.js
+++ b/RNTester/js/TextInputExample.android.js
@@ -567,6 +567,31 @@ exports.examples = [
);
}
},
+ {
+ title: 'letterSpacing',
+ render: function() {
+ return (
+
+
+
+
+
+
+ );
+ }
+ },
{
title: 'Passwords',
render: function() {
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 ec703e8cf9e317..aee1a0c7d9a7ab 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java
@@ -88,6 +88,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..004d26138694e5
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/CustomLetterSpacingSpan.java
@@ -0,0 +1,53 @@
+/**
+ * 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.
+ *
+ * The letter spacing is specified in pixels, which are converted to
+ * ems at paint time; this span must therefore be applied after any
+ * spans affecting font size.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class CustomLetterSpacingSpan extends MetricAffectingSpan {
+
+ private final float mLetterSpacing;
+
+ public CustomLetterSpacingSpan(float letterSpacing) {
+ mLetterSpacing = letterSpacing;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint paint) {
+ apply(paint);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint);
+ }
+
+ private void apply(TextPaint paint) {
+ // mLetterSpacing and paint.getTextSize() are both in pixels,
+ // yielding an accurate em value.
+ if (!Float.isNaN(mLetterSpacing)) {
+ paint.setLetterSpacing(mLetterSpacing / paint.getTextSize());
+ }
+ }
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
index 9b73869b1b9b69..075c5be23edf3c 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java
@@ -118,6 +118,14 @@ private static void buildSpannedFromShadowNode(
new SetSpanOperation(
start, end, new BackgroundColorSpan(textShadowNode.mBackgroundColor)));
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (textShadowNode.mLetterSpacing != Float.NaN) {
+ ops.add(new SetSpanOperation(
+ start,
+ end,
+ new CustomLetterSpacingSpan(textShadowNode.mLetterSpacing)));
+ }
+ }
if (textShadowNode.mFontSize != UNSET) {
ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textShadowNode.mFontSize)));
}
@@ -228,6 +236,7 @@ private static int parseNumericFontWeight(String fontWeightString) {
}
protected float mLineHeight = Float.NaN;
+ protected float mLetterSpacing = Float.NaN;
protected boolean mIsColorSet = false;
protected boolean mAllowFontScaling = true;
protected int mColor;
@@ -238,6 +247,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;
@@ -356,12 +366,22 @@ public void setLineHeight(float lineHeight) {
markUpdated();
}
+ @ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = Float.NaN)
+ public void setLetterSpacing(float letterSpacing) {
+ mLetterSpacingInput = letterSpacing;
+ mLetterSpacing = mAllowFontScaling
+ ? PixelUtil.toPixelFromSP(mLetterSpacingInput)
+ : PixelUtil.toPixelFromDIP(mLetterSpacingInput);
+ markUpdated();
+ }
+
@ReactProp(name = ViewProps.ALLOW_FONT_SCALING, defaultBoolean = true)
public void setAllowFontScaling(boolean allowFontScaling) {
if (allowFontScaling != mAllowFontScaling) {
mAllowFontScaling = allowFontScaling;
setFontSize(mFontSizeInput);
setLineHeight(mLineHeightInput);
+ setLetterSpacing(mLetterSpacingInput);
markUpdated();
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
index e15dd5a7969cda..7a9f34e2a6a0dd 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java
@@ -33,6 +33,7 @@
import android.widget.EditText;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.views.text.CustomStyleSpan;
import com.facebook.react.views.text.ReactTagSpan;
@@ -80,6 +81,7 @@ public class ReactEditText extends EditText {
private @Nullable ScrollWatcher mScrollWatcher;
private final InternalKeyListener mKeyListener;
private boolean mDetectScrollMovement = false;
+ private float mLetterSpacingPt = 0;
private ReactViewBackgroundManager mReactBackgroundManager;
@@ -627,6 +629,29 @@ public void setBorderStyle(@Nullable String style) {
mReactBackgroundManager.setBorderStyle(style);
}
+ public void setLetterSpacingPt(float letterSpacingPt) {
+ mLetterSpacingPt = letterSpacingPt;
+ updateLetterSpacing();
+ }
+
+ @Override
+ public void setTextSize (float size) {
+ super.setTextSize(size);
+ updateLetterSpacing();
+ }
+
+ @Override
+ public void setTextSize (int unit, float size) {
+ super.setTextSize(unit, size);
+ updateLetterSpacing();
+ }
+
+ protected void updateLetterSpacing() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ setLetterSpacing(PixelUtil.toPixelFromSP(mLetterSpacingPt) / getTextSize());
+ }
+ }
+
/**
* This class will redirect *TextChanged calls to the listeners only in the case where the text
* is changed by the user, and not explicitly set by JS.
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
index bf27f485dca392..e4224eaac01016 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
@@ -306,6 +306,14 @@ public void setOnScroll(final ReactEditText view, boolean onScroll) {
}
}
+ // Sets the letter spacing as an absolute point size.
+ // This extra handling, on top of what ReactBaseTextShadowNode already does, is required for the
+ // correct display of spacing in placeholder (hint) text.
+ @ReactProp(name = ViewProps.LETTER_SPACING, defaultFloat = 0)
+ public void setLetterSpacing(ReactEditText view, float letterSpacing) {
+ view.setLetterSpacingPt(letterSpacing);
+ }
+
@ReactProp(name = "placeholder")
public void setPlaceholder(ReactEditText view, @Nullable String placeholder) {
view.setHint(placeholder);