Skip to content

Commit f2a250a

Browse files
sbaiahmed1meta-codesync[bot]
authored andcommitted
fix(android): crash when setting a percentage borderRadius on Image (#57795)
Summary: Fixes #53977 Setting a percentage border radius on `<Image>` (e.g. `borderRadius: '50%'`) crashes Android with `java.lang.String cannot be cast to java.lang.Double`. Percentage radii arrive from JS as strings, but `ReactImageManager`'s `borderRadius` `ReactPropGroup` setter was still typed as `Float`, so the reflection-based property updater (`ViewManagersPropertyCache`) failed on the cast. (The crash reproduces on any API level, not just API 35 as reported.) This applies the same migration `ReactViewManager` received in 0.75: the setter now accepts a `Dynamic` and parses it with `LengthPercentage.setFromDynamic`, which handles both numbers and `'NN%'` strings (invalid values degrade to a warning + null instead of a crash). The old `Float` overload is kept as a deprecated pass-through so existing subclasses stay source/binary compatible, mirroring the `ReactViewManager` precedent, and the public API dump is updated accordingly. No rendering changes are needed: the manager already delegates to `BackgroundStyleApplicator`, which resolves `PERCENT` length values against the view bounds. Note: the same `Float`-typed setter still exists in `ReactTextViewManager`, `PreparedLayoutTextViewManager`, `ReactTextInputManager`, `ReactScrollViewManager`, and `ReactHorizontalScrollViewManager`, so `<Text>`, `<TextInput>`, and `<ScrollView>` crash the same way. I kept this PR scoped to the reported Image crash and I'm happy to follow up with the same fix for the others. ## Changelog: [ANDROID] [FIXED] - Fix crash when setting a percentage borderRadius on Image Pull Request resolved: #57795 Test Plan: - Added `testBorderRadius` to `ReactImagePropertyTest`, driving the real crash path (`ViewManager.updateProperties` → reflection prop updater) with `'50%'`, a plain number, a per-corner percentage, and null, asserting the resolved `LengthPercentage` on the view. Without the fix it fails with the exact exception from the issue; with the fix the suite passes (9/9): `./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.views.image.ReactImagePropertyTest"` - Added a `borderRadius: '50%'` image to rn-tester's Image → Border Radius example. Verified on a Pixel 9 Pro emulator (API 36) with rn-tester built from source: the screen that previously redboxed on mount now renders, with the percentage image drawn as a circle and the existing numeric-radius images unchanged (screenshot below). - Re-recorded the `Image-border-radius-e2e` screenshot baselines. The new image goes inside the shared `border-radius-example` container, which is `flexDirection: 'row'` with `flexWrap: 'wrap'`, so it wraps onto a third row and the container grows taller. That is why the old baselines failed on a dimension mismatch rather than a pixel diff. Regenerated per variant with: `jest-e2e /Image-border-radius-e2e.js -u -t '<variant>'` for `rntester_android`, `rntester_android_rtl`, `rntester_android_nougat` and `rntester_ios`. Each comparison was checked by hand before accepting, to confirm the new image renders as a green-bordered circle rather than a square, that the existing five images keep their positions and that under RTL the new image wraps to the right edge instead of the left. The dimensions back this up: 288 → 432 on `rntester_android` and `rntester_android_rtl`, 216 → 324 on `rntester_android_nougat`, which is exactly one extra row in each case with widths unchanged. All deltas: https://www.internalfb.com/compare-screenshots-from-diff/D114576433 ## Screenshot <img width="372" height="786" alt="image" src="https://github.com/user-attachments/assets/fa240927-3850-4036-9f73-f9ad9f825b18" /> Reviewed By: javache Differential Revision: D114576433 Pulled By: fabriziocucci fbshipit-source-id: 1ed7fcd14df65d870d28b149c40d40872c597605
1 parent 88feed5 commit f2a250a

4 files changed

Lines changed: 68 additions & 1 deletion

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5312,6 +5312,7 @@ public final class com/facebook/react/views/image/ReactImageManager : com/facebo
53125312
public final fun setBlurRadius (Lcom/facebook/react/views/image/ReactImageView;F)V
53135313
public final fun setBorderColor (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/Integer;)V
53145314
public final fun setBorderRadius (Lcom/facebook/react/views/image/ReactImageView;IF)V
5315+
public final fun setBorderRadius (Lcom/facebook/react/views/image/ReactImageView;ILcom/facebook/react/bridge/Dynamic;)V
53155316
public final fun setBorderWidth (Lcom/facebook/react/views/image/ReactImageView;F)V
53165317
public final fun setDefaultSource (Lcom/facebook/react/views/image/ReactImageView;Lcom/facebook/react/bridge/Dynamic;)V
53175318
public final fun setDefaultSource (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/String;)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,20 @@ public constructor(
201201
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
202202
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
203203
],
204-
defaultFloat = Float.NaN,
204+
)
205+
public fun setBorderRadius(view: ReactImageView, index: Int, rawBorderRadius: Dynamic) {
206+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
207+
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius)
208+
}
209+
210+
@Deprecated(
211+
"Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.",
205212
)
206213
public fun setBorderRadius(view: ReactImageView, index: Int, borderRadius: Float) {
214+
// Keep a direct body rather than routing a Float through DynamicFromObject:
215+
// DynamicFromObject(Float).asDouble() throws (boxed Float cannot cast to
216+
// Double), and setFromDynamic would not map NaN back to null the way the
217+
// original Float path did.
207218
val radius =
208219
if (borderRadius.isNaN()) null
209220
else LengthPercentage(borderRadius, LengthPercentageType.POINT)

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ import com.facebook.react.bridge.JavaOnlyMap
2323
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
2424
import com.facebook.react.common.ReactConstants
2525
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
26+
import com.facebook.react.uimanager.BackgroundStyleApplicator
2627
import com.facebook.react.uimanager.DisplayMetricsHolder
28+
import com.facebook.react.uimanager.LengthPercentage
29+
import com.facebook.react.uimanager.LengthPercentageType
2730
import com.facebook.react.uimanager.ReactStylesDiffMap
2831
import com.facebook.react.uimanager.ThemedReactContext
32+
import com.facebook.react.uimanager.style.BorderRadiusProp
2933
import com.facebook.react.util.RNLog
3034
import com.facebook.react.views.imagehelper.ImageSource
3135
import com.facebook.soloader.SoLoader
@@ -142,6 +146,48 @@ class ReactImagePropertyTest {
142146
.isEqualTo(view.imageSource)
143147
}
144148

149+
@Test
150+
fun testBorderRadius() {
151+
val viewManager = ReactImageManager()
152+
val view = viewManager.createViewInstance(themeContext)
153+
154+
// Percentage border radii arrive as strings and must not crash the property updater
155+
viewManager.updateProperties(view, buildStyles("borderRadius", "50%"))
156+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
157+
.isEqualTo(LengthPercentage(50f, LengthPercentageType.PERCENT))
158+
159+
viewManager.updateProperties(view, buildStyles("borderRadius", 10.0))
160+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
161+
.isEqualTo(LengthPercentage(10f, LengthPercentageType.POINT))
162+
163+
viewManager.updateProperties(view, buildStyles("borderTopLeftRadius", "25%"))
164+
assertThat(
165+
BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_TOP_LEFT_RADIUS),
166+
)
167+
.isEqualTo(LengthPercentage(25f, LengthPercentageType.PERCENT))
168+
169+
viewManager.updateProperties(view, buildStyles("borderRadius", null))
170+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
171+
.isNull()
172+
}
173+
174+
@Suppress("DEPRECATION")
175+
@Test
176+
fun testDeprecatedFloatBorderRadius() {
177+
val viewManager = ReactImageManager()
178+
val view = viewManager.createViewInstance(themeContext)
179+
180+
// The deprecated Float overload must not crash and applies the radius as points.
181+
viewManager.setBorderRadius(view, 0, 8f)
182+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
183+
.isEqualTo(LengthPercentage(8f, LengthPercentageType.POINT))
184+
185+
// NaN clears the radius, matching the pre-Dynamic Float behavior.
186+
viewManager.setBorderRadius(view, 0, Float.NaN)
187+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
188+
.isNull()
189+
}
190+
145191
@Test
146192
fun testResizeMode() {
147193
val viewManager = ReactImageManager()

packages/rn-tester/js/examples/Image/ImageExample.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,11 @@ const styles = StyleSheet.create({
11861186
borderColor: 'red',
11871187
backgroundColor: 'yellow',
11881188
},
1189+
borderRadiusPercentage: {
1190+
borderWidth: 4,
1191+
borderRadius: '50%',
1192+
borderColor: 'green',
1193+
},
11891194
boxShadow: {
11901195
margin: 10,
11911196
},
@@ -1448,6 +1453,10 @@ exports.examples = [
14481453
style={[styles.base, styles.borderRadius5]}
14491454
source={fullImage}
14501455
/>
1456+
<Image
1457+
style={[styles.base, styles.borderRadiusPercentage]}
1458+
source={fullImage}
1459+
/>
14511460
</View>
14521461
);
14531462
},

0 commit comments

Comments
 (0)