Skip to content
Open
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 @@ -902,21 +902,7 @@ internal object TextLayoutManager {
paint: TextPaint,
): Unit {
var boring = isBoring(text, paint)
var layout =
createLayout(
text,
boring,
width,
widthYogaMeasureMode,
includeFontPadding,
textBreakStrategy,
hyphenationFrequency,
alignment,
justificationMode,
null,
ReactConstants.UNSET,
paint,
)
var layout: Layout

// Minimum font size is 4pts to match the iOS implementation.
val minimumFontSize =
Expand All @@ -929,20 +915,20 @@ internal object TextLayoutManager {
currentFontSize = max(currentFontSize, span.size).toInt()
}

val initialFontSize = currentFontSize
while (
currentFontSize > minimumFontSize &&
((maximumNumberOfLines != ReactConstants.UNSET &&
maximumNumberOfLines != 0 &&
layout.lineCount > maximumNumberOfLines) ||
(heightYogaMeasureMode != YogaMeasureMode.UNDEFINED && layout.height > height) ||
(text.length == 1 && layout.getLineWidth(0) > width))
) {
// TODO: We could probably use a smarter algorithm here. This will require 0(n)
// measurements based on the number of points the font size needs to be reduced by.
currentFontSize -= max(1, 1.dpToPx().toInt())
var intervalStart = minimumFontSize
var intervalEnd = currentFontSize
var previousFontSize = currentFontSize

val ratio = currentFontSize.toFloat() / initialFontSize.toFloat()
// `true` instead of `intervalStart != intervalEnd` so that the last iteration where both are at
// the same size goes through and updates all relevant objects with the final font size
while (true) {
// Always use the point closer to the end of the interval, this way at the end when
// end - start == 1, we land at current = end instead of current = start. In the first case
// one measurement may be enough if intervalEnd is small enough to fit. In the second case
// we always end up doing two measurements to check whether intervalEnd would fit.
val currentFontSize = (intervalStart + intervalEnd + 1) / 2

val ratio = currentFontSize.toFloat() / previousFontSize.toFloat()
paint.textSize = max((paint.textSize * ratio).toInt(), minimumFontSize).toFloat()

val sizeSpans = text.getSpans(0, text.length, ReactAbsoluteSizeSpan::class.java)
Expand Down Expand Up @@ -973,6 +959,34 @@ internal object TextLayoutManager {
ReactConstants.UNSET,
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
val exceedsMaximumNumberOfLines =
maximumNumberOfLines != ReactConstants.UNSET &&
maximumNumberOfLines != 0 &&
layout.lineCount > maximumNumberOfLines

if (
currentFontSize > minimumFontSize &&
(exceedsMaximumNumberOfLines || exceedsHeight || singleLineTextExceedsWidth)
) {
// Text doesn't fit the constraints. If intervalEnd - intervalStart == 1, it's known that
// the correct font size is intervalStart. Set intervalEnd to match intervalStart and do one
// more iteration to update layout correctly.
intervalEnd = if (intervalEnd - intervalStart == 1) intervalStart else currentFontSize
} else {
// Text fits the constraints
intervalStart = currentFontSize
}

previousFontSize = currentFontSize
}
}

Expand Down