PlatformColor implementations for iOS and Android (#27908)

Summary:
This Pull Request implements the PlatformColor proposal discussed at https://github.com/react-native-community/discussions-and-proposals/issues/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: https://github.com/facebook/react-native/pull/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: https://github.com/microsoft/react-native/compare/master...tom-un:tomun/platformcolors

react-native for Windows branch: https://github.com/microsoft/react-native-windows/compare/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
This commit is contained in:
Tom Underhill 2020-03-02 15:07:50 -08:00 коммит произвёл Facebook Github Bot
Родитель 5166856d04
Коммит f4de45800f
52 изменённых файлов: 1621 добавлений и 80 удалений

Просмотреть файл

@ -47,6 +47,7 @@
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
for (ARTNode *node in self.subviews) {
[node renderTo:context];

Просмотреть файл

@ -14,6 +14,8 @@ import RCTActionSheetManager from './NativeActionSheetManager';
const invariant = require('invariant');
const processColor = require('../StyleSheet/processColor');
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
import type {ProcessedColorValue} from '../StyleSheet/processColor';
/**
* Display action sheets and share sheets on iOS.
@ -45,7 +47,7 @@ const ActionSheetIOS = {
+destructiveButtonIndex?: ?number | ?Array<number>,
+cancelButtonIndex?: ?number,
+anchor?: ?number,
+tintColor?: number | string,
+tintColor?: ColorValue | ProcessedColorValue,
+userInterfaceStyle?: string,
|},
callback: (buttonIndex: number) => void,

Просмотреть файл

@ -164,17 +164,17 @@ function interpolate(
}
function colorToRgba(input: string): string {
let int32Color = normalizeColor(input);
if (int32Color === null) {
let normalizedColor = normalizeColor(input);
if (normalizedColor === null || typeof normalizedColor !== 'number') {
return input;
}
int32Color = int32Color || 0;
normalizedColor = normalizedColor || 0;
const r = (int32Color & 0xff000000) >>> 24;
const g = (int32Color & 0x00ff0000) >>> 16;
const b = (int32Color & 0x0000ff00) >>> 8;
const a = (int32Color & 0x000000ff) / 255;
const r = (normalizedColor & 0xff000000) >>> 24;
const g = (normalizedColor & 0x00ff0000) >>> 16;
const b = (normalizedColor & 0x0000ff00) >>> 8;
const a = (normalizedColor & 0x000000ff) / 255;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}

Просмотреть файл

@ -16,6 +16,7 @@ const StyleSheet = require('../../StyleSheet/StyleSheet');
const View = require('../View/View');
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
const PlatformActivityIndicator =
Platform.OS === 'android'
@ -50,7 +51,7 @@ type Props = $ReadOnly<{|
*
* See https://reactnative.dev/docs/activityindicator.html#color
*/
color?: ?string,
color?: ?ColorValue,
/**
* Size of the indicator (default is 'small').

Просмотреть файл

@ -21,6 +21,7 @@ const View = require('./View/View');
const invariant = require('invariant');
import type {PressEvent} from '../Types/CoreEventTypes';
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
type ButtonProps = $ReadOnly<{|
/**
@ -41,7 +42,7 @@ type ButtonProps = $ReadOnly<{|
/**
* Color of the text (iOS), or background color of the button (Android)
*/
color?: ?string,
color?: ?ColorValue,
/**
* TV preferred focus (see documentation for the View component).

Просмотреть файл

@ -19,6 +19,7 @@ const requireNativeComponent = require('../../ReactNative/requireNativeComponent
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
type CheckBoxEvent = SyntheticEvent<
$ReadOnly<{|
@ -47,7 +48,12 @@ type NativeProps = $ReadOnly<{|
on?: ?boolean,
enabled?: boolean,
tintColors: {|true: ?number, false: ?number|} | typeof undefined,
tintColors:
| {|
true: ?ProcessedColorValue,
false: ?ProcessedColorValue,
|}
| typeof undefined,
|}>;
type NativeType = HostComponent<NativeProps>;

Просмотреть файл

@ -185,7 +185,7 @@ class DrawerLayoutAndroid extends React.Component<Props, State> {
...props
} = this.props;
const drawStatusBar =
Platform.Version >= 21 && this.props.statusBarBackgroundColor;
Platform.Version >= 21 && this.props.statusBarBackgroundColor != null;
const drawerViewWrapper = (
<View
style={[

Просмотреть файл

@ -23,11 +23,12 @@ import type {
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {ViewProps} from '../../Components/View/ViewPropTypes';
type PickerItem = $ReadOnly<{|
label: string,
color?: ?Int32,
color?: ?ProcessedColorValue,
|}>;
type PickerItemSelectEvent = $ReadOnly<{|

Просмотреть файл

@ -23,11 +23,12 @@ import type {
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {ViewProps} from '../../Components/View/ViewPropTypes';
type PickerItem = $ReadOnly<{|
label: string,
color?: ?Int32,
color?: ?ProcessedColorValue,
|}>;
type PickerItemSelectEvent = $ReadOnly<{|

Просмотреть файл

@ -24,6 +24,7 @@ import RCTPickerNativeComponent, {
} from './RCTPickerNativeComponent';
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
import type {ViewProps} from '../View/ViewPropTypes';
@ -37,7 +38,7 @@ type PickerIOSChangeEvent = SyntheticEvent<
type RCTPickerIOSItemType = $ReadOnly<{|
label: ?Label,
value: ?(number | string),
textColor: ?number,
textColor: ?ProcessedColorValue,
|}>;
type Label = Stringish | number;

Просмотреть файл

@ -15,6 +15,7 @@ const requireNativeComponent = require('../../ReactNative/requireNativeComponent
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {SyntheticEvent} from '../../Types/CoreEventTypes';
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
import * as React from 'react';
@ -28,7 +29,7 @@ type PickerIOSChangeEvent = SyntheticEvent<
type RCTPickerIOSItemType = $ReadOnly<{|
label: ?Label,
value: ?(number | string),
textColor: ?number,
textColor: ?ProcessedColorValue,
|}>;
type Label = Stringish | number;

Просмотреть файл

@ -15,6 +15,7 @@ const React = require('react');
import ProgressBarAndroidNativeComponent from './ProgressBarAndroidNativeComponent';
import type {ViewProps} from '../View/ViewPropTypes';
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
export type ProgressBarAndroidProps = $ReadOnly<{|
...ViewProps,
@ -49,7 +50,7 @@ export type ProgressBarAndroidProps = $ReadOnly<{|
/**
* Color of the progress bar.
*/
color?: ?string,
color?: ?ColorValue,
/**
* Used to locate this view in end-to-end tests.
*/

Просмотреть файл

@ -15,6 +15,7 @@ const React = require('react');
const invariant = require('invariant');
const processColor = require('../../StyleSheet/processColor');
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid';
import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS';
@ -62,7 +63,7 @@ type AndroidProps = $ReadOnly<{|
* The background color of the status bar.
* @platform android
*/
backgroundColor?: ?string,
backgroundColor?: ?ColorValue,
/**
* If the status bar is translucent.
* When translucent is set to true, the app will draw under the status bar.

Просмотреть файл

@ -11,12 +11,14 @@
'use strict';
import normalizeColor from '../StyleSheet/normalizeColor.js';
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
import Touchable from '../Components/Touchable/Touchable';
import View from '../Components/View/View';
import * as React from 'react';
type Props = $ReadOnly<{|
color: string,
color: ColorValue,
hitSlop: ?$ReadOnly<{|
bottom?: ?number,
left?: ?number,
@ -43,8 +45,12 @@ type Props = $ReadOnly<{|
export function PressabilityDebugView({color, hitSlop}: Props): React.Node {
if (__DEV__) {
if (isEnabled()) {
const normalizedColor = normalizeColor(color);
if (typeof normalizedColor !== 'number') {
return null;
}
const baseColor =
'#' + (normalizeColor(color) ?? 0).toString(16).padStart(8, '0');
'#' + (normalizedColor ?? 0).toString(16).padStart(8, '0');
return (
<View

Просмотреть файл

@ -125,7 +125,7 @@ class Share {
typeof content.message === 'string' ? content.message : undefined,
url: typeof content.url === 'string' ? content.url : undefined,
subject: options.subject,
tintColor: tintColor != null ? tintColor : undefined,
tintColor: typeof tintColor === 'number' ? tintColor : undefined,
excludedActivityTypes: options.excludedActivityTypes,
},
error => reject(error),

Просмотреть файл

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorValue} from './StyleSheetTypes';
import type {ProcessedColorValue} from './processColor';
export opaque type NativeColorValue = {
resource_paths?: Array<string>,
};
export const PlatformColor = (...names: Array<string>): ColorValue => {
return {resource_paths: names};
};
export const ColorAndroidPrivate = (color: string): ColorValue => {
return {resource_paths: [color]};
};
export const normalizeColorObject = (
color: NativeColorValue,
): ?ProcessedColorValue => {
if ('resource_paths' in color) {
return color;
}
return null;
};
export const processColorObject = (
color: NativeColorValue,
): ?NativeColorValue => {
return color;
};

Просмотреть файл

@ -0,0 +1,77 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorValue} from './StyleSheetTypes';
import type {ProcessedColorValue} from './processColor';
export opaque type NativeColorValue = {
semantic?: Array<string>,
dynamic?: {
light: ?(ColorValue | ProcessedColorValue),
dark: ?(ColorValue | ProcessedColorValue),
},
};
export const PlatformColor = (...names: Array<string>): ColorValue => {
return {semantic: names};
};
export type DynamicColorIOSTuplePrivate = {
light: ColorValue,
dark: ColorValue,
};
export const DynamicColorIOSPrivate = (
tuple: DynamicColorIOSTuplePrivate,
): ColorValue => {
return {dynamic: {light: tuple.light, dark: tuple.dark}};
};
export const normalizeColorObject = (
color: NativeColorValue,
): ?ProcessedColorValue => {
if ('semantic' in color) {
// an ios semantic color
return color;
} else if ('dynamic' in color && color.dynamic !== undefined) {
const normalizeColor = require('./normalizeColor');
// a dynamic, appearance aware color
const dynamic = color.dynamic;
const dynamicColor: NativeColorValue = {
dynamic: {
light: normalizeColor(dynamic.light),
dark: normalizeColor(dynamic.dark),
},
};
return dynamicColor;
}
return null;
};
export const processColorObject = (
color: NativeColorValue,
): ?NativeColorValue => {
if ('dynamic' in color && color.dynamic != null) {
const processColor = require('./processColor');
const dynamic = color.dynamic;
const dynamicColor: NativeColorValue = {
dynamic: {
light: processColor(dynamic.light),
dark: processColor(dynamic.dark),
},
};
return dynamicColor;
}
return color;
};

Просмотреть файл

@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorValue} from './StyleSheetTypes';
import {ColorAndroidPrivate} from './PlatformColorValueTypes';
export const ColorAndroid = (color: string): ColorValue => {
return ColorAndroidPrivate(color);
};

Просмотреть файл

@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorValue} from './StyleSheetTypes';
export const ColorAndroid = (color: string): ColorValue => {
throw new Error('ColorAndroid is not available on this platform.');
};

Просмотреть файл

@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorValue} from './StyleSheetTypes';
import {DynamicColorIOSPrivate} from './PlatformColorValueTypes';
export type DynamicColorIOSTuple = {
light: ColorValue,
dark: ColorValue,
};
export const DynamicColorIOS = (tuple: DynamicColorIOSTuple): ColorValue => {
return DynamicColorIOSPrivate({light: tuple.light, dark: tuple.dark});
};

Просмотреть файл

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import type {ColorValue} from './StyleSheetTypes';
export type DynamicColorIOSTuple = {
light: ColorValue,
dark: ColorValue,
};
export const DynamicColorIOS = (tuple: DynamicColorIOSTuple): ColorValue => {
throw new Error('DynamicColorIOS is not available on this platform.');
};

Просмотреть файл

@ -12,7 +12,10 @@
const AnimatedNode = require('../Animated/src/nodes/AnimatedNode');
export type ColorValue = null | string;
import type {NativeColorValue} from './PlatformColorValueTypes';
export type ColorValue = null | string | NativeColorValue;
export type ColorArrayValue = null | $ReadOnlyArray<ColorValue>;
export type PointValue = {|
x: number,

Просмотреть файл

@ -10,8 +10,16 @@
'use strict';
const {OS} = require('../../Utilities/Platform');
const normalizeColor = require('../normalizeColor');
const PlatformColorIOS = require('../PlatformColorValueTypes.ios')
.PlatformColor;
const DynamicColorIOS = require('../PlatformColorValueTypesIOS.ios')
.DynamicColorIOS;
const PlatformColorAndroid = require('../PlatformColorValueTypes.android')
.PlatformColor;
describe('normalizeColor', function() {
it('should accept only spec compliant colors', function() {
expect(normalizeColor('#abc')).not.toBe(null);
@ -128,4 +136,48 @@ describe('normalizeColor', function() {
const normalizedColor = normalizeColor('red') || 0;
expect(normalizeColor(normalizedColor)).toBe(normalizedColor);
});
describe('iOS', () => {
if (OS === 'ios') {
it('should normalize iOS PlatformColor colors', () => {
const color = PlatformColorIOS('systemRedColor');
const normalizedColor = normalizeColor(color);
const expectedColor = {semantic: ['systemRedColor']};
expect(normalizedColor).toEqual(expectedColor);
});
it('should normalize iOS Dynamic colors with named colors', () => {
const color = DynamicColorIOS({light: 'black', dark: 'white'});
const normalizedColor = normalizeColor(color);
const expectedColor = {dynamic: {light: 'black', dark: 'white'}};
expect(normalizedColor).toEqual(expectedColor);
});
it('should normalize iOS Dynamic colors with PlatformColor colors', () => {
const color = DynamicColorIOS({
light: PlatformColorIOS('systemBlackColor'),
dark: PlatformColorIOS('systemWhiteColor'),
});
const normalizedColor = normalizeColor(color);
const expectedColor = {
dynamic: {
light: {semantic: ['systemBlackColor']},
dark: {semantic: ['systemWhiteColor']},
},
};
expect(normalizedColor).toEqual(expectedColor);
});
}
});
describe('Android', () => {
if (OS === 'android') {
it('should normalize Android PlatformColor colors', () => {
const color = PlatformColorAndroid('?attr/colorPrimary');
const normalizedColor = normalizeColor(color);
const expectedColor = {resource_paths: ['?attr/colorPrimary']};
expect(normalizedColor).toEqual(expectedColor);
});
}
});
});

Просмотреть файл

@ -13,6 +13,13 @@
const {OS} = require('../../Utilities/Platform');
const processColor = require('../processColor');
const PlatformColorIOS = require('../PlatformColorValueTypes.ios')
.PlatformColor;
const DynamicColorIOS = require('../PlatformColorValueTypesIOS.ios')
.DynamicColorIOS;
const PlatformColorAndroid = require('../PlatformColorValueTypes.android')
.PlatformColor;
const platformSpecific =
OS === 'android'
? unsigned => unsigned | 0 //eslint-disable-line no-bitwise
@ -84,4 +91,33 @@ describe('processColor', () => {
expect(colorFromString).toEqual(platformSpecific(expectedInt));
});
});
describe('iOS', () => {
if (OS === 'ios') {
it('should process iOS PlatformColor colors', () => {
const color = PlatformColorIOS('systemRedColor');
const processedColor = processColor(color);
const expectedColor = {semantic: ['systemRedColor']};
expect(processedColor).toEqual(expectedColor);
});
it('should process iOS Dynamic colors', () => {
const color = DynamicColorIOS({light: 'black', dark: 'white'});
const processedColor = processColor(color);
const expectedColor = {dynamic: {light: 0xff000000, dark: 0xffffffff}};
expect(processedColor).toEqual(expectedColor);
});
}
});
describe('Android', () => {
if (OS === 'android') {
it('should process Android PlatformColor colors', () => {
const color = PlatformColorAndroid('?attr/colorPrimary');
const processedColor = processColor(color);
const expectedColor = {resource_paths: ['?attr/colorPrimary']};
expect(processedColor).toEqual(expectedColor);
});
}
});
});

Просмотреть файл

@ -13,6 +13,13 @@
const {OS} = require('../../Utilities/Platform');
const processColorArray = require('../processColorArray');
const PlatformColorIOS = require('../PlatformColorValueTypes.ios')
.PlatformColor;
const DynamicColorIOS = require('../PlatformColorValueTypesIOS.ios')
.DynamicColorIOS;
const PlatformColorAndroid = require('../PlatformColorValueTypes.android')
.PlatformColor;
const platformSpecific =
OS === 'android'
? unsigned => unsigned | 0 //eslint-disable-line no-bitwise
@ -57,4 +64,48 @@ describe('processColorArray', () => {
expect(colorFromNoArray).toEqual(null);
});
});
describe('iOS', () => {
if (OS === 'ios') {
it('should convert array of iOS PlatformColor colors', () => {
const colorFromArray = processColorArray([
PlatformColorIOS('systemColorWhite'),
PlatformColorIOS('systemColorBlack'),
]);
const expectedColorValueArray = [
{semantic: ['systemColorWhite']},
{semantic: ['systemColorBlack']},
];
expect(colorFromArray).toEqual(expectedColorValueArray);
});
it('should process iOS Dynamic colors', () => {
const colorFromArray = processColorArray([
DynamicColorIOS({light: 'black', dark: 'white'}),
DynamicColorIOS({light: 'white', dark: 'black'}),
]);
const expectedColorValueArray = [
{dynamic: {light: 0xff000000, dark: 0xffffffff}},
{dynamic: {light: 0xffffffff, dark: 0xff000000}},
];
expect(colorFromArray).toEqual(expectedColorValueArray);
});
}
});
describe('Android', () => {
if (OS === 'android') {
it('should convert array of Android PlatformColor colors', () => {
const colorFromArray = processColorArray([
PlatformColorAndroid('?attr/colorPrimary'),
PlatformColorAndroid('?colorPrimaryDark'),
]);
const expectedColorValueArray = [
{resource_paths: ['?attr/colorPrimary']},
{resource_paths: ['?colorPrimaryDark']},
];
expect(colorFromArray).toEqual(expectedColorValueArray);
});
}
});
});

Просмотреть файл

@ -12,7 +12,12 @@
'use strict';
function normalizeColor(color: string | number): ?number {
import type {ColorValue} from './StyleSheetTypes';
import type {ProcessedColorValue} from './processColor';
function normalizeColor(
color: ?(ColorValue | ProcessedColorValue),
): ?ProcessedColorValue {
const matchers = getMatchers();
let match;
@ -23,6 +28,21 @@ function normalizeColor(color: string | number): ?number {
return null;
}
if (typeof color === 'object' && color != null) {
const normalizeColorObject = require('./PlatformColorValueTypes')
.normalizeColorObject;
const normalizedColorObj = normalizeColorObject(color);
if (normalizedColorObj != null) {
return color;
}
}
if (typeof color !== 'string') {
return null;
}
// Ordered based on occurrences on Facebook codebase
if ((match = matchers.hex6.exec(color))) {
return parseInt(match[1] + 'ff', 16) >>> 0;

Просмотреть файл

@ -14,35 +14,48 @@ const Platform = require('../Utilities/Platform');
const normalizeColor = require('./normalizeColor');
// TODO: This is an empty object for now, just to enforce that everything using this
// downstream is correct. This will be replaced with an import to other files
// with a platform specific implementation. See the PR for more information
// https://github.com/facebook/react-native/pull/27908
opaque type NativeColorType = {};
export type ProcessedColorValue = ?number | NativeColorType;
import type {ColorValue} from './StyleSheetTypes';
import type {NativeColorValue} from './PlatformColorValueTypes';
export type ProcessedColorValue = number | NativeColorValue;
/* eslint no-bitwise: 0 */
function processColor(color?: ?(string | number)): ProcessedColorValue {
function processColor(color?: ?(number | ColorValue)): ?ProcessedColorValue {
if (color === undefined || color === null) {
return color;
}
let int32Color = normalizeColor(color);
if (int32Color === null || int32Color === undefined) {
let normalizedColor = normalizeColor(color);
if (normalizedColor === null || normalizedColor === undefined) {
return undefined;
}
if (typeof normalizedColor === 'object') {
const processColorObject = require('./PlatformColorValueTypes')
.processColorObject;
const processedColorObj = processColorObject(normalizedColor);
if (processedColorObj != null) {
return processedColorObj;
}
}
if (typeof normalizedColor !== 'number') {
return null;
}
// Converts 0xrrggbbaa into 0xaarrggbb
int32Color = ((int32Color << 24) | (int32Color >>> 8)) >>> 0;
normalizedColor = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0;
if (Platform.OS === 'android') {
// Android use 32 bit *signed* integer to represent the color
// We utilize the fact that bitwise operations in JS also operates on
// signed 32 bit integers, so that we can use those to convert from
// *unsigned* to *signed* 32bit int that way.
int32Color = int32Color | 0x0;
normalizedColor = normalizedColor | 0x0;
}
return int32Color;
return normalizedColor;
}
module.exports = processColor;

Просмотреть файл

@ -11,11 +11,13 @@
'use strict';
const processColor = require('./processColor');
import type {ColorValue} from './StyleSheetTypes';
import type {ProcessedColorValue} from './processColor';
function processColorArray(
colors: ?Array<string>,
): ?Array<ProcessedColorValue> {
colors: ?Array<ColorValue>,
): ?Array<?ProcessedColorValue> {
return colors == null ? null : colors.map(processColor);
}

Просмотреть файл

@ -96,6 +96,7 @@
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (!_textStorage) {
return;
}

Просмотреть файл

@ -12,6 +12,7 @@
272E6B3F1BEA849E001FCF37 /* UpdatePropertiesExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */; };
27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; };
2DDEF0101F84BF7B00DBDF73 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */; };
383889DA23A7398900D06C3E /* RCTConvert_UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */; };
3D2AFAF51D646CF80089D1A3 /* legacy_image@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3D2AFAF41D646CF80089D1A3 /* legacy_image@2x.png */; };
5C60EB1C226440DB0018C04F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5C60EB1B226440DB0018C04F /* AppDelegate.mm */; };
5CB07C9B226467E60039471C /* RNTesterTurboModuleProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5CB07C99226467E60039471C /* RNTesterTurboModuleProvider.mm */; };
@ -81,6 +82,7 @@
27F441EA1BEBE5030039B79C /* FlexibleSizeExampleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FlexibleSizeExampleView.h; path = RNTester/NativeExampleViews/FlexibleSizeExampleView.h; sourceTree = "<group>"; };
2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RNTester/Images.xcassets; sourceTree = "<group>"; };
34028D6B10F47E490042EB27 /* Pods-RNTesterUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests.debug.xcconfig"; sourceTree = "<group>"; };
383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_UIColorTests.m; sourceTree = "<group>"; };
3D2AFAF41D646CF80089D1A3 /* legacy_image@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "legacy_image@2x.png"; path = "RNTester/legacy_image@2x.png"; sourceTree = "<group>"; };
5BEC8567F3741044B6A5EFC5 /* Pods-RNTester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.release.xcconfig"; path = "Pods/Target Support Files/Pods-RNTester/Pods-RNTester.release.xcconfig"; sourceTree = "<group>"; };
5C60EB1B226440DB0018C04F /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RNTester/AppDelegate.mm; sourceTree = "<group>"; };
@ -324,6 +326,7 @@
E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */,
E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */,
E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */,
383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */,
E7DB20C822B2BAA5005AC45F /* RCTDevMenuTests.m */,
E7DB20C022B2BAA4005AC45F /* RCTEventDispatcherTests.m */,
E7DB20AF22B2BAA4005AC45F /* RCTFontTests.m */,
@ -686,6 +689,7 @@
E7DB20D322B2BAA6005AC45F /* RCTBlobManagerTests.m in Sources */,
E7DB20DC22B2BAA6005AC45F /* RCTUIManagerTests.m in Sources */,
E7DB20E322B2BAA6005AC45F /* RCTAllocationTests.m in Sources */,
383889DA23A7398900D06C3E /* RCTConvert_UIColorTests.m in Sources */,
E7DB20E622B2BAA6005AC45F /* RCTImageLoaderHelpers.m in Sources */,
E7DB20D622B2BAA6005AC45F /* RCTFontTests.m in Sources */,
E7DB20DB22B2BAA6005AC45F /* RCTNativeAnimatedNodesManagerTests.m in Sources */,

