Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/react-native/Libraries/Text/TextProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,6 @@ export type TextPropsAndroid = {
/**
* Smallest possible font scale when `adjustsFontSizeToFit` is enabled
* (values 0.01-1.0).
*
* @platform ios
*/
minimumFontScale?: ?number,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6044,7 +6044,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi
public fun setIncludeFontPadding (Z)V
public fun setLetterSpacing (F)V
public fun setLinkifyMask (I)V
public fun setMinimumFontSize (F)V
public fun setMinimumFontScale (F)V
public fun setNumberOfLines (I)V
public fun setOverflow (Ljava/lang/String;)V
public fun setSpanned (Landroid/text/Spannable;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
private @Nullable TextUtils.TruncateAt mEllipsizeLocation;
private boolean mAdjustsFontSizeToFit;
private float mFontSize;
private float mMinimumFontSize;
private float mMinimumFontScale;
private float mLetterSpacing;
private int mLinkifyMaskType;
private boolean mTextIsSelectable;
Expand Down Expand Up @@ -132,7 +132,7 @@ private void initView() {
mShouldAdjustSpannableFontSize = false;
mEllipsizeLocation = TextUtils.TruncateAt.END;
mFontSize = Float.NaN;
mMinimumFontSize = Float.NaN;
mMinimumFontScale = Float.NaN;
mLetterSpacing = 0.f;
mOverflow = Overflow.VISIBLE;
mSpanned = null;
Expand Down Expand Up @@ -238,7 +238,7 @@ protected void onDraw(Canvas canvas) {
YogaMeasureMode.EXACTLY,
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mMinimumFontScale,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
Expand Down Expand Up @@ -540,8 +540,8 @@ public void setFontSize(float fontSize) {
applyTextAttributes();
}

public void setMinimumFontSize(float minimumFontSize) {
mMinimumFontSize = minimumFontSize;
public void setMinimumFontScale(float minimumFontScale) {
mMinimumFontScale = minimumFontScale;
mShouldAdjustSpannableFontSize = true;
}

Expand Down Expand Up @@ -585,9 +585,7 @@ public void setEllipsizeLocation(@Nullable TextUtils.TruncateAt ellipsizeLocatio
public void updateView() {
@Nullable
TextUtils.TruncateAt ellipsizeLocation =
mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit
? null
: mEllipsizeLocation;
mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation;
setEllipsize(ellipsizeLocation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ public constructor(
)
view.setSpanned(spanned)

val minimumFontSize: Float =
paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SIZE).toFloat()
view.setMinimumFontSize(minimumFontSize)
val minimumFontScale: Float =
paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SCALE).toFloat()
view.setMinimumFontScale(minimumFontScale)

// Clear any stale PreparedLayout from a previous update
view.setPreparedLayout(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ internal object TextLayoutManager {
const val PA_KEY_ADJUST_FONT_SIZE_TO_FIT: Int = 3
const val PA_KEY_INCLUDE_FONT_PADDING: Int = 4
const val PA_KEY_HYPHENATION_FREQUENCY: Int = 5
const val PA_KEY_MINIMUM_FONT_SIZE: Int = 6
const val PA_KEY_MAXIMUM_FONT_SIZE: Int = 7
const val PA_KEY_MINIMUM_FONT_SCALE: Int = 6
const val PA_KEY_TEXT_ALIGN_VERTICAL: Int = 8
const val PA_KEY_TEXT_WIDTH_MODE: Int = 9

Expand Down Expand Up @@ -1046,9 +1045,9 @@ internal object TextLayoutManager {
val justificationMode = getTextJustificationMode(alignmentAttr)

if (adjustFontSizeToFit) {
val minimumFontSize =
if (paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE))
paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE).toFloat()
val minimumFontScale =
if (paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SCALE))
paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SCALE).toFloat()
else Float.NaN

adjustSpannableFontToFit(
Expand All @@ -1057,7 +1056,7 @@ internal object TextLayoutManager {
YogaMeasureMode.EXACTLY,
height,
heightYogaMeasureMode,
minimumFontSize,
minimumFontScale,
maximumNumberOfLines,
includeFontPadding,
textBreakStrategy,
Expand Down Expand Up @@ -1209,7 +1208,7 @@ internal object TextLayoutManager {
widthYogaMeasureMode: YogaMeasureMode,
height: Float,
heightYogaMeasureMode: YogaMeasureMode,
minimumFontSizeAttr: Float,
minimumFontScale: Float,
maximumNumberOfLines: Int,
includeFontPadding: Boolean,
textBreakStrategy: Int,
Expand All @@ -1221,17 +1220,21 @@ internal object TextLayoutManager {
var boring = isBoring(text, paint)
var layout: Layout

// Minimum font size is 4pts to match the iOS implementation.
val minimumFontSize =
(if (minimumFontSizeAttr.isNaN()) 4.dpToPx() else minimumFontSizeAttr).toInt()

// Find the largest font size used in the spannable to use as a starting point.
var currentFontSize = minimumFontSize
var currentFontSize = 0
val spans = text.getSpans(0, text.length, ReactAbsoluteSizeSpan::class.java)
for (span in spans) {
currentFontSize = max(currentFontSize, span.size)
}

// The smallest font size is the largest font size scaled by minimumFontScale, floored at 4dp
// to match the iOS implementation.
val absoluteMinimumFontSize = 4.dpToPx().toInt()
val minimumFontSize =
if (minimumFontScale.isNaN() || minimumFontScale <= 0f) absoluteMinimumFontSize
else max((minimumFontScale * currentFontSize).toInt(), absoluteMinimumFontSize)
currentFontSize = max(currentFontSize, minimumFontSize)

var intervalStart = minimumFontSize
var intervalEnd = currentFontSize
var previousFontSize = currentFontSize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ import android.graphics.Color
import android.graphics.Paint
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils
import android.text.style.ReplacementSpan
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.createBitmap
import androidx.core.graphics.get
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
Expand All @@ -32,6 +36,16 @@ import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
class ReactTextViewTest {

@Before
fun setUp() {
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication())
}

@After
fun tearDown() {
DisplayMetricsHolder.setScreenDisplayMetrics(null)
}

@Test
fun drawsGlyphInkOutsideLineHeightWhenOverflowIsVisible() {
val bitmap = drawReactTextViewWithOverflow(null)
Expand Down Expand Up @@ -70,7 +84,7 @@ class ReactTextViewTest {
ViewGroup.LayoutParams.WRAP_CONTENT,
)
view.setTextColor(Color.BLACK)
view.setMinimumFontSize(4f)
view.setMinimumFontScale(0.1f)
view.setNumberOfLines(0)
view.setAdjustFontSizeToFit(true)
view.setSpanned(text)
Expand Down Expand Up @@ -109,6 +123,18 @@ class ReactTextViewTest {
assertThat(view.useBoundsForWidth).isFalse()
}

@Test
fun adjustsFontSizeToFitKeepsEllipsizeLocation() {
val view = TestReactTextView(RuntimeEnvironment.getApplication())
view.setNumberOfLines(1)
view.setEllipsizeLocation(TextUtils.TruncateAt.END)
view.setAdjustFontSizeToFit(true)

view.updateView()

assertThat(view.ellipsize).isEqualTo(TextUtils.TruncateAt.END)
}

private fun layoutAndDraw(view: TestReactTextView, width: Int, height: Int) {
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright (c) Meta Platforms, Inc. and 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.text.Layout
import android.text.SpannableString
import android.text.Spanned
import android.text.TextPaint
import com.facebook.react.common.ReactConstants
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan
import com.facebook.yoga.YogaMeasureMode
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment

@RunWith(RobolectricTestRunner::class)
class TextLayoutManagerMinimumFontScaleTest {

@Before
fun setUp() {
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication())
}

@After
fun tearDown() {
DisplayMetricsHolder.setScreenDisplayMetrics(null)
}

@Test
fun `minimumFontScale limits how far the font shrinks relative to the largest font size`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

adjustToUnsatisfiableHeight(text, minimumFontScale = 0.5f)

assertThat(largestFontSize(text)).isEqualTo((LARGE_FONT_SIZE * 0.5f).toInt())
}

@Test
fun `minimumFontScale is applied to the largest font size in the spannable`() {
val text = SpannableString("Small text and LARGE TEXT")
text.setSpan(ReactAbsoluteSizeSpan(SMALL_FONT_SIZE), 0, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
text.setSpan(
ReactAbsoluteSizeSpan(LARGE_FONT_SIZE),
15,
text.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)

adjustToUnsatisfiableHeight(text, minimumFontScale = 0.5f)

assertThat(largestFontSize(text)).isEqualTo((LARGE_FONT_SIZE * 0.5f).toInt())
}

@Test
fun `missing minimumFontScale shrinks down to the 4dp floor`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

adjustToUnsatisfiableHeight(text, minimumFontScale = Float.NaN)

assertThat(largestFontSize(text)).isEqualTo(4.dpToPx().toInt())
}

@Test
fun `zero minimumFontScale shrinks down to the 4dp floor`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

adjustToUnsatisfiableHeight(text, minimumFontScale = 0f)

assertThat(largestFontSize(text)).isEqualTo(4.dpToPx().toInt())
}

@Test
fun `minimumFontScale never shrinks below the 4dp floor`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

adjustToUnsatisfiableHeight(text, minimumFontScale = 0.01f)

assertThat(largestFontSize(text)).isEqualTo(4.dpToPx().toInt())
}

@Test
fun `text that already fits is not shrunk`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

TextLayoutManager.adjustSpannableFontToFit(
text,
10_000f,
YogaMeasureMode.EXACTLY,
10_000f,
YogaMeasureMode.EXACTLY,
0.5f,
ReactConstants.UNSET,
true,
Layout.BREAK_STRATEGY_SIMPLE,
Layout.HYPHENATION_FREQUENCY_NONE,
Layout.Alignment.ALIGN_NORMAL,
0,
newPaint(),
)

assertThat(largestFontSize(text)).isEqualTo(LARGE_FONT_SIZE)
}

// Uses a height no font size can satisfy so the text is shrunk all the way to the minimum.
private fun adjustToUnsatisfiableHeight(text: SpannableString, minimumFontScale: Float) {
TextLayoutManager.adjustSpannableFontToFit(
text,
10_000f,
YogaMeasureMode.EXACTLY,
1f,
YogaMeasureMode.EXACTLY,
minimumFontScale,
ReactConstants.UNSET,
true,
Layout.BREAK_STRATEGY_SIMPLE,
Layout.HYPHENATION_FREQUENCY_NONE,
Layout.Alignment.ALIGN_NORMAL,
0,
newPaint(),
)
}

private fun spannableWithFontSize(fontSize: Int): SpannableString {
val text = SpannableString("Hello")
text.setSpan(ReactAbsoluteSizeSpan(fontSize), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return text
}

private fun newPaint(): TextPaint =
TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = LARGE_FONT_SIZE.toFloat() }

private fun largestFontSize(text: Spanned): Int =
text.getSpans(0, text.length, ReactAbsoluteSizeSpan::class.java).maxOfOrNull { it.size } ?: 0

private companion object {
const val SMALL_FONT_SIZE = 10
const val LARGE_FONT_SIZE = 40
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const {
rhs.includeFontPadding,
rhs.android_hyphenationFrequency,
rhs.textAlignVertical) &&
floatEquality(minimumFontSize, rhs.minimumFontSize) &&
floatEquality(maximumFontSize, rhs.maximumFontSize) &&
floatEquality(minimumFontScale, rhs.minimumFontScale);
}

Expand All @@ -61,13 +59,9 @@ SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const {
adjustsFontSizeToFit,
paragraphAttributes.adjustsFontSizeToFit),
debugStringConvertibleItem(
"minimumFontSize",
minimumFontSize,
paragraphAttributes.minimumFontSize),
debugStringConvertibleItem(
"maximumFontSize",
maximumFontSize,
paragraphAttributes.maximumFontSize),
"minimumFontScale",
minimumFontScale,
paragraphAttributes.minimumFontScale),
debugStringConvertibleItem(
"includeFontPadding",
includeFontPadding,
Expand Down
Loading
Loading