Skip to content

Commit ef91012

Browse files
fix(android): preserve border-radius across transparent → opaque backgroundColor transition
Summary: On Android, a View whose backgroundColor starts as transparent and later transitions to an opaque color loses its border-radius clipping and renders as a rectangle. Reported in facebook#52415; iOS is unaffected. Root cause: `setBorderRadius` only eagerly creates an inner `BackgroundDrawable` for `ImageView`. For a regular `View` with a transparent backgroundColor, the composite background drawable ends up with no inner `BackgroundDrawable` at first. When the color later becomes opaque, `setBackgroundColor` -> `ensureBackgroundDrawable` constructs a fresh `BackgroundDrawable` and swaps `view.background` with a new `CompositeBackgroundDrawable` via `withNewBackground`. The freshly constructed drawable starts with 0x0 bounds, and on the first draw the computed path is a zero-radius rectangle. Fix: always eagerly create the inner `BackgroundDrawable` inside `setBorderRadius`, not only for `ImageView`. This way subsequent `setBackgroundColor` calls mutate the existing drawable in place, avoiding the `view.background` replacement path where bounds/path are not yet primed. Changelog: [ANDROID] [FIXED] - View with borderRadius renders as rectangle after transparent → opaque backgroundColor transition Test Plan: Added an RNTester example under Border > "Border radius with transparent → opaque background" that starts with a transparent 50x50 box with borderRadius: 25 and toggles to red on tap. Without this change the opaque state renders as a square on Android; with the fix it renders as a circle. iOS rendering is unchanged.
1 parent 24b51db commit ef91012

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,14 @@ public object BackgroundStyleApplicator {
211211
compositeBackgroundDrawable.borderRadius ?: BorderRadiusStyle()
212212
compositeBackgroundDrawable.borderRadius?.set(corner, radius)
213213

214-
if (view is ImageView) {
215-
ensureBackgroundDrawable(view)
216-
}
214+
// Eagerly create the BackgroundDrawable so subsequent backgroundColor
215+
// changes can mutate the existing drawable in place instead of
216+
// replacing view.background with a freshly-constructed composite whose
217+
// inner BackgroundDrawable has 0x0 bounds on first draw. Without this,
218+
// a backgroundColor transition from transparent to opaque leaves the
219+
// view rendering as a rectangle even though borderRadius is set.
220+
// See https://github.com/facebook/react-native/issues/52415.
221+
ensureBackgroundDrawable(view)
217222
compositeBackgroundDrawable.background?.borderRadius = compositeBackgroundDrawable.borderRadius
218223
compositeBackgroundDrawable.backgroundImage?.borderRadius =
219224
compositeBackgroundDrawable.borderRadius

packages/rn-tester/js/examples/Border/BorderExample.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import type {RNTesterModule} from '../../types/RNTesterTypes';
1414

1515
import hotdog from '../../assets/hotdog.jpg';
1616
import * as React from 'react';
17+
import {useState} from 'react';
1718
import {
1819
DynamicColorIOS,
1920
Image,
2021
Platform,
2122
PlatformColor,
23+
Pressable,
2224
StyleSheet,
25+
Text,
2326
View,
2427
} from 'react-native';
2528

@@ -622,5 +625,42 @@ export default {
622625
);
623626
},
624627
},
628+
{
629+
title: 'Border radius with transparent → opaque background',
630+
name: 'border-radius-transparent-to-opaque',
631+
description:
632+
'Tap the box to toggle its background between transparent and opaque. ' +
633+
'Both states should render as a circle. Regression guard for #52415.',
634+
render: function (): React.Node {
635+
return <TransparentToOpaqueBorderRadiusExample />;
636+
},
637+
},
625638
],
626639
} as RNTesterModule;
640+
641+
function TransparentToOpaqueBorderRadiusExample(): React.Node {
642+
const [opaque, setOpaque] = useState(false);
643+
return (
644+
<Pressable
645+
onPress={() => setOpaque(prev => !prev)}
646+
testID="border-test-transparent-to-opaque">
647+
<View style={{padding: 16, backgroundColor: 'lightgray'}}>
648+
<View
649+
style={{
650+
width: 50,
651+
height: 50,
652+
borderRadius: 25,
653+
backgroundColor: opaque ? 'red' : 'transparent',
654+
}}
655+
/>
656+
<Text style={{marginTop: 12}}>
657+
Tap to toggle. backgroundColor is currently{' '}
658+
<Text style={{fontWeight: 'bold'}}>
659+
{opaque ? 'red' : 'transparent'}
660+
</Text>
661+
. Both states should render the box as a circle.
662+
</Text>
663+
</View>
664+
</Pressable>
665+
);
666+
}

0 commit comments

Comments
 (0)