From d3a0c4129d6a5a7beced4e9aa62b2da4e3f4fed4 Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Thu, 27 Jan 2022 17:34:37 -0800 Subject: [PATCH] Support string color values in Animated.Color Summary: In addition to rgba values, allow creating Animated.Color with a string color. Followup changes will include support for platform colors and native driver. Changelog: [General][Added] - Support string color values in Animated.Color Reviewed By: javache Differential Revision: D33810717 fbshipit-source-id: 208bc2675b6153a515fbf2122da15a065c473e73 --- Libraries/Animated/__tests__/Animated-test.js | 31 +++++ Libraries/Animated/nodes/AnimatedColor.js | 128 +++++++++++++----- 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/Libraries/Animated/__tests__/Animated-test.js b/Libraries/Animated/__tests__/Animated-test.js index 717b1626c10..8f62804c73b 100644 --- a/Libraries/Animated/__tests__/Animated-test.js +++ b/Libraries/Animated/__tests__/Animated-test.js @@ -974,6 +974,37 @@ describe('Animated tests', () => { }); describe('Animated Colors', () => { + it('should normalize colors', () => { + let color = new Animated.Color(); + expect(color.__getValue()).toEqual('rgba(0, 0, 0, 1)'); + + color = new Animated.Color({r: 11, g: 22, b: 33, a: 1.0}); + expect(color.__getValue()).toEqual('rgba(11, 22, 33, 1)'); + + color = new Animated.Color('rgba(255, 0, 0, 1.0)'); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color('#ff0000ff'); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color('red'); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color({ + r: new Animated.Value(255), + g: new Animated.Value(0), + b: new Animated.Value(0), + a: new Animated.Value(1.0), + }); + expect(color.__getValue()).toEqual('rgba(255, 0, 0, 1)'); + + color = new Animated.Color('unknown'); + expect(color.__getValue()).toEqual('rgba(0, 0, 0, 1)'); + + color = new Animated.Color({key: 'value'}); + expect(color.__getValue()).toEqual('rgba(0, 0, 0, 1)'); + }); + it('should animate colors', () => { const color = new Animated.Color({r: 255, g: 0, b: 0, a: 1.0}); const callback = jest.fn(); diff --git a/Libraries/Animated/nodes/AnimatedColor.js b/Libraries/Animated/nodes/AnimatedColor.js index 7a320911da3..1178a45b24c 100644 --- a/Libraries/Animated/nodes/AnimatedColor.js +++ b/Libraries/Animated/nodes/AnimatedColor.js @@ -12,12 +12,79 @@ import AnimatedValue from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; -import invariant from 'invariant'; +import normalizeColor from '../../StyleSheet/normalizeColor'; +import {processColorObject} from '../../StyleSheet/PlatformColorValueTypes'; + +import type {ColorValue} from '../../StyleSheet/StyleSheet'; +import type {NativeColorValue} from '../../StyleSheet/PlatformColorValueTypes'; type ColorListenerCallback = (value: string) => mixed; +type RgbaValue = { + +r: number, + +g: number, + +b: number, + +a: number, + ... +}; +type RgbaAnimatedValue = { + +r: AnimatedValue, + +g: AnimatedValue, + +b: AnimatedValue, + +a: AnimatedValue, + ... +}; +const defaultColor: RgbaValue = {r: 0, g: 0, b: 0, a: 1.0}; let _uniqueId = 1; +/* eslint no-bitwise: 0 */ +function processColor(color?: ?ColorValue): ?(RgbaValue | NativeColorValue) { + if (color === undefined || color === null) { + return null; + } + + let normalizedColor = normalizeColor(color); + if (normalizedColor === undefined || normalizedColor === null) { + return null; + } + + if (typeof normalizedColor === 'object') { + const processedColorObj = processColorObject(normalizedColor); + if (processedColorObj != null) { + return processedColorObj; + } + } else if (typeof normalizedColor === 'number') { + const r = (normalizedColor & 0xff000000) >>> 24; + const g = (normalizedColor & 0x00ff0000) >>> 16; + const b = (normalizedColor & 0x0000ff00) >>> 8; + const a = (normalizedColor & 0x000000ff) / 255; + + return {r, g, b, a}; + } + + return null; +} + +function isRgbaValue(value: any): boolean { + return ( + value && + typeof value.r === 'number' && + typeof value.g === 'number' && + typeof value.b === 'number' && + typeof value.a === 'number' + ); +} + +function isRgbaAnimatedValue(value: any): boolean { + return ( + value && + value.r instanceof AnimatedValue && + value.g instanceof AnimatedValue && + value.b instanceof AnimatedValue && + value.a instanceof AnimatedValue + ); +} + export default class AnimatedColor extends AnimatedWithChildren { r: AnimatedValue; g: AnimatedValue; @@ -34,39 +101,32 @@ export default class AnimatedColor extends AnimatedWithChildren { ... }; - constructor( - valueIn?: ?{ - +r: number | AnimatedValue, - +g: number | AnimatedValue, - +b: number | AnimatedValue, - +a: number | AnimatedValue, - ... - }, // TODO: support string color and platform color - ) { + constructor(valueIn?: ?(RgbaValue | RgbaAnimatedValue | ColorValue)) { super(); - const value: any = valueIn || {r: 0, g: 0, b: 0, a: 1}; // @flowfixme: shouldn't need `: any` - if ( - typeof value.r === 'number' && - typeof value.g === 'number' && - typeof value.b === 'number' && - typeof value.a === 'number' - ) { - this.r = new AnimatedValue(value.r); - this.g = new AnimatedValue(value.g); - this.b = new AnimatedValue(value.b); - this.a = new AnimatedValue(value.a); + let value: RgbaValue | RgbaAnimatedValue | ColorValue = + valueIn || defaultColor; + + if (isRgbaAnimatedValue(value)) { + // $FlowIgnore[incompatible-cast] - Type is verified above + const rgbaAnimatedValue: RgbaAnimatedValue = (value: RgbaAnimatedValue); + this.r = rgbaAnimatedValue.r; + this.g = rgbaAnimatedValue.g; + this.b = rgbaAnimatedValue.b; + this.a = rgbaAnimatedValue.a; } else { - invariant( - value.r instanceof AnimatedValue && - value.g instanceof AnimatedValue && - value.b instanceof AnimatedValue && - value.a instanceof AnimatedValue, - 'AnimatedColor must be initialized with an object of numbers or AnimatedValues.', - ); - this.r = value.r; - this.g = value.g; - this.b = value.b; - this.a = value.a; + // Handle potential parsable string color or platform color object + if (!isRgbaValue(value)) { + // $FlowIgnore[incompatible-cast] - Type is verified via conditionals + value = processColor((value: ColorValue)) || {r: 0, g: 0, b: 0, a: 1.0}; + // TODO: support platform color + } + + // $FlowIgnore[incompatible-cast] - Type is verified via conditionals + const rgbaValue: RgbaValue = (value: RgbaValue); + this.r = new AnimatedValue(rgbaValue.r); + this.g = new AnimatedValue(rgbaValue.g); + this.b = new AnimatedValue(rgbaValue.b); + this.a = new AnimatedValue(rgbaValue.a); } this._listeners = {}; } @@ -75,7 +135,7 @@ export default class AnimatedColor extends AnimatedWithChildren { * Directly set the value. This will stop any animations running on the value * and update all the bound properties. */ - setValue(value: {r: number, g: number, b: number, a: number, ...}) { + setValue(value: {r: number, g: number, b: number, a: number, ...}): void { this.r.setValue(value.r); this.g.setValue(value.g); this.b.setValue(value.b); @@ -87,7 +147,7 @@ export default class AnimatedColor extends AnimatedWithChildren { * via `setValue`, an animation, or `Animated.event`. Useful for compensating * things like the start of a pan gesture. */ - setOffset(offset: {r: number, g: number, b: number, a: number, ...}) { + setOffset(offset: {r: number, g: number, b: number, a: number, ...}): void { this.r.setOffset(offset.r); this.g.setOffset(offset.g); this.b.setOffset(offset.b);