Просмотреть файл

@ -0,0 +1,200 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <XCTest/XCTest.h>
#import <React/RCTConvert.h>
@interface RCTConvert_NSColorTests : XCTestCase
@end
static BOOL CGColorsAreEqual(CGColorRef color1, CGColorRef color2) {
CGFloat rgba1[4];
CGFloat rgba2[4];
RCTGetRGBAColorComponents(color1, rgba1);
RCTGetRGBAColorComponents(color2, rgba2);
for (int i = 0; i < 4; i++) {
if (rgba1[i] != rgba2[i]) {
return NO;
}
}
return YES;
}
@implementation RCTConvert_NSColorTests
- (void)testColor
{
id json = RCTJSONParse(@"{ \"semantic\": \"lightTextColor\" }", nil);
UIColor *value = [RCTConvert UIColor:json];
XCTAssertEqualObjects(value, [UIColor lightTextColor]);
}
- (void)testColorFailure
{
id json = RCTJSONParse(@"{ \"semantic\": \"bogusColor\" }", nil);
__block NSString *errorMessage = nil;
RCTLogFunction defaultLogFunction = RCTGetLogFunction();
RCTSetLogFunction(^(__unused RCTLogLevel level, __unused RCTLogSource source, __unused NSString *fileName, __unused NSNumber *lineNumber, NSString *message) {
errorMessage = message;
});
UIColor *value = [RCTConvert UIColor:json];
RCTSetLogFunction(defaultLogFunction);
XCTAssertEqualObjects(value, nil);
XCTAssertTrue([errorMessage containsString:@"labelColor"]); // the RedBox message will contain a list of the valid color names.
}
- (void)testFallbackColor
{
id json = RCTJSONParse(@"{ \"semantic\": \"unitTestFallbackColorIOS\" }", nil);
UIColor *value = [RCTConvert UIColor:json];
XCTAssertTrue(CGColorsAreEqual([value CGColor], [[UIColor blueColor] CGColor]));
}
- (void)testDynamicColor
{
// 0 == 0x00000000 == black
// 16777215 == 0x00FFFFFF == white
id json = RCTJSONParse(@"{ \"dynamic\": { \"light\":0, \"dark\":16777215 } }", nil);
UIColor *value = [RCTConvert UIColor:json];
XCTAssertNotNil(value);
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
id savedTraitCollection = [UITraitCollection currentTraitCollection];
[UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]];
CGFloat rgba[4];
RCTGetRGBAColorComponents([value CGColor], rgba);
XCTAssertEqual(rgba[0], 0);
XCTAssertEqual(rgba[1], 0);
XCTAssertEqual(rgba[2], 0);
XCTAssertEqual(rgba[3], 0);
[UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]];
RCTGetRGBAColorComponents([value CGColor], rgba);
XCTAssertEqual(rgba[0], 1);
XCTAssertEqual(rgba[1], 1);
XCTAssertEqual(rgba[2], 1);
XCTAssertEqual(rgba[3], 0);
[UITraitCollection setCurrentTraitCollection:savedTraitCollection];
}
#endif
}
- (void)testCompositeDynamicColor
{
id json = RCTJSONParse(@"{ \"dynamic\": { \"light\": { \"semantic\": \"systemRedColor\" }, \"dark\":{ \"semantic\": \"systemBlueColor\" } } }", nil);
UIColor *value = [RCTConvert UIColor:json];
XCTAssertNotNil(value);
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
id savedTraitCollection = [UITraitCollection currentTraitCollection];
[UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]];
XCTAssertTrue(CGColorsAreEqual([value CGColor], [[UIColor systemRedColor] CGColor]));
[UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]];
XCTAssertTrue(CGColorsAreEqual([value CGColor], [[UIColor systemBlueColor] CGColor]));
[UITraitCollection setCurrentTraitCollection:savedTraitCollection];
}
#endif
}
- (void)testGenerateFallbacks
{
NSDictionary<NSString *, NSNumber*>* semanticColors = @{
// https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors
// Label Colors
@"labelColor": @(0xFF000000),
@"secondaryLabelColor": @(0x993c3c43),
@"tertiaryLabelColor": @(0x4c3c3c43),
@"quaternaryLabelColor": @(0x2d3c3c43),
// Fill Colors
@"systemFillColor": @(0x33787880),
@"secondarySystemFillColor": @(0x28787880),
@"tertiarySystemFillColor": @(0x1e767680),
@"quaternarySystemFillColor": @(0x14747480),
// Text Colors
@"placeholderTextColor": @(0x4c3c3c43),
// Standard Content Background Colors
@"systemBackgroundColor": @(0xFFffffff),
@"secondarySystemBackgroundColor": @(0xFFf2f2f7),
@"tertiarySystemBackgroundColor": @(0xFFffffff),
// Grouped Content Background Colors
@"systemGroupedBackgroundColor": @(0xFFf2f2f7),
@"secondarySystemGroupedBackgroundColor": @(0xFFffffff),
@"tertiarySystemGroupedBackgroundColor": @(0xFFf2f2f7),
// Separator Colors
@"separatorColor": @(0x493c3c43),
@"opaqueSeparatorColor": @(0xFFc6c6c8),
// Link Color
@"linkColor": @(0xFF007aff),
// https://developer.apple.com/documentation/uikit/uicolor/standard_colors
// Adaptable Colors
@"systemBrownColor": @(0xFFa2845e),
@"systemIndigoColor": @(0xFF5856d6),
// Adaptable Gray Colors
@"systemGray2Color": @(0xFFaeaeb2),
@"systemGray3Color": @(0xFFc7c7cc),
@"systemGray4Color": @(0xFFd1d1d6),
@"systemGray5Color": @(0xFFe5e5ea),
@"systemGray6Color": @(0xFFf2f2f7),
};
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
id savedTraitCollection = nil;
if (@available(iOS 13.0, *)) {
savedTraitCollection = [UITraitCollection currentTraitCollection];
[UITraitCollection setCurrentTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]];
}
#endif
for (NSString *semanticColor in semanticColors) {
id json = RCTJSONParse([NSString stringWithFormat:@"{ \"semantic\": \"%@\" }", semanticColor], nil);
UIColor *value = [RCTConvert UIColor:json];
XCTAssertNotNil(value);
NSNumber *fallback = [semanticColors objectForKey:semanticColor];
NSUInteger rgbValue = [fallback unsignedIntegerValue];
NSUInteger alpha1 = ((rgbValue & 0xFF000000) >> 24);
NSUInteger red1 = ((rgbValue & 0x00FF0000) >> 16);
NSUInteger green1 = ((rgbValue & 0x0000FF00) >> 8);
NSUInteger blue1 = ((rgbValue & 0x000000FF) >> 0);
CGFloat rgba[4];
RCTGetRGBAColorComponents([value CGColor], rgba);
NSUInteger red2 = rgba[0] * 255;
NSUInteger green2 = rgba[1] * 255;
NSUInteger blue2 = rgba[2] * 255;
NSUInteger alpha2 = rgba[3] * 255;
XCTAssertEqual(red1, red2);
XCTAssertEqual(green1, green2);
XCTAssertEqual(blue1, blue2);
XCTAssertEqual(alpha1, alpha2);
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
[UITraitCollection setCurrentTraitCollection:savedTraitCollection];
}
#endif
}
@end

