From d2b598f36f4ac8dd38c6dc570345bda0ccababa4 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Mon, 28 Sep 2026 07:01:44 -0700 Subject: [PATCH] Use Android 15 text APIs directly Summary: ReactAndroid now compiles the text layout implementation against Android 15 or later. Call the platform glyph-bounds and start-overhang APIs directly instead of resolving and invoking them through reflection. Changelog: [Internal] Differential Revision: D122079801 --- .../react/views/text/TextLayoutManager.kt | 75 ++----------------- 1 file changed, 5 insertions(+), 70 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 53a67377158f..6cf683d666a8 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 @@ -9,7 +9,6 @@ package com.facebook.react.views.text import android.content.res.AssetManager import android.graphics.Color -import android.graphics.RectF import android.graphics.Typeface import android.os.Build import android.text.BoringLayout @@ -42,7 +41,6 @@ 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 @@ -116,69 +114,6 @@ internal object TextLayoutManager { private val tagToSpannableCache = ConcurrentHashMap() - // These wrappers mirror Android 15 APIs but use reflection because some internal targets still - // compile against Android 14. They return null when the API is unavailable or cannot be invoked. - private val setUseBoundsForWidthMethod: java.lang.reflect.Method? by lazy { - try { - StaticLayout.Builder::class - .java - .getMethod("setUseBoundsForWidth", Boolean::class.javaPrimitiveType) - } catch (_: ReflectiveOperationException) { - null - } - } - - private fun setUseBoundsForWidth( - builder: StaticLayout.Builder, - useBoundsForWidth: Boolean, - ): StaticLayout.Builder? = - try { - setUseBoundsForWidthMethod?.invoke(builder, useBoundsForWidth) as? StaticLayout.Builder - } catch (_: ReflectiveOperationException) { - null - } - - private val setShiftDrawingOffsetForStartOverhangMethod: java.lang.reflect.Method? by lazy { - try { - StaticLayout.Builder::class - .java - .getMethod( - "setShiftDrawingOffsetForStartOverhang", - Boolean::class.javaPrimitiveType, - ) - } catch (_: ReflectiveOperationException) { - null - } - } - - private fun setShiftDrawingOffsetForStartOverhang( - builder: StaticLayout.Builder, - shiftDrawingOffsetForStartOverhang: Boolean, - ): StaticLayout.Builder? = - try { - setShiftDrawingOffsetForStartOverhangMethod?.invoke( - builder, - shiftDrawingOffsetForStartOverhang, - ) as? StaticLayout.Builder - } catch (_: ReflectiveOperationException) { - null - } - - private val computeDrawingBoundingBoxMethod: java.lang.reflect.Method? by lazy { - try { - Layout::class.java.getMethod("computeDrawingBoundingBox") - } catch (_: ReflectiveOperationException) { - null - } - } - - private fun computeDrawingBoundingBox(layout: Layout): RectF? = - try { - computeDrawingBoundingBoxMethod?.invoke(layout) as? RectF - } catch (_: ReflectiveOperationException) { - null - } - fun setCachedSpannableForTag(reactTag: Int, sp: Spannable) { tagToSpannableCache[reactTag] = sp } @@ -940,7 +875,7 @@ internal object TextLayoutManager { @VisibleForTesting internal fun getRtlRightOverhang(layout: Layout): Int { if ( - Build.VERSION.SDK_INT < VERSION_CODE_VANILLA_ICE_CREAM || + Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM || layout.lineCount == 0 || (0 until layout.lineCount).any { layout.getParagraphDirection(it) != Layout.DIR_RIGHT_TO_LEFT @@ -949,7 +884,7 @@ internal object TextLayoutManager { return 0 } - val drawingBounds = computeDrawingBoundingBox(layout) ?: return 0 + val drawingBounds = layout.computeDrawingBoundingBox() return ceil(drawingBounds.right - layout.width).toInt().coerceAtLeast(0) } @@ -990,9 +925,9 @@ internal object TextLayoutManager { // Android shifts negative (left-side) start overhang itself. RTL start overhang is on the // right, so createLayout reserves that space in a second pass while preserving the EXACT Yoga // measurement returned to the caller. - if (Build.VERSION.SDK_INT >= VERSION_CODE_VANILLA_ICE_CREAM) { - setUseBoundsForWidth(builder, enableStartOverhang) - setShiftDrawingOffsetForStartOverhang(builder, enableStartOverhang) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + builder.setUseBoundsForWidth(enableStartOverhang) + builder.setShiftDrawingOffsetForStartOverhang(enableStartOverhang) } return builder.build()