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 @@ -40,6 +40,7 @@ import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.ReactAccessibilityDelegate
import com.facebook.react.util.AndroidVersion.VERSION_CODE_VANILLA_ICE_CREAM
import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan
import com.facebook.react.views.text.internal.span.CustomStyleSpan
Expand Down Expand Up @@ -764,13 +765,32 @@ internal object TextLayoutManager {
)
}

val desiredWidth = ceil(Layout.getDesiredWidth(text, paint)).toInt()

val layoutWidth =
when (widthYogaMeasureMode) {
YogaMeasureMode.EXACTLY -> ceil(width).toInt()
YogaMeasureMode.AT_MOST -> min(desiredWidth, floor(width).toInt())
else -> desiredWidth
YogaMeasureMode.AT_MOST ->
min(
getDesiredWidth(
text,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency,
alignment,
justificationMode,
paint,
),
floor(width).toInt(),
)
else ->
getDesiredWidth(
text,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency,
alignment,
justificationMode,
paint,
)
}
return buildLayout(
text,
Expand All @@ -786,6 +806,47 @@ internal object TextLayoutManager {
)
}

private fun getDesiredWidth(
text: Spannable,
includeFontPadding: Boolean,
textBreakStrategy: Int,
hyphenationFrequency: Int,
alignment: Layout.Alignment,
justificationMode: Int,
paint: TextPaint,
): Int {
val advanceWidth = ceil(Layout.getDesiredWidth(text, paint)).toInt()

if (
Build.VERSION.SDK_INT < VERSION_CODE_VANILLA_ICE_CREAM ||
setUseBoundsForWidthMethod == null
) {
return advanceWidth
}

val visualBoundsLayout =
buildLayout(
text,
Int.MAX_VALUE / 2,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency,
alignment,
justificationMode,
null,
ReactConstants.UNSET,
paint,
useBoundsForWidth = true,
)

var visualBoundsWidth = 0f
for (i in 0 until visualBoundsLayout.lineCount) {
visualBoundsWidth = max(visualBoundsWidth, visualBoundsLayout.getLineMax(i))
}

return max(advanceWidth, ceil(visualBoundsWidth).toInt())
}

private fun buildLayout(
text: Spannable,
layoutWidth: Int,
Expand All @@ -797,6 +858,7 @@ internal object TextLayoutManager {
ellipsizeMode: TextUtils.TruncateAt?,
maxNumberOfLines: Int,
paint: TextPaint,
useBoundsForWidth: Boolean = false,
): Layout {
val builder =
StaticLayout.Builder.obtain(text, 0, text.length, paint, layoutWidth)
Expand All @@ -818,6 +880,10 @@ internal object TextLayoutManager {
builder.setUseLineSpacingFromFallbacks(true)
}

if (useBoundsForWidth) {
setUseBoundsForWidthMethod?.invoke(builder, true)
}

return builder.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import android.text.TextPaint
import android.text.TextUtils
import com.facebook.yoga.YogaMeasureMode
import kotlin.math.ceil
import kotlin.math.floor
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.RobolectricTestRunner

/**
Expand Down Expand Up @@ -77,6 +79,42 @@ class TextLayoutManagerAbsoluteLayoutWithFractionalPixelTest {
.isGreaterThanOrEqualTo(renderedLineWidth)
}

@Test
@Config(sdk = [35])
fun `Android 15 EXACTLY mode keeps the width provided by Yoga`() {
val text = SpannableString("First line\nSecond line")
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 16f }
val exactWidth = 48.5f

val layout = invokeCreateLayout(text, exactWidth, paint)

assertThat(layout.width).isEqualTo(ceil(exactWidth).toInt())
}

@Test
@Config(sdk = [35])
fun `Android 15 AT_MOST mode does not exceed the width provided by Yoga`() {
val text = SpannableString("Prison Break")
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 16f }
val maxWidth = 8.5f

val layout = invokeCreateLayout(text, maxWidth, paint, YogaMeasureMode.AT_MOST)

assertThat(layout.width).isLessThanOrEqualTo(floor(maxWidth).toInt())
}

@Test
@Config(sdk = [35])
fun `Android 15 UNDEFINED mode does not shrink below advance width`() {
val text = SpannableString("Prison Break")
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 16f }
val advanceWidth = ceil(Layout.getDesiredWidth(text, paint)).toInt()

val layout = invokeCreateLayout(text, 0f, paint, YogaMeasureMode.UNDEFINED)

assertThat(layout.width).isGreaterThanOrEqualTo(advanceWidth)
}

/**
* Invokes the private TextLayoutManager.createLayout via reflection. We can't call it directly
* because it's `private` (friend_paths only opens up `internal`). Default values mirror what
Expand All @@ -90,6 +128,7 @@ class TextLayoutManagerAbsoluteLayoutWithFractionalPixelTest {
text: SpannableString,
width: Float,
paint: TextPaint,
widthYogaMeasureMode: YogaMeasureMode = YogaMeasureMode.EXACTLY,
): Layout {
val boring: BoringLayout.Metrics? = BoringLayout.isBoring(text, paint)
val method =
Expand Down Expand Up @@ -117,7 +156,7 @@ class TextLayoutManagerAbsoluteLayoutWithFractionalPixelTest {
text,
boring,
width,
YogaMeasureMode.EXACTLY,
widthYogaMeasureMode,
/* includeFontPadding = */ false,
/* textBreakStrategy = */ Layout.BREAK_STRATEGY_HIGH_QUALITY,
/* hyphenationFrequency = */ Layout.HYPHENATION_FREQUENCY_NONE,
Expand Down
Loading