Просмотреть файл

@ -12,28 +12,29 @@
import * as React from 'react';
import {Appearance} from 'react-native';
import type {ColorValue} from '../../../Libraries/StyleSheet/StyleSheetTypes';
export type RNTesterTheme = {
LabelColor: string,
SecondaryLabelColor: string,
TertiaryLabelColor: string,
QuaternaryLabelColor: string,
PlaceholderTextColor: string,
SystemBackgroundColor: string,
SecondarySystemBackgroundColor: string,
TertiarySystemBackgroundColor: string,
GroupedBackgroundColor: string,
SecondaryGroupedBackgroundColor: string,
TertiaryGroupedBackgroundColor: string,
SystemFillColor: string,
SecondarySystemFillColor: string,
TertiarySystemFillColor: string,
QuaternarySystemFillColor: string,
SeparatorColor: string,
OpaqueSeparatorColor: string,
LinkColor: string,
SystemPurpleColor: string,
ToolbarColor: string,
LabelColor: ColorValue,
SecondaryLabelColor: ColorValue,
TertiaryLabelColor: ColorValue,
QuaternaryLabelColor: ColorValue,
PlaceholderTextColor: ColorValue,
SystemBackgroundColor: ColorValue,
SecondarySystemBackgroundColor: ColorValue,
TertiarySystemBackgroundColor: ColorValue,
GroupedBackgroundColor: ColorValue,
SecondaryGroupedBackgroundColor: ColorValue,
TertiaryGroupedBackgroundColor: ColorValue,
SystemFillColor: ColorValue,
SecondarySystemFillColor: ColorValue,
TertiarySystemFillColor: ColorValue,
QuaternarySystemFillColor: ColorValue,
SeparatorColor: ColorValue,
OpaqueSeparatorColor: ColorValue,
LinkColor: ColorValue,
SystemPurpleColor: ColorValue,
ToolbarColor: ColorValue,
...
};

