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
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6040,6 +6040,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 setMinimumFontScale (F)V
public fun setMinimumFontSize (F)V
public fun setNumberOfLines (I)V
public fun setOverflow (Ljava/lang/String;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
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 @@ -133,6 +134,7 @@ private void initView() {
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 @@ -239,6 +241,7 @@ protected void onDraw(Canvas canvas) {
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mMinimumFontScale,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
Expand Down Expand Up @@ -540,11 +543,20 @@ public void setFontSize(float fontSize) {
applyTextAttributes();
}

/**
* @deprecated Use {@link #setMinimumFontScale(float)} instead.
*/
@Deprecated
public void setMinimumFontSize(float minimumFontSize) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a public API change - please avoid. Can you add back the old API as deprecated?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setMinimumFontSize() was added back as deprecated, and setMinimumFontScale() was added alongside it. minimumFontSize still takes precedence when explicitly set, so existing callers keep the old behavior.

mMinimumFontSize = minimumFontSize;
mShouldAdjustSpannableFontSize = true;
}

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

@Override
public void setIncludeFontPadding(boolean includepad) {
super.setIncludeFontPadding(includepad);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ public constructor(

val minimumFontSize: Float =
paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SIZE).toFloat()
view.setMinimumFontSize(minimumFontSize)
@Suppress("DEPRECATION") view.setMinimumFontSize(minimumFontSize)
val minimumFontScale: Float =
if (paragraphAttributes.contains(TextLayoutManager.PA_KEY_MINIMUM_FONT_SCALE))
paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SCALE).toFloat()
else Float.NaN
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 @@ -98,6 +98,7 @@ internal object TextLayoutManager {
const val PA_KEY_MINIMUM_FONT_SIZE: Int = 6
const val PA_KEY_TEXT_ALIGN_VERTICAL: Int = 8
const val PA_KEY_TEXT_WIDTH_MODE: Int = 9
Comment on lines 99 to 100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: re-order

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been fixed.

const val PA_KEY_MINIMUM_FONT_SCALE: Int = 10

private val TAG: String = TextLayoutManager::class.java.simpleName

Expand Down Expand Up @@ -1171,6 +1172,10 @@ internal object TextLayoutManager {
if (paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE))
paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE).toFloat()
else Float.NaN
val minimumFontScale =
if (paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SCALE))
paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SCALE).toFloat()
else Float.NaN

adjustSpannableFontToFit(
text,
Expand All @@ -1179,6 +1184,7 @@ internal object TextLayoutManager {
height,
heightYogaMeasureMode,
minimumFontSize,
minimumFontScale,
maximumNumberOfLines,
includeFontPadding,
textBreakStrategy,
Expand Down Expand Up @@ -1338,6 +1344,7 @@ internal object TextLayoutManager {
height: Float,
heightYogaMeasureMode: YogaMeasureMode,
minimumFontSizeAttr: Float,
minimumFontScale: Float,
maximumNumberOfLines: Int,
includeFontPadding: Boolean,
textBreakStrategy: Int,
Expand All @@ -1349,17 +1356,25 @@ 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)
}

// An explicit minimum font size wins over minimumFontScale, which is applied to the largest
// font size in the spannable. The 4dp floor matches the iOS implementation.
val absoluteMinimumFontSize = 4.dpToPx().toInt()
val minimumFontSize =
when {
!minimumFontSizeAttr.isNaN() -> minimumFontSizeAttr.toInt()
!minimumFontScale.isNaN() && minimumFontScale > 0f ->
max((minimumFontScale * currentFontSize).toInt(), absoluteMinimumFontSize)
else -> 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 @@ -21,8 +21,11 @@ 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 +35,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 +83,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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 `explicit minimumFontSize is used as the floor`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

adjustToUnsatisfiableHeight(text, minimumFontSize = 12f, minimumFontScale = Float.NaN)

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

@Test
fun `explicit minimumFontSize takes precedence over minimumFontScale`() {
val text = spannableWithFontSize(LARGE_FONT_SIZE)

adjustToUnsatisfiableHeight(text, minimumFontSize = 12f, minimumFontScale = 0.5f)

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

@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,
Float.NaN,
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,
minimumFontSize: Float = Float.NaN,
) {
TextLayoutManager.adjustSpannableFontToFit(
text,
10_000f,
YogaMeasureMode.EXACTLY,
1f,
YogaMeasureMode.EXACTLY,
minimumFontSize,
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 @@ -59,6 +59,10 @@ SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const {
"adjustsFontSizeToFit",
adjustsFontSizeToFit,
paragraphAttributes.adjustsFontSizeToFit),
debugStringConvertibleItem(
"minimumFontScale",
minimumFontScale,
paragraphAttributes.minimumFontScale),
debugStringConvertibleItem(
"minimumFontSize",
minimumFontSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class ParagraphAttributes : public DebugStringConvertible {

/*
* In case of font size adjustment enabled, defines the minimum font size.
* Deprecated in favor of minimumFontScale.
*/
Float minimumFontSize{std::numeric_limits<Float>::quiet_NaN()};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@ constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5;
constexpr static MapBuffer::Key PA_KEY_MINIMUM_FONT_SIZE = 6;
constexpr static MapBuffer::Key PA_KEY_TEXT_ALIGN_VERTICAL = 8;
constexpr static MapBuffer::Key PA_KEY_TEXT_WIDTH_MODE = 9;
constexpr static MapBuffer::Key PA_KEY_MINIMUM_FONT_SCALE = 10;

inline MapBuffer toMapBuffer(const ParagraphAttributes &paragraphAttributes)
{
Expand All @@ -1211,6 +1212,7 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes &paragraphAttributes)
builder.putString(PA_KEY_TEXT_ALIGN_VERTICAL, toString(*paragraphAttributes.textAlignVertical));
}
builder.putDouble(PA_KEY_MINIMUM_FONT_SIZE, paragraphAttributes.minimumFontSize);
builder.putDouble(PA_KEY_MINIMUM_FONT_SCALE, paragraphAttributes.minimumFontScale);

return builder.build();
}
Expand Down
Loading
Loading