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
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
private int mLinkifyMaskType;
private boolean mTextIsSelectable;
private boolean mShouldAdjustSpannableFontSize;
private boolean mAdjustedTextExceedsLineLimit;
private Overflow mOverflow = Overflow.VISIBLE;

private @Nullable Spannable mSpanned;
Expand Down Expand Up @@ -130,6 +131,7 @@ private void initView() {
mLinkifyMaskType = 0;
mTextIsSelectable = false;
mShouldAdjustSpannableFontSize = false;
mAdjustedTextExceedsLineLimit = false;
mEllipsizeLocation = TextUtils.TruncateAt.END;
mFontSize = Float.NaN;
mMinimumFontSize = Float.NaN;
Expand Down Expand Up @@ -232,22 +234,27 @@ protected void onDraw(Canvas canvas) {
Spannable spanned = getSpanned();
if (mAdjustsFontSizeToFit && spanned != null && mShouldAdjustSpannableFontSize) {
mShouldAdjustSpannableFontSize = false;
TextLayoutManager.adjustSpannableFontToFit(
spanned,
getWidth(),
YogaMeasureMode.EXACTLY,
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
getHyphenationFrequency(),
// always passing ALIGN_NORMAL here should be fine, since this method doesn't depend on
// how exactly lines are aligned, just their width
Layout.Alignment.ALIGN_NORMAL,
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? -1 : getJustificationMode(),
getPaint());
boolean exceedsLineLimit =
TextLayoutManager.adjustSpannableFontToFit(
spanned,
getWidth(),
YogaMeasureMode.EXACTLY,
getHeight(),
YogaMeasureMode.EXACTLY,
mMinimumFontSize,
mNumberOfLines,
getIncludeFontPadding(),
getBreakStrategy(),
getHyphenationFrequency(),
// always passing ALIGN_NORMAL here should be fine, since this method doesn't
// depend on how exactly lines are aligned, just their width
Layout.Alignment.ALIGN_NORMAL,
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? -1 : getJustificationMode(),
getPaint());
if (exceedsLineLimit != mAdjustedTextExceedsLineLimit) {
mAdjustedTextExceedsLineLimit = exceedsLineLimit;
updateView();
}
setText(spanned);
}

Expand Down Expand Up @@ -585,7 +592,8 @@ public void setEllipsizeLocation(@Nullable TextUtils.TruncateAt ellipsizeLocatio
public void updateView() {
@Nullable
TextUtils.TruncateAt ellipsizeLocation =
mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit
mNumberOfLines == ViewDefaults.NUMBER_OF_LINES
|| (mAdjustsFontSizeToFit && !mAdjustedTextExceedsLineLimit)
? null
: mEllipsizeLocation;
setEllipsize(ellipsizeLocation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,7 @@ internal object TextLayoutManager {
alignment: Layout.Alignment,
justificationMode: Int,
paint: TextPaint,
) {
): Boolean {
var boring = isBoring(text, paint)
var layout: Layout

Expand Down Expand Up @@ -1405,11 +1405,6 @@ internal object TextLayoutManager {
paint,
)

if (intervalStart == intervalEnd) {
// everything is updated at this point
break
}

val singleLineTextExceedsWidth = text.length == 1 && layout.getLineWidth(0) > width
val exceedsHeight =
heightYogaMeasureMode != YogaMeasureMode.UNDEFINED && layout.height > height
Expand All @@ -1418,6 +1413,11 @@ internal object TextLayoutManager {
maximumNumberOfLines != 0 &&
layout.lineCount > maximumNumberOfLines

if (intervalStart == intervalEnd) {
// everything is updated at this point
return exceedsMaximumNumberOfLines || singleLineTextExceedsWidth
}

if (
currentFontSize > minimumFontSize &&
(exceedsMaximumNumberOfLines || exceedsHeight || singleLineTextExceedsWidth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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
Expand Down Expand Up @@ -109,6 +110,37 @@ class ReactTextViewTest {
assertThat(view.useBoundsForWidth).isFalse()
}

@Test
fun adjustsFontSizeToFitEllipsizesOnlyWhileTextExceedsNumberOfLines() {
val text = SpannableString("Hello\nworld")
text.setSpan(ReactAbsoluteSizeSpan(40), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val view = TestReactTextView(RuntimeEnvironment.getApplication())
view.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
view.setTextColor(Color.BLACK)
view.setMinimumFontSize(20f)
view.setNumberOfLines(1)
view.setEllipsizeLocation(TextUtils.TruncateAt.END)
view.setAdjustFontSizeToFit(true)
view.setSpanned(text)
view.text = text
view.updateView()

// Two lines can never fit in one, so the ellipsize applies.
layoutAndDraw(view, 200, 60)
assertThat(view.ellipsize).isEqualTo(TextUtils.TruncateAt.END)

// Once two lines are allowed the text fits, so the ellipsize is dropped again.
view.setNumberOfLines(2)
view.updateView()
layoutAndDraw(view, 200, 60)
assertThat(view.ellipsize).isNull()
}

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,71 @@
/*
* 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.views.text.internal.span.ReactAbsoluteSizeSpan
import com.facebook.yoga.YogaMeasureMode
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class TextLayoutManagerAdjustSpannableFontToFitTest {

@Test
fun `reports that the line limit is not exceeded when the text fits`() {
val exceedsLineLimit = adjustToFit("Hello world", maximumNumberOfLines = 1)

assertThat(exceedsLineLimit).isFalse()
}

@Test
fun `reports that the line limit is exceeded when the text has more lines at the minimum size`() {
val exceedsLineLimit = adjustToFit("Hello\nworld", maximumNumberOfLines = 1)

assertThat(exceedsLineLimit).isTrue()
}

@Test
fun `reports that the line limit is not exceeded when only the height is exceeded`() {
val exceedsLineLimit =
adjustToFit("Hello world", height = 1f, maximumNumberOfLines = ReactConstants.UNSET)

assertThat(exceedsLineLimit).isFalse()
}

private fun adjustToFit(
string: String,
height: Float = 10_000f,
maximumNumberOfLines: Int,
): Boolean {
val text = SpannableString(string)
text.setSpan(ReactAbsoluteSizeSpan(40), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

return TextLayoutManager.adjustSpannableFontToFit(
text,
10_000f,
YogaMeasureMode.EXACTLY,
height,
YogaMeasureMode.EXACTLY,
20f,
maximumNumberOfLines,
true,
Layout.BREAK_STRATEGY_SIMPLE,
Layout.HYPHENATION_FREQUENCY_NONE,
Layout.Alignment.ALIGN_NORMAL,
0,
TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 40f },
)
}
}
Loading