Skip to content

Commit f4de458

Browse files
tom-unfacebook-github-bot
authored andcommitted
PlatformColor implementations for iOS and Android (#27908)
Summary: This Pull Request implements the PlatformColor proposal discussed at react-native-community/discussions-and-proposals#126. The changes include implementations for iOS and Android as well as a PlatformColorExample page in RNTester. Every native platform has the concept of system defined colors. Instead of specifying a concrete color value the app developer can choose a system color that varies in appearance depending on a system theme settings such Light or Dark mode, accessibility settings such as a High Contrast mode, and even its context within the app such as the traits of a containing view or window. The proposal is to add true platform color support to react-native by extending the Flow type `ColorValue` with platform specific color type information for each platform and to provide a convenience function, `PlatformColor()`, for instantiating platform specific ColorValue objects. `PlatformColor(name [, name ...])` where `name` is a system color name on a given platform. If `name` does not resolve to a color for any reason, the next `name` in the argument list will be resolved and so on. If none of the names resolve, a RedBox error occurs. This allows a latest platform color to be used, but if running on an older platform it will fallback to a previous version. The function returns a `ColorValue`. On iOS the values of `name` is one of the iOS [UI Element](https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors) or [Standard Color](https://developer.apple.com/documentation/uikit/uicolor/standard_colors) names such as `labelColor` or `systemFillColor`. On Android the `name` values are the same [app resource](https://developer.android.com/guide/topics/resources/providing-resources) path strings that can be expressed in XML: XML Resource: `@ [<package_name>:]<resource_type>/<resource_name>` Style reference from current theme: `?[<package_name>:][<resource_type>/]<resource_name>` For example: - `?android:colorError` - `?android:attr/colorError` - `?attr/colorPrimary` - `?colorPrimaryDark` - `android:color/holo_purple` - `color/catalyst_redbox_background` On iOS another type of system dynamic color can be created using the `IOSDynamicColor({dark: <color>, light:<color>})` method. The arguments are a tuple containing custom colors for light and dark themes. Such dynamic colors are useful for branding colors or other app specific colors that still respond automatically to system setting changes. Example: `<View style={{ backgroundColor: IOSDynamicColor({light: 'black', dark: 'white'}) }}/>` Other platforms could create platform specific functions similar to `IOSDynamicColor` per the needs of those platforms. For example, macOS has a similar dynamic color type that could be implemented via a `MacDynamicColor`. On Windows custom brushes that tint or otherwise modify a system brush could be created using a platform specific method. ## Changelog [General] [Added] - Added PlatformColor implementations for iOS and Android Pull Request resolved: #27908 Test Plan: The changes have been tested using the RNTester test app for iOS and Android. On iOS a set of XCTestCase's were added to the Unit Tests. <img width="924" alt="PlatformColor-ios-android" src="https://user-images.githubusercontent.com/30053638/73472497-ff183a80-433f-11ea-90d8-2b04338bbe79.png"> In addition `PlatformColor` support has been added to other out-of-tree platforms such as macOS and Windows has been implemented using these changes: react-native for macOS branch: microsoft/react-native-macos@master...tom-un:tomun/platformcolors react-native for Windows branch: microsoft/react-native-windows@master...tom-un:tomun/platformcolors iOS |Light|Dark| |{F229354502}|{F229354515}| Android |Light|Dark| |{F230114392}|{F230114490}| {F230122700} Reviewed By: hramos Differential Revision: D19837753 Pulled By: TheSavior fbshipit-source-id: 82ca70d40802f3b24591bfd4b94b61f3c38ba829
1 parent 5166856 commit f4de458

File tree

52 files changed

+1621
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1621
-80
lines changed

Libraries/ART/ARTSurfaceView.m

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ - (void)invalidate
4747

4848
- (void)drawRect:(CGRect)rect
4949
{
50+
[super drawRect:rect];
5051
CGContextRef context = UIGraphicsGetCurrentContext();
5152
for (ARTNode *node in self.subviews) {
5253
[node renderTo:context];

Libraries/ActionSheetIOS/ActionSheetIOS.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import RCTActionSheetManager from './NativeActionSheetManager';
1414

1515
const invariant = require('invariant');
1616
const processColor = require('../StyleSheet/processColor');
17+
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
18+
import type {ProcessedColorValue} from '../StyleSheet/processColor';
1719

1820
/**
1921
* Display action sheets and share sheets on iOS.
@@ -45,7 +47,7 @@ const ActionSheetIOS = {
4547
+destructiveButtonIndex?: ?number | ?Array<number>,
4648
+cancelButtonIndex?: ?number,
4749
+anchor?: ?number,
48-
+tintColor?: number | string,
50+
+tintColor?: ColorValue | ProcessedColorValue,
4951
+userInterfaceStyle?: string,
5052
|},
5153
callback: (buttonIndex: number) => void,

Libraries/Animated/src/nodes/AnimatedInterpolation.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,17 @@ function interpolate(
164164
}
165165

166166
function colorToRgba(input: string): string {
167-
let int32Color = normalizeColor(input);
168-
if (int32Color === null) {
167+
let normalizedColor = normalizeColor(input);
168+
if (normalizedColor === null || typeof normalizedColor !== 'number') {
169169
return input;
170170
}
171171

172-
int32Color = int32Color || 0;
172+
normalizedColor = normalizedColor || 0;
173173

174-
const r = (int32Color & 0xff000000) >>> 24;
175-
const g = (int32Color & 0x00ff0000) >>> 16;
176-
const b = (int32Color & 0x0000ff00) >>> 8;
177-
const a = (int32Color & 0x000000ff) / 255;
174+
const r = (normalizedColor & 0xff000000) >>> 24;
175+
const g = (normalizedColor & 0x00ff0000) >>> 16;
176+
const b = (normalizedColor & 0x0000ff00) >>> 8;
177+
const a = (normalizedColor & 0x000000ff) / 255;
178178

179179
return `rgba(${r}, ${g}, ${b}, ${a})`;
180180
}

Libraries/Components/ActivityIndicator/ActivityIndicator.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const StyleSheet = require('../../StyleSheet/StyleSheet');
1616
const View = require('../View/View');
1717
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
1818
import type {ViewProps} from '../View/ViewPropTypes';
19+
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
1920

2021
const PlatformActivityIndicator =
2122
Platform.OS === 'android'
@@ -50,7 +51,7 @@ type Props = $ReadOnly<{|
5051
*
5152
* See https://reactnative.dev/docs/activityindicator.html#color
5253
*/
53-
color?: ?string,
54+
color?: ?ColorValue,
5455

5556
/**
5657
* Size of the indicator (default is 'small').

Libraries/Components/Button.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const View = require('./View/View');
2121
const invariant = require('invariant');
2222

2323
import type {PressEvent} from '../Types/CoreEventTypes';
24+
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
2425

2526
type ButtonProps = $ReadOnly<{|
2627
/**
@@ -41,7 +42,7 @@ type ButtonProps = $ReadOnly<{|
4142
/**
4243
* Color of the text (iOS), or background color of the button (Android)
4344
*/
44-
color?: ?string,
45+
color?: ?ColorValue,
4546

4647
/**
4748
* TV preferred focus (see documentation for the View component).

Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const requireNativeComponent = require('../../ReactNative/requireNativeComponent
1919
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
2020
import type {ViewProps} from '../View/ViewPropTypes';
2121
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
22+
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
2223

2324
type CheckBoxEvent = SyntheticEvent<
2425
$ReadOnly<{|
@@ -47,7 +48,12 @@ type NativeProps = $ReadOnly<{|
4748

4849
on?: ?boolean,
4950
enabled?: boolean,
50-
tintColors: {|true: ?number, false: ?number|} | typeof undefined,
51+
tintColors:
52+
| {|
53+
true: ?ProcessedColorValue,
54+
false: ?ProcessedColorValue,
55+
|}
56+
| typeof undefined,
5157
|}>;
5258

5359
type NativeType = HostComponent<NativeProps>;

Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class DrawerLayoutAndroid extends React.Component<Props, State> {
185185
...props
186186
} = this.props;
187187
const drawStatusBar =
188-
Platform.Version >= 21 && this.props.statusBarBackgroundColor;
188+
Platform.Version >= 21 && this.props.statusBarBackgroundColor != null;
189189
const drawerViewWrapper = (
190190
<View
191191
style={[

Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import type {
2323
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
2424
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
2525
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
26+
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
2627
import type {ViewProps} from '../../Components/View/ViewPropTypes';
2728

2829
type PickerItem = $ReadOnly<{|
2930
label: string,
30-
color?: ?Int32,
31+
color?: ?ProcessedColorValue,
3132
|}>;
3233

3334
type PickerItemSelectEvent = $ReadOnly<{|

Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import type {
2323
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
2424
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
2525
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
26+
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
2627
import type {ViewProps} from '../../Components/View/ViewPropTypes';
2728

2829
type PickerItem = $ReadOnly<{|
2930
label: string,
30-
color?: ?Int32,
31+
color?: ?ProcessedColorValue,
3132
|}>;
3233

3334
type PickerItemSelectEvent = $ReadOnly<{|

Libraries/Components/Picker/PickerIOS.ios.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import RCTPickerNativeComponent, {
2424
} from './RCTPickerNativeComponent';
2525
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
2626
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
27+
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
2728
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
2829
import type {ViewProps} from '../View/ViewPropTypes';
2930

@@ -37,7 +38,7 @@ type PickerIOSChangeEvent = SyntheticEvent<
3738
type RCTPickerIOSItemType = $ReadOnly<{|
3839
label: ?Label,
3940
value: ?(number | string),
40-
textColor: ?number,
41+
textColor: ?ProcessedColorValue,
4142
|}>;
4243

4344
type Label = Stringish | number;

Libraries/Components/Picker/RCTPickerNativeComponent.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const requireNativeComponent = require('../../ReactNative/requireNativeComponent
1515
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
1616
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
1717
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
18+
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
1819
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
1920
import * as React from 'react';
2021

@@ -28,7 +29,7 @@ type PickerIOSChangeEvent = SyntheticEvent<
2829
type RCTPickerIOSItemType = $ReadOnly<{|
2930
label: ?Label,
3031
value: ?(number | string),
31-
textColor: ?number,
32+
textColor: ?ProcessedColorValue,
3233
|}>;
3334

3435
type Label = Stringish | number;

Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const React = require('react');
1515
import ProgressBarAndroidNativeComponent from './ProgressBarAndroidNativeComponent';
1616

1717
import type {ViewProps} from '../View/ViewPropTypes';
18+
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
1819

1920
export type ProgressBarAndroidProps = $ReadOnly<{|
2021
...ViewProps,
@@ -49,7 +50,7 @@ export type ProgressBarAndroidProps = $ReadOnly<{|
4950
/**
5051
* Color of the progress bar.
5152
*/
52-
color?: ?string,
53+
color?: ?ColorValue,
5354
/**
5455
* Used to locate this view in end-to-end tests.
5556
*/

Libraries/Components/StatusBar/StatusBar.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const React = require('react');
1515

1616
const invariant = require('invariant');
1717
const processColor = require('../../StyleSheet/processColor');
18+
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
1819

1920
import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid';
2021
import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS';
@@ -62,7 +63,7 @@ type AndroidProps = $ReadOnly<{|
6263
* The background color of the status bar.
6364
* @platform android
6465
*/
65-
backgroundColor?: ?string,
66+
backgroundColor?: ?ColorValue,
6667
/**
6768
* If the status bar is translucent.
6869
* When translucent is set to true, the app will draw under the status bar.

Libraries/Pressability/PressabilityDebug.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
'use strict';
1212

1313
import normalizeColor from '../StyleSheet/normalizeColor.js';
14+
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
15+
1416
import Touchable from '../Components/Touchable/Touchable';
1517
import View from '../Components/View/View';
1618
import * as React from 'react';
1719

1820
type Props = $ReadOnly<{|
19-
color: string,
21+
color: ColorValue,
2022
hitSlop: ?$ReadOnly<{|
2123
bottom?: ?number,
2224
left?: ?number,
@@ -43,8 +45,12 @@ type Props = $ReadOnly<{|
4345
export function PressabilityDebugView({color, hitSlop}: Props): React.Node {
4446
if (__DEV__) {
4547
if (isEnabled()) {
48+
const normalizedColor = normalizeColor(color);
49+
if (typeof normalizedColor !== 'number') {
50+
return null;
51+
}
4652
const baseColor =
47-
'#' + (normalizeColor(color) ?? 0).toString(16).padStart(8, '0');
53+
'#' + (normalizedColor ?? 0).toString(16).padStart(8, '0');
4854

4955
return (
5056
<View

Libraries/Share/Share.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class Share {
125125
typeof content.message === 'string' ? content.message : undefined,
126126
url: typeof content.url === 'string' ? content.url : undefined,
127127
subject: options.subject,
128-
tintColor: tintColor != null ? tintColor : undefined,
128+
tintColor: typeof tintColor === 'number' ? tintColor : undefined,
129129
excludedActivityTypes: options.excludedActivityTypes,
130130
},
131131
error => reject(error),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
'use strict';
12+
13+
import type {ColorValue} from './StyleSheetTypes';
14+
import type {ProcessedColorValue} from './processColor';
15+
16+
export opaque type NativeColorValue = {
17+
resource_paths?: Array<string>,
18+
};
19+
20+
export const PlatformColor = (...names: Array<string>): ColorValue => {
21+
return {resource_paths: names};
22+
};
23+
24+
export const ColorAndroidPrivate = (color: string): ColorValue => {
25+
return {resource_paths: [color]};
26+
};
27+
28+
export const normalizeColorObject = (
29+
color: NativeColorValue,
30+
): ?ProcessedColorValue => {
31+
if ('resource_paths' in color) {
32+
return color;
33+
}
34+
return null;
35+
};
36+
37+
export const processColorObject = (
38+
color: NativeColorValue,
39+
): ?NativeColorValue => {
40+
return color;
41+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
'use strict';
12+
13+
import type {ColorValue} from './StyleSheetTypes';
14+
import type {ProcessedColorValue} from './processColor';
15+
16+
export opaque type NativeColorValue = {
17+
semantic?: Array<string>,
18+
dynamic?: {
19+
light: ?(ColorValue | ProcessedColorValue),
20+
dark: ?(ColorValue | ProcessedColorValue),
21+
},
22+
};
23+
24+
export const PlatformColor = (...names: Array<string>): ColorValue => {
25+
return {semantic: names};
26+
};
27+
28+
export type DynamicColorIOSTuplePrivate = {
29+
light: ColorValue,
30+
dark: ColorValue,
31+
};
32+
33+
export const DynamicColorIOSPrivate = (
34+
tuple: DynamicColorIOSTuplePrivate,
35+
): ColorValue => {
36+
return {dynamic: {light: tuple.light, dark: tuple.dark}};
37+
};
38+
39+
export const normalizeColorObject = (
40+
color: NativeColorValue,
41+
): ?ProcessedColorValue => {
42+
if ('semantic' in color) {
43+
// an ios semantic color
44+
return color;
45+
} else if ('dynamic' in color && color.dynamic !== undefined) {
46+
const normalizeColor = require('./normalizeColor');
47+
48+
// a dynamic, appearance aware color
49+
const dynamic = color.dynamic;
50+
const dynamicColor: NativeColorValue = {
51+
dynamic: {
52+
light: normalizeColor(dynamic.light),
53+
dark: normalizeColor(dynamic.dark),
54+
},
55+
};
56+
return dynamicColor;
57+
}
58+
59+
return null;
60+
};
61+
62+
export const processColorObject = (
63+
color: NativeColorValue,
64+
): ?NativeColorValue => {
65+
if ('dynamic' in color && color.dynamic != null) {
66+
const processColor = require('./processColor');
67+
const dynamic = color.dynamic;
68+
const dynamicColor: NativeColorValue = {
69+
dynamic: {
70+
light: processColor(dynamic.light),
71+
dark: processColor(dynamic.dark),
72+
},
73+
};
74+
return dynamicColor;
75+
}
76+
return color;
77+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
'use strict';
12+
13+
import type {ColorValue} from './StyleSheetTypes';
14+
import {ColorAndroidPrivate} from './PlatformColorValueTypes';
15+
16+
export const ColorAndroid = (color: string): ColorValue => {
17+
return ColorAndroidPrivate(color);
18+
};

0 commit comments

Comments
 (0)