From c5eb21123b6ce64b0290c19b1efead2d9d1793cc Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Fri, 25 Sep 2026 13:57:19 -0700 Subject: [PATCH] Encapsulate Android 15 text-layout reflection (#58681) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58681 Wrap the three Android 15 text-layout APIs behind typed private helpers that mirror their platform signatures. This keeps reflection isolated and makes replacing each helper with a direct API call a local change once all targets compile against Android 15 or later. Changelog: [Internal] ___ Differential Revision: D121810388 fbshipit-source-id: d46ac4fd38c4151564fd3f868883fcd470a75924 --- .../react/views/text/TextLayoutManager.kt | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 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 8e4d2665bd1..53a67377158 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 @@ -116,8 +116,8 @@ internal object TextLayoutManager { private val tagToSpannableCache = ConcurrentHashMap() - // Lazily cached methods for showing glyph ink that overhangs the start of a line (API 35+). - // Reflection is needed because some internal targets compile against an SDK older than 35. + // 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 @@ -128,6 +128,16 @@ internal object TextLayoutManager { } } + 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 @@ -141,6 +151,19 @@ internal object TextLayoutManager { } } + 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") @@ -149,6 +172,13 @@ internal object TextLayoutManager { } } + private fun computeDrawingBoundingBox(layout: Layout): RectF? = + try { + computeDrawingBoundingBoxMethod?.invoke(layout) as? RectF + } catch (_: ReflectiveOperationException) { + null + } + fun setCachedSpannableForTag(reactTag: Int, sp: Spannable) { tagToSpannableCache[reactTag] = sp } @@ -919,12 +949,7 @@ internal object TextLayoutManager { return 0 } - val drawingBounds = - try { - computeDrawingBoundingBoxMethod?.invoke(layout) as? RectF - } catch (_: ReflectiveOperationException) { - null - } ?: return 0 + val drawingBounds = computeDrawingBoundingBox(layout) ?: return 0 return ceil(drawingBounds.right - layout.width).toInt().coerceAtLeast(0) } @@ -966,8 +991,8 @@ internal object TextLayoutManager { // 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) { - setUseBoundsForWidthMethod?.invoke(builder, enableStartOverhang) - setShiftDrawingOffsetForStartOverhangMethod?.invoke(builder, enableStartOverhang) + setUseBoundsForWidth(builder, enableStartOverhang) + setShiftDrawingOffsetForStartOverhang(builder, enableStartOverhang) } return builder.build()