Просмотреть файл

@ -191,7 +191,9 @@ exports.examples = [
paddingVertical: 2,
color: theme.LabelColor,
}}>
{theme[key]}
{typeof theme[key] === 'string'
? theme[key]
: JSON.stringify(theme[key])}
</Text>
</View>
</View>

Просмотреть файл

@ -0,0 +1,355 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const React = require('react');
const ReactNative = require('react-native');
import Platform from '../../../../Libraries/Utilities/Platform';
const {
ColorAndroid,
DynamicColorIOS,
PlatformColor,
StyleSheet,
Text,
View,
} = ReactNative;
function PlatformColorsExample() {
function createTable() {
let colors = [];
if (Platform.OS === 'ios') {
colors = [
// https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors
// Label Colors
{label: 'labelColor', color: PlatformColor('labelColor')},
{
label: 'secondaryLabelColor',
color: PlatformColor('secondaryLabelColor'),
},
{
label: 'tertiaryLabelColor',
color: PlatformColor('tertiaryLabelColor'),
},
{
label: 'quaternaryLabelColor',
color: PlatformColor('quaternaryLabelColor'),
},
// Fill Colors
{label: 'systemFillColor', color: PlatformColor('systemFillColor')},
{
label: 'secondarySystemFillColor',
color: PlatformColor('secondarySystemFillColor'),
},
{
label: 'tertiarySystemFillColor',
color: PlatformColor('tertiarySystemFillColor'),
},
{
label: 'quaternarySystemFillColor',
color: PlatformColor('quaternarySystemFillColor'),
},
// Text Colors
{
label: 'placeholderTextColor',
color: PlatformColor('placeholderTextColor'),
},
// Standard Content Background Colors
{
label: 'systemBackgroundColor',
color: PlatformColor('systemBackgroundColor'),
},
{
label: 'secondarySystemBackgroundColor',
color: PlatformColor('secondarySystemBackgroundColor'),
},
{
label: 'tertiarySystemBackgroundColor',
color: PlatformColor('tertiarySystemBackgroundColor'),
},
// Grouped Content Background Colors
{
label: 'systemGroupedBackgroundColor',
color: PlatformColor('systemGroupedBackgroundColor'),
},
{
label: 'secondarySystemGroupedBackgroundColor',
color: PlatformColor('secondarySystemGroupedBackgroundColor'),
},
{
label: 'tertiarySystemGroupedBackgroundColor',
color: PlatformColor('tertiarySystemGroupedBackgroundColor'),
},
// Separator Colors
{label: 'separatorColor', color: PlatformColor('separatorColor')},
{
label: 'opaqueSeparatorColor',
color: PlatformColor('opaqueSeparatorColor'),
},
// Link Color
{label: 'linkColor', color: PlatformColor('linkColor')},
// Nonadaptable Colors
{label: 'darkTextColor', color: PlatformColor('darkTextColor')},
{label: 'lightTextColor', color: PlatformColor('lightTextColor')},
// https://developer.apple.com/documentation/uikit/uicolor/standard_colors
// Adaptable Colors
{label: 'systemBlueColor', color: PlatformColor('systemBlueColor')},
{label: 'systemBrownColor', color: PlatformColor('systemBrownColor')},
{label: 'systemGreenColor', color: PlatformColor('systemGreenColor')},
{label: 'systemIndigoColor', color: PlatformColor('systemIndigoColor')},
{label: 'systemOrangeColor', color: PlatformColor('systemOrangeColor')},
{label: 'systemPinkColor', color: PlatformColor('systemPinkColor')},
{label: 'systemPurpleColor', color: PlatformColor('systemPurpleColor')},
{label: 'systemRedColor', color: PlatformColor('systemRedColor')},
{label: 'systemTealColor', color: PlatformColor('systemTealColor')},
{label: 'systemYellowColor', color: PlatformColor('systemYellowColor')},
// Adaptable Gray Colors
{label: 'systemGrayColor', color: PlatformColor('systemGrayColor')},
{label: 'systemGray2Color', color: PlatformColor('systemGray2Color')},
{label: 'systemGray3Color', color: PlatformColor('systemGray3Color')},
{label: 'systemGray4Color', color: PlatformColor('systemGray4Color')},
{label: 'systemGray5Color', color: PlatformColor('systemGray5Color')},
{label: 'systemGray6Color', color: PlatformColor('systemGray6Color')},
];
} else if (Platform.OS === 'android') {
colors = [
{label: '?attr/colorAccent', color: PlatformColor('?attr/colorAccent')},
{
label: '?attr/colorBackgroundFloating',
color: PlatformColor('?attr/colorBackgroundFloating'),
},
{
label: '?attr/colorButtonNormal',
color: PlatformColor('?attr/colorButtonNormal'),
},
{
label: '?attr/colorControlActivated',
color: PlatformColor('?attr/colorControlActivated'),
},
{
label: '?attr/colorControlHighlight',
color: PlatformColor('?attr/colorControlHighlight'),
},
{
label: '?attr/colorControlNormal',
color: PlatformColor('?attr/colorControlNormal'),
},
{
label: '?android:colorError',
color: PlatformColor('?android:colorError'),
},
{
label: '?android:attr/colorError',
color: PlatformColor('?android:attr/colorError'),
},
{
label: '?attr/colorPrimary',
color: PlatformColor('?attr/colorPrimary'),
},
{label: '?colorPrimaryDark', color: PlatformColor('?colorPrimaryDark')},
{
label: '@android:color/holo_purple',
color: PlatformColor('@android:color/holo_purple'),
},
{
label: '@android:color/holo_green_light',
color: PlatformColor('@android:color/holo_green_light'),
},
{
label: '@color/catalyst_redbox_background',
color: PlatformColor('@color/catalyst_redbox_background'),
},
{
label: '@color/catalyst_logbox_background',
color: PlatformColor('@color/catalyst_logbox_background'),
},
];
}
let table = [];
for (let color of colors) {
table.push(
<View style={styles.row} key={color.label}>
<Text style={styles.labelCell}>{color.label}</Text>
<View
style={{
...styles.colorCell,
backgroundColor: color.color,
}}
/>
</View>,
);
}
return table;
}
return <View style={styles.column}>{createTable()}</View>;
}
function FallbackColorsExample() {
let color = {};
if (Platform.OS === 'ios') {
color = {
label: "PlatformColor('bogus', 'systemGreenColor')",
color: PlatformColor('bogus', 'systemGreenColor'),
};
} else if (Platform.OS === 'android') {
color = {
label: "PlatformColor('bogus', '@color/catalyst_redbox_background')",
color: PlatformColor('bogus', '@color/catalyst_redbox_background'),
};
} else {
throw 'Unexpected Platform.OS: ' + Platform.OS;
}
return (
<View style={styles.column}>
<View style={styles.row}>
<Text style={styles.labelCell}>{color.label}</Text>
<View
style={{
...styles.colorCell,
backgroundColor: color.color,
}}
/>
</View>
</View>
);
}
function DynamicColorsExample() {
return Platform.OS === 'ios' ? (
<View style={styles.column}>
<View style={styles.row}>
<Text style={styles.labelCell}>
DynamicColorIOS({'{\n'}
{' '}light: 'red', dark: 'blue'{'\n'}
{'}'})
</Text>
<View
style={{
...styles.colorCell,
backgroundColor: DynamicColorIOS({light: 'red', dark: 'blue'}),
}}
/>
</View>
<View style={styles.row}>
<Text style={styles.labelCell}>
DynamicColorIOS({'{\n'}
{' '}light: PlatformColor('systemBlueColor'),{'\n'}
{' '}dark: PlatformColor('systemRedColor'),{'\n'}
{'}'})
</Text>
<View
style={{
...styles.colorCell,
backgroundColor: DynamicColorIOS({
light: PlatformColor('systemBlueColor'),
dark: PlatformColor('systemRedColor'),
}),
}}
/>
</View>
</View>
) : (
<Text style={styles.labelCell}>Not applicable on this platform</Text>
);
}
function AndroidColorsExample() {
return Platform.OS === 'android' ? (
<View style={styles.column}>
<View style={styles.row}>
<Text style={styles.labelCell}>ColorAndroid('?attr/colorAccent')</Text>
<View
style={{
...styles.colorCell,
backgroundColor: ColorAndroid('?attr/colorAccent'),
}}
/>
</View>
</View>
) : (
<Text style={styles.labelCell}>Not applicable on this platform</Text>
);
}
function VariantColorsExample() {
return (
<View style={styles.column}>
<View style={styles.row}>
<Text style={styles.labelCell}>
{Platform.OS === 'ios'
? "DynamicColorIOS({light: 'red', dark: 'blue'})"
: "ColorAndroid('?attr/colorAccent')"}
</Text>
<View
style={{
...styles.colorCell,
backgroundColor:
Platform.OS === 'ios'
? DynamicColorIOS({light: 'red', dark: 'blue'})
: ColorAndroid('?attr/colorAccent'),
}}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
column: {flex: 1, flexDirection: 'column'},
row: {flex: 0.75, flexDirection: 'row'},
labelCell: {
flex: 1,
alignItems: 'stretch',
...Platform.select({
ios: {color: PlatformColor('labelColor')},
default: {color: 'black'},
}),
},
colorCell: {flex: 0.25, alignItems: 'stretch'},
});
exports.title = 'PlatformColor';
exports.description =
'Examples that show how PlatformColors may be used in an app.';
exports.examples = [
{
title: 'Platform Colors',
render(): React.Element<any> {
return <PlatformColorsExample />;
},
},
{
title: 'Fallback Colors',
render(): React.Element<any> {
return <FallbackColorsExample />;
},
},
{
title: 'iOS Dynamic Colors',
render(): React.Element<any> {
return <DynamicColorsExample />;
},
},
{
title: 'Android Colors',
render(): React.Element<any> {
return <AndroidColorsExample />;
},
},
{
title: 'Variant Colors',
render(): React.Element<any> {
return <VariantColorsExample />;
},
},
];

