Fix incorrect view flattening when using a specific not fully transparent color (#51379)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/51379

Changelog: [General][Fixed] Fix incorrect flattening / non-rendering of views with backgroundColor set to `rgba(255, 255, 255, 127/256)`

Fixes #51378.

## Context

When testing some unrelated things with Fantom is realized that the color for some text that I wasn't explicitly defining was being set to `rgba(255, 255, 255, 127)`, like here:

https://github.com/facebook/react-native/blob/249a24ac756275eadbe3b4df1ff9c974af1671d2/packages/react-native-fantom/src/__tests__/Fantom-itest.js#L540-L542

When digging a bit more about why, I realized that was actually the value for `UndefinedColor`. When looking a bit deeper, I saw that the value for that constant was being set like this:

```
using Color = int32_t;

namespace HostPlatformColor {
static const facebook::react::Color UndefinedColor =
    std::numeric_limits<facebook::react::Color>::max();
}
```

I'm not sure what the logic could've been here:
- Defining it as a value out of bounds for all valid colors? In this case, it's a 32 bit value so all the range of values are actually valid RGBA colors.
- Defining it as a fully opaque white? Seems dangerous for a default because you wouldn't be able to distinguish a explicitly set white color from a non-set color, relevant if you're seeing a white background color in a view on top of another view with any other background color.

The result of this existing logic was actually setting `UndefinedColor` to `rgba(255, 255, 255, 127)` because the alpha channel is defined in the first bits of the value, and `Color` being a signed int with 32 bits, the largest value is `01111....1`, so extracting the first 8 bits, you get 127.

## Changes

This changes the value set for the `UndefinedColor` constant (which is used, among other things, to determine if a view sets a background color, or otherwise could potentially be flattened).

The new value, instead of white with a 127/256 opacity, is black with 0% opacity (or simply the number 0 in `int32_t`).

Reviewed By: javache

Differential Revision: D74869311

fbshipit-source-id: 5582b4803b0b5c72cb3c1b33720c4542c5e3f1de
This commit is contained in:
Rubén Norte
2025-05-20 10:38:05 -07:00
committed by Facebook GitHub Bot
parent 6581c69e10
commit b1e8729f4d
5 changed files with 141 additions and 119 deletions
+1 -1
View File
@@ -538,7 +538,7 @@ describe('Fantom', () => {
},
],
props: {
foregroundColor: 'rgba(255, 255, 255, 127)',
foregroundColor: 'rgba(0, 0, 0, 0)',
},
type: 'Paragraph',
});
@@ -33,9 +33,7 @@ describe('<Button>', () => {
).toEqual(
// Upper case on Android (also used by Fantom)
<rn-view backgroundColor="rgba(33, 150, 243, 255)">
<rn-paragraph
backgroundColor="rgba(255, 255, 255, 127)"
foregroundColor="rgba(255, 255, 255, 255)">
<rn-paragraph foregroundColor="rgba(255, 255, 255, 255)">
HELLO
</rn-paragraph>
</rn-view>,
@@ -57,9 +55,7 @@ describe('<Button>', () => {
.toJSX(),
).toEqual(
<rn-view backgroundColor="rgba(0, 0, 255, 255)">
<rn-paragraph
backgroundColor="rgba(255, 255, 255, 127)"
foregroundColor="rgba(255, 255, 255, 255)">
<rn-paragraph foregroundColor="rgba(255, 255, 255, 255)">
HELLO
</rn-paragraph>
</rn-view>,
@@ -105,9 +101,7 @@ describe('<Button>', () => {
.toJSX(),
).toEqual(
<rn-view backgroundColor="rgba(223, 223, 223, 255)">
<rn-paragraph
backgroundColor="rgba(255, 255, 255, 127)"
foregroundColor="rgba(161, 161, 161, 255)">
<rn-paragraph foregroundColor="rgba(161, 161, 161, 255)">
HELLO
</rn-paragraph>
</rn-view>,
@@ -189,9 +183,7 @@ describe('<Button>', () => {
.toJSX(),
).toEqual(
<rn-view backgroundColor="rgba(33, 150, 243, 255)">
<rn-paragraph
backgroundColor="rgba(255, 255, 255, 127)"
foregroundColor="rgba(255, 255, 255, 255)">
<rn-paragraph foregroundColor="rgba(255, 255, 255, 255)">
HELLO
</rn-paragraph>
</rn-view>,
@@ -233,9 +225,7 @@ describe('<Button>', () => {
.toJSX(),
).toEqual(
<rn-view backgroundColor="rgba(223, 223, 223, 255)">
<rn-paragraph
backgroundColor="rgba(255, 255, 255, 127)"
foregroundColor="rgba(161, 161, 161, 255)">
<rn-paragraph foregroundColor="rgba(161, 161, 161, 255)">
HELLO
</rn-paragraph>
</rn-view>,
@@ -15,8 +15,7 @@ namespace facebook::react {
using Color = int32_t;
namespace HostPlatformColor {
static const facebook::react::Color UndefinedColor =
std::numeric_limits<facebook::react::Color>::max();
constexpr facebook::react::Color UndefinedColor = 0;
}
inline Color
@@ -16,8 +16,7 @@ namespace facebook::react {
using Color = int32_t;
namespace HostPlatformColor {
static const facebook::react::Color UndefinedColor =
std::numeric_limits<facebook::react::Color>::max();
constexpr facebook::react::Color UndefinedColor = 0;
}
inline Color
@@ -231,125 +231,159 @@ describe('ViewFlattening', () => {
'Insert {type: "View", parentNativeID: "J", index: 1, nativeID: "B"}',
]);
});
});
test('parent-child switching from unflattened-flattened to flattened-unflattened', () => {
const root = Fantom.createRoot();
test('parent-child switching from unflattened-flattened to flattened-unflattened', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 100,
opacity: 0,
}}>
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 50,
}}>
<View
nativeID={'child'}
style={{height: 10, width: 10, backgroundColor: 'red'}}
/>
</View>
</View>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: "child"}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
// force view to be flattened.
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 100,
}}>
<View
style={{
marginTop: 50,
marginTop: 100,
opacity: 0,
}}>
<View
nativeID={'child'}
style={{height: 10, width: 10, backgroundColor: 'red'}}
/>
</View>
</View>,
);
});
style={{
marginTop: 50,
}}>
<View
nativeID={'child'}
style={{height: 10, width: 10, backgroundColor: 'red'}}
/>
</View>
</View>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "View", nativeID: "child"}',
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
'Delete {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: "child"}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
test('parent-child switching from flattened-unflattened to unflattened-flattened', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 100,
}}>
// force view to be flattened.
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 50,
marginTop: 100,
}}>
<View
style={{
marginTop: 50,
opacity: 0,
}}>
<View
nativeID={'child'}
style={{height: 10, width: 10, backgroundColor: 'red'}}
/>
</View>
</View>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "View", nativeID: "child"}',
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
'Delete {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
});
test('parent-child switching from flattened-unflattened to unflattened-flattened', () => {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 100,
}}>
<View
style={{
marginTop: 50,
opacity: 0,
}}>
<View nativeID={'child'} style={{height: 10, width: 10}} />
</View>
</View>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: "child"}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
// force view to be flattened.
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 100,
opacity: 0,
}}>
<View nativeID={'child'} style={{height: 10, width: 10}} />
</View>
</View>,
);
<View
style={{
marginTop: 50,
}}>
<View nativeID={'child'} style={{height: 10, width: 10}} />
</View>
</View>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "View", nativeID: "child"}',
'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Delete {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
]);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: "child"}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
test('#51378: view with rgba(255,255,255,127/256) background color is not flattened', () => {
const root = Fantom.createRoot();
// force view to be flattened.
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 100,
opacity: 0,
}}>
Fantom.runTask(() => {
root.render(
<View
style={{
marginTop: 50,
}}>
<View nativeID={'child'} style={{height: 10, width: 10}} />
</View>
</View>,
width: 100,
height: 100,
backgroundColor: `rgba(255, 255, 255, ${127 / 256})`,
}}
/>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "RootView", nativeID: (root)}',
'Create {type: "View", nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
]);
expect(
root
.getRenderedOutput({props: ['width', 'height', 'backgroundColor']})
.toJSX(),
).toEqual(
<rn-view
width="100.000000"
height="100.000000"
backgroundColor="rgba(255, 255, 255, 127)"
/>,
);
});
expect(root.takeMountingManagerLogs()).toEqual([
'Update {type: "View", nativeID: "child"}',
'Remove {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
'Delete {type: "View", nativeID: (N/A)}',
'Create {type: "View", nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: (N/A)}',
'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
]);
});
describe('reconciliation of setNativeProps and React commit', () => {