Repository navigation
fix: android 15+ text cut off #97860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
linhvovan29546
wants to merge
3
commits into
Expensify:main
from
linhvovan29546:fix/android15-text-cut-off
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
235 changes: 235 additions & 0 deletions
235
patches/react-native/react-native+0.85.3+041+fix-android15-text-clipping-boundswidth.patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt | ||
| index d740c6f..2fffcd0 100644 | ||
| --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt | ||
| +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt | ||
| @@ -29,8 +29,6 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android : | ||
|
|
||
| override fun enableSwiftUIBasedFilters(): Boolean = true | ||
|
|
||
| - override fun fixTextClippingAndroid15useBoundsForWidth(): Boolean = true | ||
| - | ||
| override fun preventShadowTreeCommitExhaustion(): Boolean = true | ||
|
|
||
| override fun useSharedAnimatedBackend(): Boolean = true | ||
| diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt | ||
| index 325ab60..05c9221 100644 | ||
| --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt | ||
| +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/util/AndroidVersion.kt | ||
| @@ -13,6 +13,13 @@ import android.os.Build | ||
| /** Helper class for checking Android version-related information. */ | ||
| internal object AndroidVersion { | ||
|
|
||
| + /** | ||
| + * This is the version code for Android 15 (SDK Level 35). Internally at Meta this code is also | ||
| + * compiled against SDK 34, so we need to retain this constant instead of using | ||
| + * [Build.VERSION_CODES.VANILLA_ICE_CREAM] directly. | ||
| + */ | ||
| + internal const val VERSION_CODE_VANILLA_ICE_CREAM: Int = 35 | ||
| + | ||
| /** | ||
| * This is the version code for Android 16 (SDK Level 36). Delete it once we bump up the default | ||
| * compile SDK version to 36. | ||
| diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt | ||
| index dca0ecb..786b75a 100644 | ||
| --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt | ||
| +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt | ||
| @@ -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 | ||
| @@ -170,6 +171,18 @@ internal object TextLayoutManager { | ||
|
|
||
| private val tagToSpannableCache = ConcurrentHashMap<Int, Spannable>() | ||
|
|
||
| + // Lazily cached Method for StaticLayout.Builder.setUseBoundsForWidth (API 35+). | ||
| + // Reflection is needed because some internal targets compile against an SDK older than 35. | ||
| + private val setUseBoundsForWidthMethod: java.lang.reflect.Method? by lazy { | ||
| + try { | ||
| + StaticLayout.Builder::class | ||
| + .java | ||
| + .getMethod("setUseBoundsForWidth", Boolean::class.javaPrimitiveType) | ||
| + } catch (_: ReflectiveOperationException) { | ||
| + null | ||
| + } | ||
| + } | ||
| + | ||
| fun setCachedSpannableForTag(reactTag: Int, sp: Spannable): Unit { | ||
| tagToSpannableCache[reactTag] = sp | ||
| } | ||
| @@ -827,36 +840,69 @@ internal object TextLayoutManager { | ||
| ) | ||
| } | ||
|
|
||
| - // Pre-Android 15: Use existing advance-based logic | ||
| + val layoutWidth = | ||
| + when (widthYogaMeasureMode) { | ||
| + YogaMeasureMode.EXACTLY -> ceil(width).toInt() | ||
| + 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, | ||
| + layoutWidth, | ||
| + includeFontPadding, | ||
| + textBreakStrategy, | ||
| + hyphenationFrequency, | ||
| + alignment, | ||
| + justificationMode, | ||
| + ellipsizeMode, | ||
| + maxNumberOfLines, | ||
| + paint, | ||
| + ) | ||
| + } | ||
| + | ||
| + // Android 15+ can clip text whose visual glyph bounds are wider than its advance-based desired | ||
| + // width. Probe the visual bounds width (when the setUseBoundsForWidth API is available) and use | ||
| + // it only to grow the desired width — never to shrink below the plain advance-based width. | ||
| + 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 < Build.VERSION_CODES.VANILLA_ICE_CREAM || | ||
| - !ReactNativeFeatureFlags.fixTextClippingAndroid15useBoundsForWidth() | ||
| + Build.VERSION.SDK_INT < VERSION_CODE_VANILLA_ICE_CREAM || | ||
| + setUseBoundsForWidthMethod == null | ||
| ) { | ||
| - val desiredWidth = ceil(Layout.getDesiredWidth(text, paint)).toInt() | ||
| - | ||
| - val layoutWidth = | ||
| - when (widthYogaMeasureMode) { | ||
| - YogaMeasureMode.EXACTLY -> floor(width).toInt() | ||
| - YogaMeasureMode.AT_MOST -> min(desiredWidth, floor(width).toInt()) | ||
| - else -> desiredWidth | ||
| - } | ||
| - return buildLayout( | ||
| - text, | ||
| - layoutWidth, | ||
| - includeFontPadding, | ||
| - textBreakStrategy, | ||
| - hyphenationFrequency, | ||
| - alignment, | ||
| - justificationMode, | ||
| - ellipsizeMode, | ||
| - maxNumberOfLines, | ||
| - paint, | ||
| - ) | ||
| + return advanceWidth | ||
| } | ||
|
|
||
| - // Android 15+: Need to account for visual bounds | ||
| - // Step 1: Create unconstrained layout to get visual bounds width | ||
| - val unconstrainedLayout = | ||
| + val visualBoundsLayout = | ||
| buildLayout( | ||
| text, | ||
| Int.MAX_VALUE / 2, | ||
| @@ -868,34 +914,15 @@ internal object TextLayoutManager { | ||
| null, | ||
| ReactConstants.UNSET, | ||
| paint, | ||
| + useBoundsForWidth = true, | ||
| ) | ||
|
|
||
| - // Calculate visual bounds width from unconstrained layout | ||
| - var desiredVisualWidth = 0f | ||
| - for (i in 0 until unconstrainedLayout.lineCount) { | ||
| - val lineWidth = unconstrainedLayout.getLineRight(i) - unconstrainedLayout.getLineLeft(i) | ||
| - desiredVisualWidth = max(desiredVisualWidth, lineWidth) | ||
| + var visualBoundsWidth = 0f | ||
| + for (i in 0 until visualBoundsLayout.lineCount) { | ||
| + visualBoundsWidth = max(visualBoundsWidth, visualBoundsLayout.getLineMax(i)) | ||
| } | ||
|
|
||
| - val layoutWidth = | ||
| - when (widthYogaMeasureMode) { | ||
| - YogaMeasureMode.AT_MOST -> min(ceil(desiredVisualWidth).toInt(), floor(width).toInt()) | ||
| - else -> ceil(desiredVisualWidth).toInt() | ||
| - } | ||
| - | ||
| - // Step 2: Create final layout with correct width | ||
| - return buildLayout( | ||
| - text, | ||
| - layoutWidth, | ||
| - includeFontPadding, | ||
| - textBreakStrategy, | ||
| - hyphenationFrequency, | ||
| - alignment, | ||
| - justificationMode, | ||
| - ellipsizeMode, | ||
| - maxNumberOfLines, | ||
| - paint, | ||
| - ) | ||
| + return max(advanceWidth, ceil(visualBoundsWidth).toInt()) | ||
| } | ||
|
|
||
| private fun buildLayout( | ||
| @@ -909,6 +936,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) | ||
| @@ -930,11 +958,8 @@ internal object TextLayoutManager { | ||
| builder.setUseLineSpacingFromFallbacks(true) | ||
| } | ||
|
|
||
| - if ( | ||
| - Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && | ||
| - ReactNativeFeatureFlags.fixTextClippingAndroid15useBoundsForWidth() | ||
| - ) { | ||
| - builder.setUseBoundsForWidth(true) | ||
| + if (useBoundsForWidth) { | ||
| + setUseBoundsForWidthMethod?.invoke(builder, true) | ||
| } | ||
|
|
||
| return builder.build() | ||
| diff --git a/node_modules/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h b/node_modules/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h | ||
| index b97ed6d..d5045cc 100644 | ||
| --- a/node_modules/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h | ||
| +++ b/node_modules/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h | ||
| @@ -39,10 +39,6 @@ class ReactNativeFeatureFlagsOverridesOSSExperimental : public ReactNativeFeatur | ||
| return true; | ||
| } | ||
|
|
||
| - bool fixTextClippingAndroid15useBoundsForWidth() override { | ||
| - return true; | ||
| - } | ||
| - | ||
| bool preventShadowTreeCommitExhaustion() override { | ||
| return true; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@linhvovan29546 Do we have any upstream PR to implement this batch into the original repo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, but I didn't author it. react/react-native#57117
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we hold this until there are updates in the upstream PR?