Просмотреть файл

@ -200,6 +200,10 @@ const APIExamples: Array<RNTesterExample> = [
key: 'PermissionsExampleAndroid',
module: require('../examples/PermissionsAndroid/PermissionsExample'),
},
{
key: 'PlatformColorExample',
module: require('../examples/PlatformColor/PlatformColorExample'),
},
{
key: 'PointerEventsExample',
module: require('../examples/PointerEvents/PointerEventsExample'),

Просмотреть файл

@ -279,6 +279,11 @@ const APIExamples: Array<RNTesterExample> = [
module: require('../examples/PanResponder/PanResponderExample'),
supportsTVOS: false,
},
{
key: 'PlatformColorExample',
module: require('../examples/PlatformColor/PlatformColorExample'),
supportsTVOS: true,
},
{
key: 'PointerEventsExample',
module: require('../examples/PointerEvents/PointerEventsExample'),

Просмотреть файл

@ -506,6 +506,207 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
@"a", @"b", @"c", @"d", @"tx", @"ty"
]))
static NSString *const RCTFallback = @"fallback";
static NSString *const RCTFallbackARGB = @"fallback-argb";
static NSString *const RCTSelector = @"selector";
static NSString *const RCTIndex = @"index";
/** The following dictionary defines the react-native semantic colors for ios.
* If the value for a given name is empty then the name itself
* is used as the UIColor selector.
* If the RCTSelector key is present then that value is used for a selector instead
* of the key name.
* If the given selector is not available on the running OS version then
* the RCTFallback selector is used instead.
* If the RCTIndex key is present then object returned from UIColor is an
* NSArray and the object at index RCTIndex is to be used.
*/
static NSDictionary<NSString *, NSDictionary *>* RCTSemanticColorsMap()
{
static NSDictionary<NSString *, NSDictionary *> *colorMap = nil;
if (colorMap == nil) {
colorMap = @{
// https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors
// Label Colors
@"labelColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFF000000) // fallback for iOS<=12: RGBA returned by this semantic color in light mode on iOS 13
},
@"secondaryLabelColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x993c3c43)
},
@"tertiaryLabelColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x4c3c3c43)
},
@"quaternaryLabelColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x2d3c3c43)
},
// Fill Colors
@"systemFillColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x33787880)
},
@"secondarySystemFillColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x28787880)
},
@"tertiarySystemFillColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x1e767680)
},
@"quaternarySystemFillColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x14747480)
},
// Text Colors
@"placeholderTextColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x4c3c3c43)
},
// Standard Content Background Colors
@"systemBackgroundColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFffffff)
},
@"secondarySystemBackgroundColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFf2f2f7)
},
@"tertiarySystemBackgroundColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFffffff)
},
// Grouped Content Background Colors
@"systemGroupedBackgroundColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFf2f2f7)
},
@"secondarySystemGroupedBackgroundColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFffffff)
},
@"tertiarySystemGroupedBackgroundColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFf2f2f7)
},
// Separator Colors
@"separatorColor": @{ // iOS 13.0
RCTFallbackARGB: @(0x493c3c43)
},
@"opaqueSeparatorColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFc6c6c8)
},
// Link Color
@"linkColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFF007aff)
},
// Nonadaptable Colors
@"darkTextColor": @{},
@"lightTextColor": @{},
// https://developer.apple.com/documentation/uikit/uicolor/standard_colors
// Adaptable Colors
@"systemBlueColor": @{},
@"systemBrownColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFa2845e)
},
@"systemGreenColor": @{},
@"systemIndigoColor": @{ // iOS 13.0
RCTFallbackARGB: @(0xFF5856d6)
},
@"systemOrangeColor": @{},
@"systemPinkColor": @{},
@"systemPurpleColor": @{},
@"systemRedColor": @{},
@"systemTealColor": @{},
@"systemYellowColor": @{},
// Adaptable Gray Colors
@"systemGrayColor": @{},
@"systemGray2Color": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFaeaeb2)
},
@"systemGray3Color": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFc7c7cc)
},
@"systemGray4Color": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFd1d1d6)
},
@"systemGray5Color": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFe5e5ea)
},
@"systemGray6Color": @{ // iOS 13.0
RCTFallbackARGB: @(0xFFf2f2f7)
},
#if DEBUG
// The follow exist for Unit Tests
@"unitTestFallbackColor": @{
RCTFallback: @"gridColor"
},
@"unitTestFallbackColorIOS": @{
RCTFallback: @"blueColor"
},
@"unitTestFallbackColorEven": @{
RCTSelector: @"unitTestFallbackColorEven",
RCTIndex: @0,
RCTFallback: @"controlAlternatingRowBackgroundColors"
},
@"unitTestFallbackColorOdd": @{
RCTSelector: @"unitTestFallbackColorOdd",
RCTIndex: @1,
RCTFallback: @"controlAlternatingRowBackgroundColors"
},
#endif
};
}
return colorMap;
}
/** Returns a UIColor based on a semantic color name.
* Returns nil if the semantic color name is invalid.
*/
static UIColor *RCTColorFromSemanticColorName(NSString *semanticColorName)
{
NSDictionary<NSString *, NSDictionary *> *colorMap = RCTSemanticColorsMap();
UIColor *color = nil;
NSDictionary<NSString *, id> *colorInfo = colorMap[semanticColorName];
if (colorInfo) {
NSString *semanticColorSelector = colorInfo[RCTSelector];
if (semanticColorSelector == nil) {
semanticColorSelector = semanticColorName;
}
SEL selector = NSSelectorFromString(semanticColorSelector);
if (![UIColor respondsToSelector:selector]) {
NSNumber *fallbackRGB = colorInfo[RCTFallbackARGB];
if (fallbackRGB != nil) {
RCTAssert([fallbackRGB isKindOfClass:[NSNumber class]], @"fallback ARGB is not a number");
return [RCTConvert UIColor:fallbackRGB];
}
semanticColorSelector = colorInfo[RCTFallback];
selector = NSSelectorFromString(semanticColorSelector);
}
RCTAssert ([UIColor respondsToSelector:selector], @"RCTUIColor does not respond to a semantic color selector.");
Class klass = [UIColor class];
IMP imp = [klass methodForSelector:selector];
id (*getSemanticColorObject)(id, SEL) = (void *)imp;
id colorObject = getSemanticColorObject(klass, selector);
if ([colorObject isKindOfClass:[UIColor class]]) {
color = colorObject;
} else if ([colorObject isKindOfClass:[NSArray class]]) {
NSArray *colors = colorObject;
NSNumber *index = colorInfo[RCTIndex];
RCTAssert(index, @"index should not be null");
color = colors[[index unsignedIntegerValue]];
} else {
RCTAssert(false, @"selector return an unknown object type");
}
}
return color;
}
/** Returns an alphabetically sorted comma seperated list of the valid semantic color names
*/
static NSString *RCTSemanticColorNames()
{
NSMutableString *names = [[NSMutableString alloc] init];
NSDictionary<NSString *, NSDictionary *> *colorMap = RCTSemanticColorsMap();
NSArray *allKeys = [[[colorMap allKeys] mutableCopy] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
for(id key in allKeys) {
if ([names length]) {
[names appendString:@", "];
}
[names appendString:key];
}
return names;
}
+ (UIColor *)UIColor:(id)json
{
if (!json) {
@ -525,6 +726,56 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
CGFloat g = ((argb >> 8) & 0xFF) / 255.0;
CGFloat b = (argb & 0xFF) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:a];
} else if ([json isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = json;
id value = nil;
if ((value = [dictionary objectForKey:@"semantic"])) {
if ([value isKindOfClass:[NSString class]]) {
NSString *semanticName = value;
UIColor *color = RCTColorFromSemanticColorName(semanticName);
if (color == nil) {
RCTLogConvertError(json, [@"a UIColor. Expected one of the following values: " stringByAppendingString:RCTSemanticColorNames()]);
}
return color;
} else if ([value isKindOfClass:[NSArray class]]) {
for (id name in value) {
UIColor *color = RCTColorFromSemanticColorName(name);
if (color != nil) {
return color;
}
}
RCTLogConvertError(json, [@"a UIColor. None of the names in the array were one of the following values: " stringByAppendingString:RCTSemanticColorNames()]);
return nil;
}
RCTLogConvertError(json, @"a UIColor. Expected either a single name or an array of names but got something else.");
return nil;
} else if ((value = [dictionary objectForKey:@"dynamic"])) {
NSDictionary *appearances = value;
id light = [appearances objectForKey:@"light"];
UIColor *lightColor = [RCTConvert UIColor:light];
id dark = [appearances objectForKey:@"dark"];
UIColor *darkColor = [RCTConvert UIColor:dark];
if (lightColor != nil && darkColor != nil) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
UIColor *color = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull collection) {
return collection.userInterfaceStyle == UIUserInterfaceStyleDark ? darkColor : lightColor;
}];
return color;
} else {
#endif
return lightColor;
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
}
#endif
} else {
RCTLogConvertError(json, @"a UIColor. Expected an iOS dynamic appearance aware color.");
return nil;
}
} else {
RCTLogConvertError(json, @"a UIColor. Expected an iOS semantic color or dynamic appearance aware color.");
return nil;
}
} else {
RCTLogConvertError(json, @"a UIColor. Did you forget to call processColor() on the JS side?");
return nil;

Просмотреть файл

@ -141,6 +141,9 @@ RCT_EXTERN UIImage *__nullable RCTImageFromLocalBundleAssetURL(NSURL *imageURL);
// Creates a new, unique temporary file path with the specified extension
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *__nullable extension, NSError **error);
// Get RGBA components of CGColor
RCT_EXTERN void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[_Nonnull 4]);
// Converts a CGColor to a hex string
RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color);

