From 5cefe0b999a974b78afcde84b0df5d25e38624c7 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 2 Dec 2025 00:26:38 -0800 Subject: [PATCH] Update Android's adjustsFontSizeToFit algorithm (#54715) Summary: Changelog: [Android][Fixed] Fixed `adjustsFontSizeToFit` scaling the font size too much down Fixes https://github.com/facebook/react-native/issues/54356 Previous implementation was always using the starting font size as base for calculating ratio and was applying it to the already scaled down font sizes in each iteration. This caused the text size to be scaled down too aggresively. In this diff I changed the implementation to use the font size from the previous iteration for calculating the scale ratio to fix that. I also updated it to use binary seach instead of linear search for finding the largest fitting font size. Differential Revision: D87973778 --- .../react/views/text/TextLayoutManager.kt | 70 +++++++++++-------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index 5a893fdd2b58..d02eaa0bb052 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -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 = @@ -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) @@ -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 } }