Skip to content

Commit fb14f61

Browse files
committed
Add Android support for alpha modifier in PlatformColor
This update enables the use of the alpha modifier for PlatformColor on Android by normalizing color specs to include alpha and updating the native bridge to apply alpha values. The rn-tester example is updated to demonstrate alpha usage on both iOS and Android, and documentation is clarified to reflect cross-platform support.
1 parent 9b15405 commit fb14f61

3 files changed

Lines changed: 124 additions & 50 deletions

File tree

packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.android.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,37 @@ export type PlatformColorOptions = {
2626

2727
export type PlatformColorSpec = string | PlatformColorOptions;
2828

29+
type AndroidColorSpec = {
30+
name: string,
31+
alpha?: number,
32+
};
33+
2934
/** The actual type of the opaque NativeColorValue on Android platform */
3035
type LocalNativeColorValue = {
31-
resource_paths?: Array<string>,
36+
resource_paths?: Array<AndroidColorSpec>,
3237
};
3338

3439
/**
35-
* Extracts color name from a spec (string or options object).
36-
* On Android, only the name is used - modifiers like alpha/prominence are iOS-specific.
40+
* Normalizes a color spec (string or options object) to Android color spec format.
41+
* On Android, only name and alpha are supported - prominence/contentHeadroom are iOS-specific.
3742
*/
38-
function getColorName(spec: PlatformColorSpec): string {
43+
function normalizeColorSpec(spec: PlatformColorSpec): AndroidColorSpec {
3944
if (typeof spec === 'string') {
40-
return spec;
45+
return {name: spec};
46+
}
47+
const result: AndroidColorSpec = {name: spec.name};
48+
if (spec.alpha != null) {
49+
result.alpha = spec.alpha;
4150
}
42-
return spec.name;
51+
return result;
4352
}
4453

4554
export const PlatformColor = (
4655
...specs: Array<PlatformColorSpec>
4756
): NativeColorValue => {
48-
const names = specs.map(getColorName);
57+
const normalizedSpecs = specs.map(normalizeColorSpec);
4958
// $FlowExpectedError[incompatible-return] LocalNativeColorValue is compatible with NativeColorValue
50-
return {resource_paths: names};
59+
return {resource_paths: normalizedSpecs};
5160
};
5261

5362
export const normalizeColorObject = (

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.kt

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ public object ColorPropConverter {
2323
private fun supportWideGamut(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
2424

2525
private const val JSON_KEY = "resource_paths"
26+
private const val JSON_KEY_NAME = "name"
27+
private const val JSON_KEY_ALPHA = "alpha"
2628
private const val PREFIX_RESOURCE = "@"
2729
private const val PREFIX_ATTR = "?"
2830
private const val PACKAGE_DELIMITER = ":"
2931
private const val PATH_DELIMITER = "/"
3032
private const val ATTR = "attr"
3133
private const val ATTR_SEGMENT = "attr/"
3234

35+
private fun applyAlpha(color: Int, alpha: Double): Int {
36+
val a = (alpha * 255).toInt().coerceIn(0, 255)
37+
return (color and 0x00FFFFFF) or (a shl 24)
38+
}
39+
3340
private fun getColorInteger(value: Any?, context: Context): Int? {
3441
if (value == null) {
3542
return null
@@ -54,13 +61,30 @@ public object ColorPropConverter {
5461
val resourcePaths =
5562
value.getArray(JSON_KEY)
5663
?: throw JSApplicationCausedNativeException(
57-
"ColorValue: The `$JSON_KEY` must be an array of color resource path strings."
64+
"ColorValue: The `$JSON_KEY` must be an array of color resource path strings or objects."
5865
)
5966

6067
for (i in 0 until resourcePaths.size()) {
61-
val result = resolveResourcePath(context, resourcePaths.getString(i))
68+
val item = resourcePaths.getDynamic(i)
69+
val resourcePath: String?
70+
val alpha: Double?
71+
72+
when (item.type) {
73+
ReadableType.String -> {
74+
resourcePath = item.asString()
75+
alpha = null
76+
}
77+
ReadableType.Map -> {
78+
val map = item.asMap()
79+
resourcePath = map.getString(JSON_KEY_NAME)
80+
alpha = if (map.hasKey(JSON_KEY_ALPHA)) map.getDouble(JSON_KEY_ALPHA) else null
81+
}
82+
else -> continue
83+
}
84+
85+
val result = resolveResourcePath(context, resourcePath)
6286
if (result != null) {
63-
return result
87+
return if (alpha != null) applyAlpha(result, alpha) else result
6488
}
6589
}
6690

@@ -103,13 +127,31 @@ public object ColorPropConverter {
103127
val resourcePaths =
104128
value.getArray(JSON_KEY)
105129
?: throw JSApplicationCausedNativeException(
106-
"ColorValue: The `$JSON_KEY` must be an array of color resource path strings."
130+
"ColorValue: The `$JSON_KEY` must be an array of color resource path strings or objects."
107131
)
108132

109133
for (i in 0 until resourcePaths.size()) {
110-
val result = resolveResourcePath(context, resourcePaths.getString(i))
134+
val item = resourcePaths.getDynamic(i)
135+
val resourcePath: String?
136+
val alpha: Double?
137+
138+
when (item.type) {
139+
ReadableType.String -> {
140+
resourcePath = item.asString()
141+
alpha = null
142+
}
143+
ReadableType.Map -> {
144+
val map = item.asMap()
145+
resourcePath = map.getString(JSON_KEY_NAME)
146+
alpha = if (map.hasKey(JSON_KEY_ALPHA)) map.getDouble(JSON_KEY_ALPHA) else null
147+
}
148+
else -> continue
149+
}
150+
151+
val result = resolveResourcePath(context, resourcePath)
111152
if (supportWideGamut() && result != null) {
112-
return Color.valueOf(result)
153+
val colorWithAlpha = if (alpha != null) applyAlpha(result, alpha) else result
154+
return Color.valueOf(colorWithAlpha)
113155
}
114156
}
115157

packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -185,39 +185,62 @@ function PlatformColorsExample() {
185185
}
186186

187187
function AlphaColorsExample() {
188-
if (Platform.OS !== 'ios') {
189-
return (
190-
<RNTesterText style={styles.labelCell}>
191-
Alpha modifier is iOS-specific (no-op on other platforms)
192-
</RNTesterText>
193-
);
194-
}
188+
let colors: Array<{
189+
color: ReturnType<typeof PlatformColor>,
190+
label: string,
191+
}> = [];
195192

196-
const colors = [
197-
{label: 'systemBlue', color: PlatformColor('systemBlue')},
198-
{
199-
label: '{name: systemBlue, alpha: 0.75}',
200-
color: PlatformColor({name: 'systemBlue', alpha: 0.75}),
201-
},
202-
{
203-
label: '{name: systemBlue, alpha: 0.5}',
204-
color: PlatformColor({name: 'systemBlue', alpha: 0.5}),
205-
},
206-
{
207-
label: '{name: systemBlue, alpha: 0.25}',
208-
color: PlatformColor({name: 'systemBlue', alpha: 0.25}),
209-
},
210-
{label: 'systemRed', color: PlatformColor('systemRed')},
211-
{
212-
label: '{name: systemRed, alpha: 0.5}',
213-
color: PlatformColor({name: 'systemRed', alpha: 0.5}),
214-
},
215-
{label: 'label', color: PlatformColor('label')},
216-
{
217-
label: '{name: label, alpha: 0.5}',
218-
color: PlatformColor({name: 'label', alpha: 0.5}),
219-
},
220-
];
193+
if (Platform.OS === 'ios') {
194+
colors = [
195+
{label: 'systemBlue', color: PlatformColor('systemBlue')},
196+
{
197+
label: '{name: systemBlue, alpha: 0.75}',
198+
color: PlatformColor({name: 'systemBlue', alpha: 0.75}),
199+
},
200+
{
201+
label: '{name: systemBlue, alpha: 0.5}',
202+
color: PlatformColor({name: 'systemBlue', alpha: 0.5}),
203+
},
204+
{
205+
label: '{name: systemBlue, alpha: 0.25}',
206+
color: PlatformColor({name: 'systemBlue', alpha: 0.25}),
207+
},
208+
{label: 'systemRed', color: PlatformColor('systemRed')},
209+
{
210+
label: '{name: systemRed, alpha: 0.5}',
211+
color: PlatformColor({name: 'systemRed', alpha: 0.5}),
212+
},
213+
{label: 'label', color: PlatformColor('label')},
214+
{
215+
label: '{name: label, alpha: 0.5}',
216+
color: PlatformColor({name: 'label', alpha: 0.5}),
217+
},
218+
];
219+
} else if (Platform.OS === 'android') {
220+
colors = [
221+
{label: '?attr/colorPrimary', color: PlatformColor('?attr/colorPrimary')},
222+
{
223+
label: '{name: ?attr/colorPrimary, alpha: 0.75}',
224+
color: PlatformColor({name: '?attr/colorPrimary', alpha: 0.75}),
225+
},
226+
{
227+
label: '{name: ?attr/colorPrimary, alpha: 0.5}',
228+
color: PlatformColor({name: '?attr/colorPrimary', alpha: 0.5}),
229+
},
230+
{
231+
label: '{name: ?attr/colorPrimary, alpha: 0.25}',
232+
color: PlatformColor({name: '?attr/colorPrimary', alpha: 0.25}),
233+
},
234+
{
235+
label: '?attr/colorAccent',
236+
color: PlatformColor('?attr/colorAccent'),
237+
},
238+
{
239+
label: '{name: ?attr/colorAccent, alpha: 0.5}',
240+
color: PlatformColor({name: '?attr/colorAccent', alpha: 0.5}),
241+
},
242+
];
243+
}
221244

222245
return <ColorTable colors={colors} />;
223246
}
@@ -553,22 +576,22 @@ exports.examples = [
553576
},
554577
},
555578
{
556-
title: 'Alpha Modifier (iOS)',
557-
description: 'Use .alpha() to set the opacity of a platform color',
579+
title: 'Alpha Modifier',
580+
description: 'Use alpha option to set the opacity of a platform color',
558581
render(): React.MixedElement {
559582
return <AlphaColorsExample />;
560583
},
561584
},
562585
{
563586
title: 'Prominence Modifier (iOS 18+)',
564-
description: 'Use .prominence() to set the visual prominence level',
587+
description: 'Use prominence option to set the visual prominence level',
565588
render(): React.MixedElement {
566589
return <ProminenceColorsExample />;
567590
},
568591
},
569592
{
570593
title: 'Content Headroom (iOS 26+)',
571-
description: 'Use .contentHeadroom() for HDR color brightness',
594+
description: 'Use contentHeadroom option for HDR color brightness',
572595
render(): React.MixedElement {
573596
return <ContentHeadroomColorsExample />;
574597
},

0 commit comments

Comments
 (0)