Просмотреть файл

@ -832,7 +832,7 @@ RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *extension, NSError **e
return [directory stringByAppendingPathComponent:filename];
}
static void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4])
RCT_EXTERN void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4])
{
CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color));
const CGFloat *components = CGColorGetComponents(color);

Просмотреть файл

@ -608,6 +608,17 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange: previousTraitCollection];
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[self.layer setNeedsDisplay];
}
}
#endif
}
#pragma mark - Borders
- (UIColor *)backgroundColor
@ -783,11 +794,22 @@ static CGFloat RCTDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x) {
// solve this, we'll need to add a container view inside the main view to
// correctly clip the subviews.
CGColorRef backgroundColor;
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor;
} else {
backgroundColor = _backgroundColor.CGColor;
}
#else
backgroundColor = _backgroundColor.CGColor;
#endif
if (useIOSBorderRendering) {
layer.cornerRadius = cornerRadii.topLeft;
layer.borderColor = borderColors.left;
layer.borderWidth = borderInsets.left;
layer.backgroundColor = _backgroundColor.CGColor;
layer.backgroundColor = backgroundColor;
layer.contents = nil;
layer.needsDisplayOnBoundsChange = NO;
layer.mask = nil;
@ -799,7 +821,7 @@ static CGFloat RCTDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x) {
cornerRadii,
borderInsets,
borderColors,
_backgroundColor.CGColor,
backgroundColor,
self.clipsToBounds);
layer.backgroundColor = NULL;

