From 9a2d05d9b2aea459ef3b3e92376cf15bca4d17fa Mon Sep 17 00:00:00 2001 From: Alexsander Akers Date: Thu, 17 Sep 2015 08:36:08 -0700 Subject: [PATCH] Move color processing to JS Reviewed By: @vjeux Differential Revision: D2346353 --- Examples/UIExplorer/BorderExample.js | 4 +- Examples/UIExplorer/PanResponderExample.js | 10 +- .../UIExplorer.xcodeproj/project.pbxproj | 4 - .../RCTConvert_UIColorTests.m | 79 ----- Libraries/Components/TextInput/TextInput.js | 32 +- .../ToolbarAndroid/ToolbarAndroid.android.js | 7 +- .../TouchableNativeFeedback.android.js | 3 +- Libraries/ReactIOS/NativeMethodsMixin.js | 27 +- Libraries/ReactIOS/requireNativeComponent.js | 27 +- .../ReactNative/ReactNativeBaseComponent.js | 12 +- .../ReactNative/ReactNativeStyleAttributes.js | 13 + .../StyleSheet/__tests__/processColor-test.js | 118 +++++++ Libraries/StyleSheet/precomputeStyle.js | 53 +++- Libraries/StyleSheet/processColor.js | 26 ++ Libraries/react-native/react-native.js | 1 + React/Base/RCTConvert.m | 299 +----------------- package.json | 1 + 17 files changed, 292 insertions(+), 424 deletions(-) delete mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m create mode 100644 Libraries/StyleSheet/__tests__/processColor-test.js create mode 100644 Libraries/StyleSheet/processColor.js diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index 677b17d8c..40c8f5992 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -26,12 +26,12 @@ var styles = StyleSheet.create({ }, border1: { borderWidth: 10, - borderColor: '#a52a2a', + borderColor: 'brown', }, borderRadius: { borderWidth: 10, borderRadius: 10, - borderColor: '#00ffff', + borderColor: 'cyan', }, border2: { borderWidth: 10, diff --git a/Examples/UIExplorer/PanResponderExample.js b/Examples/UIExplorer/PanResponderExample.js index 49ff6611a..b41b56f3e 100644 --- a/Examples/UIExplorer/PanResponderExample.js +++ b/Examples/UIExplorer/PanResponderExample.js @@ -11,22 +11,22 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * - * @flow + * @flow-weak */ 'use strict'; var React = require('react-native'); var { - StyleSheet, PanResponder, + StyleSheet, View, + processColor, } = React; var CIRCLE_SIZE = 80; var CIRCLE_COLOR = 'blue'; var CIRCLE_HIGHLIGHT_COLOR = 'green'; - var PanResponderExample = React.createClass({ statics: { @@ -78,13 +78,13 @@ var PanResponderExample = React.createClass({ _highlight: function() { this.circle && this.circle.setNativeProps({ - backgroundColor: CIRCLE_HIGHLIGHT_COLOR + backgroundColor: processColor(CIRCLE_HIGHLIGHT_COLOR) }); }, _unHighlight: function() { this.circle && this.circle.setNativeProps({ - backgroundColor: CIRCLE_COLOR + backgroundColor: processColor(CIRCLE_COLOR) }); }, diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index a259da744..6aa45700d 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -57,7 +57,6 @@ 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; }; 8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; }; 8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */; }; - 83A936C81B7E0F08005B9C36 /* RCTConvert_UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A936C71B7E0F08005B9C36 /* RCTConvert_UIColorTests.m */; }; D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ @@ -231,7 +230,6 @@ 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = ""; }; 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = ""; }; 8385CF051B8747A000C6273E /* RCTImageLoaderHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageLoaderHelpers.h; sourceTree = ""; }; - 83A936C71B7E0F08005B9C36 /* RCTConvert_UIColorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_UIColorTests.m; sourceTree = ""; }; D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -385,7 +383,6 @@ 138D6A151B53CD440074A87E /* RCTCacheTests.m */, 1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */, 1497CFA71B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m */, - 83A936C71B7E0F08005B9C36 /* RCTConvert_UIColorTests.m */, 1497CFA81B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m */, 1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */, 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, @@ -846,7 +843,6 @@ 138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */, 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */, 1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */, - 83A936C81B7E0F08005B9C36 /* RCTConvert_UIColorTests.m in Sources */, 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */, 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */, 8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */, diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m deleted file mode 100644 index f74429974..000000000 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m +++ /dev/null @@ -1,79 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#import - -#import "RCTConvert.h" - -@interface RCTConvert_UIColorTests : XCTestCase - -@end - -@implementation RCTConvert_UIColorTests - -#define XCTAssertEqualColors(color1, color2) do { \ - CGFloat r1, g1, b1, a1; \ - CGFloat r2, g2, b2, a2; \ - XCTAssertTrue([(color1) getRed:&r1 green:&g1 blue:&b1 alpha:&a1] && \ - [(color2) getRed:&r2 green:&g2 blue:&b2 alpha:&a2] && \ - r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2, \ - @"rgba(%d, %d, %d, %.3f) != rgba(%d, %d, %d, %.3f)", \ - (int)(r1 * 255), (int)(g1 * 255), (int)(b1 * 255), a1, \ - (int)(r2 * 255), (int)(g2 * 255), (int)(b2 * 255), a2 \ - ); \ -} while (0) - -- (void)testHex3 -{ - UIColor *color = [RCTConvert UIColor:@"#333"]; - UIColor *expected = [UIColor colorWithWhite:0.2 alpha:1.0]; - XCTAssertEqualColors(color, expected); -} - -- (void)testHex6 -{ - UIColor *color = [RCTConvert UIColor:@"#666"]; - UIColor *expected = [UIColor colorWithWhite:0.4 alpha:1.0]; - XCTAssertEqualColors(color, expected); -} - -- (void)testRGB -{ - UIColor *color = [RCTConvert UIColor:@"rgb(51, 102, 153)"]; - UIColor *expected = [UIColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:1.0]; - XCTAssertEqualColors(color, expected); -} - -- (void)testRGBA -{ - UIColor *color = [RCTConvert UIColor:@"rgba(51, 102, 153, 0.5)"]; - UIColor *expected = [UIColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.5]; - XCTAssertEqualColors(color, expected); -} - -- (void)testHSL -{ - UIColor *color = [RCTConvert UIColor:@"hsl(30, 50%, 50%)"]; - UIColor *expected = [UIColor colorWithHue:30.0 / 360.0 saturation:0.5 brightness:0.5 alpha:1.0]; - XCTAssertEqualColors(color, expected); -} - -- (void)testHSLA -{ - UIColor *color = [RCTConvert UIColor:@"hsla(30, 50%, 50%, 0.5)"]; - UIColor *expected = [UIColor colorWithHue:30.0 / 360.0 saturation:0.5 brightness:0.5 alpha:0.5]; - XCTAssertEqualColors(color, expected); -} - -@end diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index d7901aed0..d90349412 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -40,30 +40,7 @@ var notMultiline = { onSubmitEditing: true, }; -var AndroidTextInputAttributes = { - autoCapitalize: true, - autoCorrect: true, - autoFocus: true, - textAlign: true, - textAlignVertical: true, - keyboardType: true, - mostRecentEventCount: true, - multiline: true, - numberOfLines: true, - password: true, - placeholder: true, - placeholderTextColor: true, - text: true, - testID: true, - underlineColorAndroid: true, - editable : true, -}; - -var viewConfigAndroid = { - uiViewClassName: 'AndroidTextInput', - validAttributes: AndroidTextInputAttributes, -}; - +var AndroidTextInput = requireNativeComponent('AndroidTextInput', null); var RCTTextView = requireNativeComponent('RCTTextView', null); var RCTTextField = requireNativeComponent('RCTTextField', null); @@ -317,7 +294,7 @@ var TextInput = React.createClass({ mixins: [NativeMethodsMixin, TimerMixin], viewConfig: ((Platform.OS === 'ios' ? RCTTextField.viewConfig : - (Platform.OS === 'android' ? viewConfigAndroid : {})) : Object), + (Platform.OS === 'android' ? AndroidTextInput.viewConfig : {})) : Object), isFocused: function(): boolean { return TextInputState.currentlyFocusedField() === @@ -578,9 +555,4 @@ var styles = StyleSheet.create({ }, }); -var AndroidTextInput = createReactNativeComponentClass({ - validAttributes: AndroidTextInputAttributes, - uiViewClassName: 'AndroidTextInput', -}); - module.exports = TextInput; diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js index 961b59ac4..371f49e42 100644 --- a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js @@ -17,7 +17,7 @@ var React = require('React'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var ReactPropTypes = require('ReactPropTypes'); -var createReactNativeComponentClass = require('createReactNativeComponentClass'); +var requireNativeComponent = require('requireNativeComponent'); /** * React component that wraps the Android-only [`Toolbar` widget][0]. A Toolbar can display a logo, @@ -166,9 +166,6 @@ var toolbarAttributes = { titleColor: true, }; -var NativeToolbar = createReactNativeComponentClass({ - validAttributes: toolbarAttributes, - uiViewClassName: 'ToolbarAndroid', -}); +var NativeToolbar = requireNativeComponent('ToolbarAndroid', null); module.exports = ToolbarAndroid; diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index 98b59c3c3..b993f1bab 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -21,6 +21,7 @@ var createReactNativeComponentClass = require('createReactNativeComponentClass') var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var onlyChild = require('onlyChild'); +var processColor = require('processColor'); var rippleBackgroundPropType = createStrictShapeTypeChecker({ type: React.PropTypes.oneOf(['RippleAndroid']), @@ -112,7 +113,7 @@ var TouchableNativeFeedback = React.createClass({ return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackgroundBorderless'}; }, Ripple: function(color, borderless) { - return {type: 'RippleAndroid', color: color, borderless: borderless}; + return {type: 'RippleAndroid', color: processColor(color), borderless: borderless}; }, }, diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index 7a448ce40..afc79f777 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -77,15 +77,34 @@ var NativeMethodsMixin = { break; } } - var style = precomputeStyle(flattenStyle(nativeProps.style)); + + var validAttributes = this.viewConfig.validAttributes; + var hasProcessedProps = false; + var processedProps = {}; + for (var key in nativeProps) { + var process = validAttributes[key] && validAttributes[key].process; + if (process) { + hasProcessedProps = true; + processedProps[key] = process(nativeProps[key]); + } + } + + var style = precomputeStyle( + flattenStyle(processedProps.style || nativeProps.style), + this.viewConfig.validAttributes + ); var props = null; if (hasOnlyStyle) { props = style; - } else if (!style) { - props = nativeProps; } else { - props = mergeFast(nativeProps, style); + props = nativeProps; + if (hasProcessedProps) { + props = mergeFast(props, processedProps); + } + if (style) { + props = mergeFast(props, style); + } } RCTUIManager.updateView( diff --git a/Libraries/ReactIOS/requireNativeComponent.js b/Libraries/ReactIOS/requireNativeComponent.js index b291c7abe..0bbb93a4d 100644 --- a/Libraries/ReactIOS/requireNativeComponent.js +++ b/Libraries/ReactIOS/requireNativeComponent.js @@ -18,6 +18,7 @@ var createReactNativeComponentClass = require('createReactNativeComponentClass') var insetsDiffer = require('insetsDiffer'); var pointsDiffer = require('pointsDiffer'); var matricesDiffer = require('matricesDiffer'); +var processColor = require('processColor'); var sizesDiffer = require('sizesDiffer'); var verifyPropTypes = require('verifyPropTypes'); var warning = require('warning'); @@ -57,8 +58,22 @@ function requireNativeComponent( viewConfig.validAttributes = {}; viewConfig.propTypes = componentInterface && componentInterface.propTypes; for (var key in nativeProps) { + var useAttribute = false; + var attribute = {}; + var differ = TypeToDifferMap[nativeProps[key]]; - viewConfig.validAttributes[key] = differ ? {diff: differ} : true; + if (differ) { + attribute.diff = differ; + useAttribute = true; + } + + var processor = TypeToProcessorMap[nativeProps[key]]; + if (processor) { + attribute.process = processor; + useAttribute = true; + } + + viewConfig.validAttributes[key] = useAttribute ? attribute : true; } if (__DEV__) { componentInterface && verifyPropTypes( @@ -80,4 +95,14 @@ var TypeToDifferMap = { // (not yet implemented) }; +var TypeToProcessorMap = { + // iOS Types + CGColor: processColor, + CGColorArray: processColor, + UIColor: processColor, + UIColorArray: processColor, + // Android Types + Color: processColor, +}; + module.exports = requireNativeComponent; diff --git a/Libraries/ReactNative/ReactNativeBaseComponent.js b/Libraries/ReactNative/ReactNativeBaseComponent.js index 92896127b..02e327a99 100644 --- a/Libraries/ReactNative/ReactNativeBaseComponent.js +++ b/Libraries/ReactNative/ReactNativeBaseComponent.js @@ -158,13 +158,23 @@ ReactNativeBaseComponent.Mixin = { validAttributes ); + for (var key in updatePayload) { + var process = validAttributes[key] && validAttributes[key].process; + if (process) { + updatePayload[key] = process(updatePayload[key]); + } + } + // The style property is a deeply nested element which includes numbers // to represent static objects. Most of the time, it doesn't change across // renders, so it's faster to spend the time checking if it is different // before actually doing the expensive flattening operation in order to // compute the diff. if (styleDiffer(nextProps.style, prevProps.style)) { - var nextFlattenedStyle = precomputeStyle(flattenStyle(nextProps.style)); + var nextFlattenedStyle = precomputeStyle( + flattenStyle(nextProps.style), + this.viewConfig.validAttributes + ); updatePayload = diffRawProperties( updatePayload, this.previousFlattenedStyle, diff --git a/Libraries/ReactNative/ReactNativeStyleAttributes.js b/Libraries/ReactNative/ReactNativeStyleAttributes.js index 43b5902a6..e2099c9d2 100644 --- a/Libraries/ReactNative/ReactNativeStyleAttributes.js +++ b/Libraries/ReactNative/ReactNativeStyleAttributes.js @@ -18,6 +18,7 @@ var ViewStylePropTypes = require('ViewStylePropTypes'); var keyMirror = require('keyMirror'); var matricesDiffer = require('matricesDiffer'); +var processColor = require('processColor'); var sizesDiffer = require('sizesDiffer'); var ReactNativeStyleAttributes = { @@ -32,4 +33,16 @@ ReactNativeStyleAttributes.shadowOffset = { diff: sizesDiffer }; // Do not rely on this attribute. ReactNativeStyleAttributes.decomposedMatrix = 'decomposedMatrix'; +var colorAttributes = { process: processColor }; +ReactNativeStyleAttributes.backgroundColor = colorAttributes; +ReactNativeStyleAttributes.borderBottomColor = colorAttributes; +ReactNativeStyleAttributes.borderColor = colorAttributes; +ReactNativeStyleAttributes.borderLeftColor = colorAttributes; +ReactNativeStyleAttributes.borderRightColor = colorAttributes; +ReactNativeStyleAttributes.borderTopColor = colorAttributes; +ReactNativeStyleAttributes.color = colorAttributes; +ReactNativeStyleAttributes.shadowColor = colorAttributes; +ReactNativeStyleAttributes.textDecorationColor = colorAttributes; +ReactNativeStyleAttributes.tintColor = colorAttributes; + module.exports = ReactNativeStyleAttributes; diff --git a/Libraries/StyleSheet/__tests__/processColor-test.js b/Libraries/StyleSheet/__tests__/processColor-test.js new file mode 100644 index 000000000..9e11931cd --- /dev/null +++ b/Libraries/StyleSheet/__tests__/processColor-test.js @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +var processColor = require('processColor'); + +describe('processColor', () => { + + describe('predefined color names', () => { + + it('should convert red', () => { + var colorFromString = processColor('red'); + var expectedInt = 0xFFFF0000; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert white', () => { + var colorFromString = processColor('white'); + var expectedInt = 0xFFFFFFFF; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert black', () => { + var colorFromString = processColor('black'); + var expectedInt = 0xFF000000; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert transparent', () => { + var colorFromString = processColor('transparent'); + var expectedInt = 0x00000000; + expect(colorFromString).toEqual(expectedInt); + }); + }); + + describe('RGB strings', () => { + + it('should convert rgb(x, y, z)', () => { + var colorFromString = processColor('rgb(10, 20, 30)'); + var expectedInt = 0xFF0A141E; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert rgb x, y, z', () => { + var colorFromString = processColor('rgb 10, 20, 30'); + var expectedInt = 0xFF0A141E; + expect(colorFromString).toEqual(expectedInt); + }); + + }); + + describe('RGBA strings', () => { + + it('should convert rgba(x, y, z, a)', () => { + var colorFromString = processColor('rgba(10, 20, 30, 0.4)'); + var expectedInt = 0x660A141E; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert rgba x, y, z, a', () => { + var colorFromString = processColor('rgba 10, 20, 30, 0.4'); + var expectedInt = 0x660A141E; + expect(colorFromString).toEqual(expectedInt); + }); + + }); + + describe('HSL strings', () => { + + it('should convert hsl(x, y%, z%)', () => { + var colorFromString = processColor('hsl(318, 69%, 55%)'); + var expectedInt = 0xFFDB3DAC; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert hsl x, y%, z%', () => { + var colorFromString = processColor('hsl 318, 69%, 55%'); + var expectedInt = 0xFFDB3DAC; + expect(colorFromString).toEqual(expectedInt); + }); + + }); + + describe('HSL strings', () => { + + it('should convert hsl(x, y%, z%)', () => { + var colorFromString = processColor('hsla(318, 69%, 55%, 0.25)'); + var expectedInt = 0x40DB3DAC; + expect(colorFromString).toEqual(expectedInt); + }); + + it('should convert hsl x, y%, z%', () => { + var colorFromString = processColor('hsla 318, 69%, 55%, 0.25'); + var expectedInt = 0x40DB3DAC; + expect(colorFromString).toEqual(expectedInt); + }); + + }); + + describe('hex strings', () => { + + it('should convert #xxxxxx', () => { + var colorFromString = processColor('#1e83c9'); + var expectedInt = 0xFF1E83C9; + expect(colorFromString).toEqual(expectedInt); + }); + + }); + +}); diff --git a/Libraries/StyleSheet/precomputeStyle.js b/Libraries/StyleSheet/precomputeStyle.js index 3f1955b4a..5a9ed809f 100644 --- a/Libraries/StyleSheet/precomputeStyle.js +++ b/Libraries/StyleSheet/precomputeStyle.js @@ -12,6 +12,7 @@ 'use strict'; var MatrixMath = require('MatrixMath'); +var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); var Platform = require('Platform'); var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); @@ -22,19 +23,57 @@ var stringifySafe = require('stringifySafe'); * This method provides a hook where flattened styles may be precomputed or * otherwise prepared to become better input data for native code. */ -function precomputeStyle(style: ?Object): ?Object { - if (!style || !style.transform) { +function precomputeStyle(style: ?Object, validAttributes: Object): ?Object { + if (!style) { return style; } - invariant( - !style.transformMatrix, - 'transformMatrix and transform styles cannot be used on the same component' - ); - var newStyle = _precomputeTransforms({...style}); + + var hasPreprocessKeys = false; + for (var i = 0, keys = Object.keys(style); i < keys.length; i++) { + var key = keys[i]; + if (_processor(key, validAttributes)) { + hasPreprocessKeys = true; + break; + } + } + + if (!hasPreprocessKeys && !style.transform) { + return style; + } + + var newStyle = {...style}; + for (var i = 0, keys = Object.keys(style); i < keys.length; i++) { + var key = keys[i]; + var process = _processor(key, validAttributes); + if (process) { + newStyle[key] = process(newStyle[key]); + } + } + + if (style.transform) { + invariant( + !style.transformMatrix, + 'transformMatrix and transform styles cannot be used on the same component' + ); + + newStyle = _precomputeTransforms(newStyle); + } + deepFreezeAndThrowOnMutationInDev(newStyle); return newStyle; } +function _processor(key: string, validAttributes: Object) { + var process = validAttributes[key] && validAttributes[key].process; + if (!process) { + process = + ReactNativeStyleAttributes[key] && + ReactNativeStyleAttributes[key].process; + } + + return process; +} + /** * Generate a transform matrix based on the provided transforms, and use that * within the style object instead. diff --git a/Libraries/StyleSheet/processColor.js b/Libraries/StyleSheet/processColor.js new file mode 100644 index 000000000..059450c15 --- /dev/null +++ b/Libraries/StyleSheet/processColor.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule processColor + */ +'use strict'; + +var tinycolor = require('tinycolor2'); + +function processColor(color) { + if (!color || typeof color === 'number') { + return color; + } else if (color instanceof Array) { + return color.map(processColor); + } else { + var hexString = tinycolor(color).toHex8(); + return parseInt(hexString, 16); + } +} + +module.exports = processColor; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 7b2286782..5aaf47ef9 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -78,6 +78,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { NativeAppEventEmitter: require('RCTNativeAppEventEmitter'), NativeModules: require('NativeModules'), Platform: require('Platform'), + processColor: require('processColor'), requireNativeComponent: require('requireNativeComponent'), // Prop Types diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 8141b1386..3a79abced 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -379,292 +379,21 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ + (UIColor *)UIColor:(id)json { - // Check color cache - static RCTCache *colorCache = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - colorCache = [RCTCache new]; - colorCache.countLimit = 128; - }); - UIColor *color = colorCache[json]; - if (color) { - return color; + if ([json isKindOfClass:[NSArray class]]) { + NSArray *components = [self NSNumberArray:json]; + CGFloat alpha = components.count > 3 ? [self CGFloat:components[3]] : 1.0; + return [UIColor colorWithRed:[self CGFloat:components[0]] + green:[self CGFloat:components[1]] + blue:[self CGFloat:components[2]] + alpha:alpha]; + } else { + NSUInteger argb = [self NSUInteger:json]; + CGFloat a = ((argb >> 24) & 0xFF) / 255.0; + CGFloat r = ((argb >> 16) & 0xFF) / 255.0; + CGFloat g = ((argb >> 8) & 0xFF) / 255.0; + CGFloat b = (argb & 0xFF) / 255.0; + return [UIColor colorWithRed:r green:g blue:b alpha:a]; } - - if ([json isKindOfClass:[NSString class]]) { - - // Check named colors - static NSDictionary *namedColors = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - namedColors = @{ - - // CSS colors - @"aliceblue": @"#f0f8ff", - @"antiquewhite": @"#faebd7", - @"aqua": @"#00ffff", - @"aquamarine": @"#7fffd4", - @"azure": @"#f0ffff", - @"beige": @"#f5f5dc", - @"bisque": @"#ffe4c4", - @"black": @"#000000", - @"blanchedalmond": @"#ffebcd", - @"blue": @"#0000ff", - @"blueviolet": @"#8a2be2", - @"brown": @"#a52a2a", - @"burlywood": @"#deb887", - @"cadetblue": @"#5f9ea0", - @"chartreuse": @"#7fff00", - @"chocolate": @"#d2691e", - @"coral": @"#ff7f50", - @"cornflowerblue": @"#6495ed", - @"cornsilk": @"#fff8dc", - @"crimson": @"#dc143c", - @"cyan": @"#00ffff", - @"darkblue": @"#00008b", - @"darkcyan": @"#008b8b", - @"darkgoldenrod": @"#b8860b", - @"darkgray": @"#a9a9a9", - @"darkgrey": @"#a9a9a9", - @"darkgreen": @"#006400", - @"darkkhaki": @"#bdb76b", - @"darkmagenta": @"#8b008b", - @"darkolivegreen": @"#556b2f", - @"darkorange": @"#ff8c00", - @"darkorchid": @"#9932cc", - @"darkred": @"#8b0000", - @"darksalmon": @"#e9967a", - @"darkseagreen": @"#8fbc8f", - @"darkslateblue": @"#483d8b", - @"darkslategray": @"#2f4f4f", - @"darkslategrey": @"#2f4f4f", - @"darkturquoise": @"#00ced1", - @"darkviolet": @"#9400d3", - @"deeppink": @"#ff1493", - @"deepskyblue": @"#00bfff", - @"dimgray": @"#696969", - @"dimgrey": @"#696969", - @"dodgerblue": @"#1e90ff", - @"firebrick": @"#b22222", - @"floralwhite": @"#fffaf0", - @"forestgreen": @"#228b22", - @"fuchsia": @"#ff00ff", - @"gainsboro": @"#dcdcdc", - @"ghostwhite": @"#f8f8ff", - @"gold": @"#ffd700", - @"goldenrod": @"#daa520", - @"gray": @"#808080", - @"grey": @"#808080", - @"green": @"#008000", - @"greenyellow": @"#adff2f", - @"honeydew": @"#f0fff0", - @"hotpink": @"#ff69b4", - @"indianred": @"#cd5c5c", - @"indigo": @"#4b0082", - @"ivory": @"#fffff0", - @"khaki": @"#f0e68c", - @"lavender": @"#e6e6fa", - @"lavenderblush": @"#fff0f5", - @"lawngreen": @"#7cfc00", - @"lemonchiffon": @"#fffacd", - @"lightblue": @"#add8e6", - @"lightcoral": @"#f08080", - @"lightcyan": @"#e0ffff", - @"lightgoldenrodyellow": @"#fafad2", - @"lightgray": @"#d3d3d3", - @"lightgrey": @"#d3d3d3", - @"lightgreen": @"#90ee90", - @"lightpink": @"#ffb6c1", - @"lightsalmon": @"#ffa07a", - @"lightseagreen": @"#20b2aa", - @"lightskyblue": @"#87cefa", - @"lightslategray": @"#778899", - @"lightslategrey": @"#778899", - @"lightsteelblue": @"#b0c4de", - @"lightyellow": @"#ffffe0", - @"lime": @"#00ff00", - @"limegreen": @"#32cd32", - @"linen": @"#faf0e6", - @"magenta": @"#ff00ff", - @"maroon": @"#800000", - @"mediumaquamarine": @"#66cdaa", - @"mediumblue": @"#0000cd", - @"mediumorchid": @"#ba55d3", - @"mediumpurple": @"#9370db", - @"mediumseagreen": @"#3cb371", - @"mediumslateblue": @"#7b68ee", - @"mediumspringgreen": @"#00fa9a", - @"mediumturquoise": @"#48d1cc", - @"mediumvioletred": @"#c71585", - @"midnightblue": @"#191970", - @"mintcream": @"#f5fffa", - @"mistyrose": @"#ffe4e1", - @"moccasin": @"#ffe4b5", - @"navajowhite": @"#ffdead", - @"navy": @"#000080", - @"oldlace": @"#fdf5e6", - @"olive": @"#808000", - @"olivedrab": @"#6b8e23", - @"orange": @"#ffa500", - @"orangered": @"#ff4500", - @"orchid": @"#da70d6", - @"palegoldenrod": @"#eee8aa", - @"palegreen": @"#98fb98", - @"paleturquoise": @"#afeeee", - @"palevioletred": @"#db7093", - @"papayawhip": @"#ffefd5", - @"peachpuff": @"#ffdab9", - @"peru": @"#cd853f", - @"pink": @"#ffc0cb", - @"plum": @"#dda0dd", - @"powderblue": @"#b0e0e6", - @"purple": @"#800080", - @"rebeccapurple": @"#663399", - @"red": @"#ff0000", - @"rosybrown": @"#bc8f8f", - @"royalblue": @"#4169e1", - @"saddlebrown": @"#8b4513", - @"salmon": @"#fa8072", - @"sandybrown": @"#f4a460", - @"seagreen": @"#2e8b57", - @"seashell": @"#fff5ee", - @"sienna": @"#a0522d", - @"silver": @"#c0c0c0", - @"skyblue": @"#87ceeb", - @"slateblue": @"#6a5acd", - @"slategray": @"#708090", - @"slategrey": @"#708090", - @"snow": @"#fffafa", - @"springgreen": @"#00ff7f", - @"steelblue": @"#4682b4", - @"tan": @"#d2b48c", - @"teal": @"#008080", - @"thistle": @"#d8bfd8", - @"tomato": @"#ff6347", - @"turquoise": @"#40e0d0", - @"violet": @"#ee82ee", - @"wheat": @"#f5deb3", - @"white": @"#ffffff", - @"whitesmoke": @"#f5f5f5", - @"yellow": @"#ffff00", - @"yellowgreen": @"#9acd32", - - // Nonstandard color extensions - @"transparent": @"rgba(0,0,0,0)", - }; - }); - NSString *colorString = namedColors[json]; - if (!colorString) { - colorString = json; - } - - // Parse color - enum { - MODE_RGB = 0, - MODE_HSB = 1, - }; - struct { - union { - struct { - double r, g, b; - } rgb; - struct { - double h, s, b; - } hsb; - }; - double a; - unsigned int mode: 1; - } components = { - .a = 1.0, - .mode = MODE_RGB, - }; - - if ([colorString hasPrefix:@"#"]) { - uint32_t redInt = 0, greenInt = 0, blueInt = 0; - if (colorString.length == 4) { // 3 digit hex - sscanf(colorString.UTF8String, "#%01x%01x%01x", &redInt, &greenInt, &blueInt); - // expand to 6 digit hex - components.rgb.r = redInt / 15.0; - components.rgb.g = greenInt / 15.0; - components.rgb.b = blueInt / 15.0; - } else if (colorString.length == 7) { // 6 digit hex - sscanf(colorString.UTF8String, "#%02x%02x%02x", &redInt, &greenInt, &blueInt); - components.rgb.r = redInt / 255.0; - components.rgb.g = greenInt / 255.0; - components.rgb.b = blueInt / 255.0; - } else { - RCTLogError(@"Invalid hex color %@. Hex colors should be 3 or 6 digits long.", colorString); - components.a = -1; - } - } else if (4 == sscanf(colorString.UTF8String, "rgba(%lf,%lf,%lf,%lf)", &components.rgb.r, &components.rgb.g, &components.rgb.b, &components.a) || - 3 == sscanf(colorString.UTF8String, "rgb(%lf,%lf,%lf)", &components.rgb.r, &components.rgb.g, &components.rgb.b)) { - components.rgb.r /= 255.0; - components.rgb.g /= 255.0; - components.rgb.b /= 255.0; - } else if (4 == sscanf(colorString.UTF8String, "hsla(%lf,%lf%%,%lf%%,%lf)", &components.hsb.h, &components.hsb.s, &components.hsb.b, &components.a) || - 3 == sscanf(colorString.UTF8String, "hsl(%lf,%lf%%,%lf%%)", &components.hsb.h, &components.hsb.s, &components.hsb.b)) { - components.hsb.h /= 360.0; - components.hsb.s /= 100.0; - components.hsb.b /= 100.0; - components.mode = MODE_HSB; - } else { - RCTLogError(@"Unrecognized color format '%@', must be one of #hex|rgba|rgb or a valid CSS color name.", colorString); - components.a = -1; - } - if (components.a < 0) { - RCTLogError(@"Invalid color string '%@'", colorString); - } else { - if (components.mode == MODE_RGB) { - color = [UIColor colorWithRed:components.rgb.r green:components.rgb.g blue:components.rgb.b alpha:components.a]; - } else { - color = [UIColor colorWithHue:components.hsb.h saturation:components.hsb.s brightness:components.hsb.b alpha:components.a]; - } - } - - } else if ([json isKindOfClass:[NSArray class]]) { - - if ([json count] < 3 || [json count] > 4) { - RCTLogError(@"Expected array with count 3 or 4, but count is %zd: %@", [json count], json); - } else { - - // Color array - color = [UIColor colorWithRed:[self CGFloat:json[0]] - green:[self CGFloat:json[1]] - blue:[self CGFloat:json[2]] - alpha:[json count] > 3 ? [self CGFloat:json[3]] : 1]; - } - - } else if ([json isKindOfClass:[NSDictionary class]]) { - - // Color dictionary - if (json[@"r"]) { - color = [UIColor colorWithRed:[self CGFloat:json[@"r"]] - green:[self CGFloat:json[@"g"]] - blue:[self CGFloat:json[@"b"]] - alpha:[self CGFloat:json[@"a"] ?: @1]]; - } else if (json[@"h"]) { - color = [UIColor colorWithHue:[self CGFloat:json[@"h"]] - saturation:[self CGFloat:json[@"s"]] - brightness:[self CGFloat:json[@"b"]] - alpha:[self CGFloat:json[@"a"] ?: @1]]; - } else { - RCTLogError(@"Expected dictionary with keys {r,g,b} or {h,s,b}, got: %@", [json allKeys]); - } - - } else if (json) { - RCTLogConvertError(json, @"a color"); - } - - // Default color - if (!color) { - color = [UIColor whiteColor]; - } - - // Cache and return - if (json) { - colorCache[json] = color; - } - return color; } + (CGColorRef)CGColor:(id)json diff --git a/package.json b/package.json index 1f905bc13..2c5016b7a 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "semver": "^5.0.1", "source-map": "^0.4.4", "stacktrace-parser": "^0.1.3", + "tinycolor2": "^1.1.2", "uglify-js": "^2.4.24", "underscore": "^1.8.3", "wordwrap": "^1.0.0",