Просмотреть файл

@ -441,6 +441,7 @@ dependencies {
api("com.facebook.yoga:proguard-annotations:1.14.1")
api("javax.inject:javax.inject:1")
api("androidx.appcompat:appcompat:1.0.2")
api("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
api("com.facebook.fresco:fresco:${FRESCO_VERSION}")
api("com.facebook.fresco:imagepipeline-okhttp3:${FRESCO_VERSION}")
api("com.facebook.soloader:soloader:${SO_LOADER_VERSION}")

Просмотреть файл

@ -0,0 +1,124 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.bridge;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import androidx.core.content.res.ResourcesCompat;
public class ColorPropConverter {
private static final String JSON_KEY = "resource_paths";
private static final String PREFIX_RESOURCE = "@";
private static final String PREFIX_ATTR = "?";
private static final String PACKAGE_DELIMITER = ":";
private static final String PATH_DELIMITER = "/";
private static final String ATTR = "attr";
private static final String ATTR_SEGMENT = "attr/";
public static Integer getColor(Object value, Context context) {
if (value == null) {
return null;
}
if (value instanceof Double) {
return ((Double) value).intValue();
}
if (context == null) {
throw new RuntimeException("Context may not be null.");
}
if (value instanceof ReadableMap) {
ReadableMap map = (ReadableMap) value;
ReadableArray resourcePaths = map.getArray(JSON_KEY);
if (resourcePaths == null) {
throw new JSApplicationCausedNativeException(
"ColorValue: The `" + JSON_KEY + "` must be an array of color resource path strings.");
}
for (int i = 0; i < resourcePaths.size(); i++) {
String resourcePath = resourcePaths.getString(i);
if (resourcePath == null || resourcePath.isEmpty()) {
continue;
}
boolean isResource = resourcePath.startsWith(PREFIX_RESOURCE);
boolean isThemeAttribute = resourcePath.startsWith(PREFIX_ATTR);
resourcePath = resourcePath.substring(1);
try {
if (isResource) {
return resolveResource(context, resourcePath);
} else if (isThemeAttribute) {
return resolveThemeAttribute(context, resourcePath);
}
} catch (Resources.NotFoundException exception) {
// The resource could not be found so do nothing to allow the for loop to continue and
// try the next fallback resource in the array. If none of the fallbacks are
// found then the exception immediately after the for loop will be thrown.
}
}
throw new JSApplicationCausedNativeException(
"ColorValue: None of the paths in the `"
+ JSON_KEY
+ "` array resolved to a color resource.");
}
throw new JSApplicationCausedNativeException(
"ColorValue: the value must be a number or Object.");
}
private static int resolveResource(Context context, String resourcePath) {
String[] pathTokens = resourcePath.split(PACKAGE_DELIMITER);
String packageName = context.getPackageName();
String resource = resourcePath;
if (pathTokens.length > 1) {
packageName = pathTokens[0];
resource = pathTokens[1];
}
String[] resourceTokens = resource.split(PATH_DELIMITER);
String resourceType = resourceTokens[0];
String resourceName = resourceTokens[1];
int resourceId = context.getResources().getIdentifier(resourceName, resourceType, packageName);
return ResourcesCompat.getColor(context.getResources(), resourceId, context.getTheme());
}
private static int resolveThemeAttribute(Context context, String resourcePath) {
String path = resourcePath.replaceAll(ATTR_SEGMENT, "");
String[] pathTokens = path.split(PACKAGE_DELIMITER);
String packageName = context.getPackageName();
String resourceName = path;
if (pathTokens.length > 1) {
packageName = pathTokens[0];
resourceName = pathTokens[1];
}
int resourceId = context.getResources().getIdentifier(resourceName, ATTR, packageName);
TypedValue outValue = new TypedValue();
Resources.Theme theme = context.getTheme();
if (theme.resolveAttribute(resourceId, outValue, true)) {
return outValue.data;
}
throw new Resources.NotFoundException();
}
}

Просмотреть файл

@ -9,6 +9,7 @@ package com.facebook.react.uimanager;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ColorPropConverter;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.yoga.YogaConstants;
@ -47,7 +48,8 @@ public abstract class BaseViewManagerDelegate<T extends View, U extends BaseView
mViewManager.setViewState(view, (ReadableMap) value);
break;
case ViewProps.BACKGROUND_COLOR:
mViewManager.setBackgroundColor(view, value == null ? 0 : ((Double) value).intValue());
mViewManager.setBackgroundColor(
view, value == null ? 0 : ColorPropConverter.getColor(value, view.getContext()));
break;
case ViewProps.BORDER_RADIUS:
mViewManager.setBorderRadius(

Просмотреть файл

@ -7,9 +7,11 @@
package com.facebook.react.uimanager;
import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ColorPropConverter;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.DynamicFromObject;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
@ -81,13 +83,13 @@ import java.util.Map;
try {
if (mIndex == null) {
VIEW_MGR_ARGS[0] = viewToUpdate;
VIEW_MGR_ARGS[1] = getValueOrDefault(value);
VIEW_MGR_ARGS[1] = getValueOrDefault(value, viewToUpdate.getContext());
mSetter.invoke(viewManager, VIEW_MGR_ARGS);
Arrays.fill(VIEW_MGR_ARGS, null);
} else {
VIEW_MGR_GROUP_ARGS[0] = viewToUpdate;
VIEW_MGR_GROUP_ARGS[1] = mIndex;
VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value);
VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value, viewToUpdate.getContext());
mSetter.invoke(viewManager, VIEW_MGR_GROUP_ARGS);
Arrays.fill(VIEW_MGR_GROUP_ARGS, null);
}
@ -105,12 +107,12 @@ import java.util.Map;
public void updateShadowNodeProp(ReactShadowNode nodeToUpdate, Object value) {
try {
if (mIndex == null) {
SHADOW_ARGS[0] = getValueOrDefault(value);
SHADOW_ARGS[0] = getValueOrDefault(value, nodeToUpdate.getThemedContext());
mSetter.invoke(nodeToUpdate, SHADOW_ARGS);
Arrays.fill(SHADOW_ARGS, null);
} else {
SHADOW_GROUP_ARGS[0] = mIndex;
SHADOW_GROUP_ARGS[1] = getValueOrDefault(value);
SHADOW_GROUP_ARGS[1] = getValueOrDefault(value, nodeToUpdate.getThemedContext());
mSetter.invoke(nodeToUpdate, SHADOW_GROUP_ARGS);
Arrays.fill(SHADOW_GROUP_ARGS, null);
}
@ -125,7 +127,7 @@ import java.util.Map;
}
}
protected abstract @Nullable Object getValueOrDefault(Object value);
protected abstract @Nullable Object getValueOrDefault(Object value, Context context);
}
private static class DynamicPropSetter extends PropSetter {
@ -139,7 +141,7 @@ import java.util.Map;
}
@Override
protected Object getValueOrDefault(Object value) {
protected Object getValueOrDefault(Object value, Context context) {
if (value instanceof Dynamic) {
return value;
} else {
@ -163,7 +165,7 @@ import java.util.Map;
}
@Override
protected Object getValueOrDefault(Object value) {
protected Object getValueOrDefault(Object value, Context context) {
// All numbers from JS are Doubles which can't be simply cast to Integer
return value == null ? mDefaultValue : (Integer) ((Double) value).intValue();
}
@ -184,11 +186,34 @@ import java.util.Map;
}
@Override
protected Object getValueOrDefault(Object value) {
protected Object getValueOrDefault(Object value, Context context) {
return value == null ? mDefaultValue : (Double) value;
}
}
private static class ColorPropSetter extends PropSetter {
private final int mDefaultValue;
public ColorPropSetter(ReactProp prop, Method setter) {
this(prop, setter, 0);
}
public ColorPropSetter(ReactProp prop, Method setter, int defaultValue) {
super(prop, "mixed", setter);
mDefaultValue = defaultValue;
}
@Override
protected Object getValueOrDefault(Object value, Context context) {
if (value == null) {
return mDefaultValue;
}
return ColorPropConverter.getColor(value, context);
}
}
private static class BooleanPropSetter extends PropSetter {
private final boolean mDefaultValue;
@ -199,7 +224,7 @@ import java.util.Map;
}
@Override
protected Object getValueOrDefault(Object value) {
protected Object getValueOrDefault(Object value, Context context) {
boolean val = value == null ? mDefaultValue : (boolean) value;
return val ? Boolean.TRUE : Boolean.FALSE;
}
@ -220,7 +245,7 @@ import java.util.Map;
}
@Override
protected Object getValueOrDefault(Object value) {
protected Object getValueOrDefault(Object value, Context context) {
// All numbers from JS are Doubles which can't be simply cast to Float
return value == null ? mDefaultValue : (Float) ((Double) value).floatValue();
}
@ -233,7 +258,7 @@ import java.util.Map;
}
@Override
protected @Nullable Object getValueOrDefault(Object value) {
protected @Nullable Object getValueOrDefault(Object value, Context context) {
return (ReadableArray) value;
}
}
@ -245,7 +270,7 @@ import java.util.Map;
}
@Override
protected @Nullable Object getValueOrDefault(Object value) {
protected @Nullable Object getValueOrDefault(Object value, Context context) {
return (ReadableMap) value;
}
}
@ -257,7 +282,7 @@ import java.util.Map;
}
@Override
protected @Nullable Object getValueOrDefault(Object value) {
protected @Nullable Object getValueOrDefault(Object value, Context context) {
return (String) value;
}
}
@ -269,7 +294,7 @@ import java.util.Map;
}
@Override
protected @Nullable Object getValueOrDefault(Object value) {
protected @Nullable Object getValueOrDefault(Object value, Context context) {
if (value != null) {
return (boolean) value ? Boolean.TRUE : Boolean.FALSE;
}
@ -288,7 +313,7 @@ import java.util.Map;
}
@Override
protected @Nullable Object getValueOrDefault(Object value) {
protected @Nullable Object getValueOrDefault(Object value, Context context) {
if (value != null) {
if (value instanceof Double) {
return ((Double) value).intValue();
@ -379,6 +404,9 @@ import java.util.Map;
} else if (propTypeClass == boolean.class) {
return new BooleanPropSetter(annotation, method, annotation.defaultBoolean());
} else if (propTypeClass == int.class) {
if ("Color".equals(annotation.customType())) {
return new ColorPropSetter(annotation, method, annotation.defaultInt());
}
return new IntPropSetter(annotation, method, annotation.defaultInt());
} else if (propTypeClass == float.class) {
return new FloatPropSetter(annotation, method, annotation.defaultFloat());
@ -389,6 +417,9 @@ import java.util.Map;
} else if (propTypeClass == Boolean.class) {
return new BoxedBooleanPropSetter(annotation, method);
} else if (propTypeClass == Integer.class) {
if ("Color".equals(annotation.customType())) {
return new ColorPropSetter(annotation, method);
}
return new BoxedIntPropSetter(annotation, method);
} else if (propTypeClass == ReadableArray.class) {
return new ArrayPropSetter(annotation, method);

Просмотреть файл

@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener;
import com.facebook.react.bridge.ColorPropConverter;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableType;
@ -68,7 +69,11 @@ public class SwipeRefreshLayoutManager extends ViewGroupManager<ReactSwipeRefres
if (colors != null) {
int[] colorValues = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
colorValues[i] = colors.getInt(i);
if (colors.getType(i) == ReadableType.Map) {
colorValues[i] = ColorPropConverter.getColor(colors.getMap(i), view.getContext());
} else {
colorValues[i] = colors.getInt(i);
}
}
view.setColorSchemeColors(colorValues);
} else {

Просмотреть файл

@ -469,7 +469,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
markUpdated();
}
@ReactProp(name = ViewProps.COLOR)
@ReactProp(name = ViewProps.COLOR, customType = "Color")
public void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
if (mIsColorSet) {

Просмотреть файл

@ -12,6 +12,7 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;

Просмотреть файл

@ -12,9 +12,11 @@ import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import android.annotation.TargetApi;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;

Просмотреть файл

@ -94,6 +94,9 @@ import typeof RCTNativeAppEventEmitter from './Libraries/EventEmitter/RCTNativeA
import typeof NativeModules from './Libraries/BatchedBridge/NativeModules';
import typeof Platform from './Libraries/Utilities/Platform';
import typeof processColor from './Libraries/StyleSheet/processColor';
import typeof {PlatformColor} from './Libraries/StyleSheet/PlatformColorValueTypes';
import typeof {DynamicColorIOS} from './Libraries/StyleSheet/PlatformColorValueTypesIOS';
import typeof {ColorAndroid} from './Libraries/StyleSheet/PlatformColorValueTypesAndroid';
import typeof RootTagContext from './Libraries/ReactNative/RootTagContext';
import typeof DeprecatedColorPropType from './Libraries/DeprecatedPropTypes/DeprecatedColorPropType';
import typeof DeprecatedEdgeInsetsPropType from './Libraries/DeprecatedPropTypes/DeprecatedEdgeInsetsPropType';
@ -462,6 +465,18 @@ module.exports = {
get processColor(): processColor {
return require('./Libraries/StyleSheet/processColor');
},
get PlatformColor(): PlatformColor {
return require('./Libraries/StyleSheet/PlatformColorValueTypes')
.PlatformColor;
},
get DynamicColorIOS(): DynamicColorIOS {
return require('./Libraries/StyleSheet/PlatformColorValueTypesIOS')
.DynamicColorIOS;
},
get ColorAndroid(): ColorAndroid {
return require('./Libraries/StyleSheet/PlatformColorValueTypesAndroid')
.ColorAndroid;
},
get requireNativeComponent(): <T>(
uiViewClassName: string,
) => HostComponent<T> {

Просмотреть файл

@ -275,6 +275,12 @@ type ModuleProps = $ReadOnly<{|
color_array_optional_value: ?ColorArrayValue,
color_array_optional_both?: ?ColorArrayValue,
// ProcessedColorValue props
processed_color_required: ProcessedColorValue,
processed_color_optional_key?: ProcessedColorValue,
processed_color_optional_value: ?ProcessedColorValue,
processed_color_optional_both?: ?ProcessedColorValue,
// PointValue props
point_required: PointValue,
point_optional_key?: PointValue,
@ -310,7 +316,7 @@ const codegenNativeComponent = require('codegenNativeComponent');
import type {Int32, Double, Float, WithDefault} from 'CodegenTypes';
import type {ImageSource} from 'ImageSource';
import type {ColorValue, PointValue, EdgeInsetsValue} from 'StyleSheetTypes';
import type {ColorValue, PointValue, ProcessColorValue, EdgeInsetsValue} from 'StyleSheetTypes';
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
@ -508,6 +514,12 @@ type ModuleProps = $ReadOnly<{|
color_optional_value: $ReadOnly<{|prop: ?ColorValue|}>,
color_optional_both: $ReadOnly<{|prop?: ?ColorValue|}>,
// ProcessedColorValue props
processed_color_required: $ReadOnly<{|prop: ProcessedColorValue|}>,
processed_color_optional_key: $ReadOnly<{|prop?: ProcessedColorValue|}>,
processed_color_optional_value: $ReadOnly<{|prop: ?ProcessedColorValue|}>,
processed_color_optional_both: $ReadOnly<{|prop?: ?ProcessedColorValue|}>,
// PointValue props
point_required: $ReadOnly<{|prop: PointValue|}>,
point_optional_key: $ReadOnly<{|prop?: PointValue|}>,

Просмотреть файл

@ -456,6 +456,38 @@ Object {
"type": "ArrayTypeAnnotation",
},
},
Object {
"name": "processed_color_required",
"optional": false,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
Object {
"name": "processed_color_optional_key",
"optional": true,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
Object {
"name": "processed_color_optional_value",
"optional": true,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
Object {
"name": "processed_color_optional_both",
"optional": true,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
Object {
"name": "point_required",
"optional": false,
@ -5480,6 +5512,74 @@ Object {
"type": "ObjectTypeAnnotation",
},
},
Object {
"name": "processed_color_required",
"optional": false,
"typeAnnotation": Object {
"properties": Array [
Object {
"name": "prop",
"optional": false,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
],
"type": "ObjectTypeAnnotation",
},
},
Object {
"name": "processed_color_optional_key",
"optional": false,
"typeAnnotation": Object {
"properties": Array [
Object {
"name": "prop",
"optional": true,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
],
"type": "ObjectTypeAnnotation",
},
},
Object {
"name": "processed_color_optional_value",
"optional": false,
"typeAnnotation": Object {
"properties": Array [
Object {
"name": "prop",
"optional": true,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
],
"type": "ObjectTypeAnnotation",
},
},
Object {
"name": "processed_color_optional_both",
"optional": false,
"typeAnnotation": Object {
"properties": Array [
Object {
"name": "prop",
"optional": true,
"typeAnnotation": Object {
"name": "ColorPrimitive",
"type": "NativePrimitiveTypeAnnotation",
},
},
],
"type": "ObjectTypeAnnotation",
},
},
Object {
"name": "point_required",
"optional": false,

Просмотреть файл

@ -94,6 +94,7 @@ function getTypeAnnotationForArray(name, typeAnnotation, defaultValue, types) {
name: 'ImageSourcePrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'NativePrimitiveTypeAnnotation',
name: 'ColorPrimitive',
@ -217,6 +218,7 @@ function getTypeAnnotation(
name: 'ImageSourcePrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'NativePrimitiveTypeAnnotation',
name: 'ColorPrimitive',