mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Initial non-native implementation of AnimatedColor
Summary:
Creates Animated.Color for animating color props.
Implement AnimatedColor, which basically consists of 4 AnimatedValues (along the same vein as ValueXY) which allows us to just use AnimatedValue's interpolation. Provides a string color value of shape 'rgba(r, g, b, a)'
AnimationNode DAG looks like:
{F696076974}
Followup changes will include support for string color values, platform colors, and native driver.
Changelog:
[General][Added] - New Animated.Color node
Reviewed By: mdvacca
Differential Revision: D33778456
fbshipit-source-id: 83ddbc955156bf589c864f229a5f83fe6875fd0e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
20b9ed91c0
commit
ea90a76efe
@@ -38,6 +38,8 @@ import type {DecayAnimationConfig} from './animations/DecayAnimation';
|
||||
import type {SpringAnimationConfig} from './animations/SpringAnimation';
|
||||
import type {Mapping, EventConfig} from './AnimatedEvent';
|
||||
|
||||
import AnimatedColor from './nodes/AnimatedColor';
|
||||
|
||||
export type CompositeAnimation = {
|
||||
start: (callback?: ?EndCallback) => void,
|
||||
stop: () => void,
|
||||
@@ -102,7 +104,7 @@ const _combineCallbacks = function (
|
||||
};
|
||||
|
||||
const maybeVectorAnim = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: Object,
|
||||
anim: (value: AnimatedValue, config: Object) => CompositeAnimation,
|
||||
): ?CompositeAnimation {
|
||||
@@ -121,16 +123,42 @@ const maybeVectorAnim = function (
|
||||
// We use `stopTogether: false` here because otherwise tracking will break
|
||||
// because the second animation will get stopped before it can update.
|
||||
return parallel([aX, aY], {stopTogether: false});
|
||||
} else if (value instanceof AnimatedColor) {
|
||||
const configR = {...config};
|
||||
const configG = {...config};
|
||||
const configB = {...config};
|
||||
const configA = {...config};
|
||||
for (const key in config) {
|
||||
const {r, g, b, a} = config[key];
|
||||
if (
|
||||
r !== undefined &&
|
||||
g !== undefined &&
|
||||
b !== undefined &&
|
||||
a !== undefined
|
||||
) {
|
||||
configR[key] = r;
|
||||
configG[key] = g;
|
||||
configB[key] = b;
|
||||
configA[key] = a;
|
||||
}
|
||||
}
|
||||
const aR = anim((value: AnimatedColor).r, configR);
|
||||
const aG = anim((value: AnimatedColor).g, configG);
|
||||
const aB = anim((value: AnimatedColor).b, configB);
|
||||
const aA = anim((value: AnimatedColor).a, configA);
|
||||
// We use `stopTogether: false` here because otherwise tracking will break
|
||||
// because the second animation will get stopped before it can update.
|
||||
return parallel([aR, aG, aB, aA], {stopTogether: false});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const spring = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: SpringAnimationConfig,
|
||||
): CompositeAnimation {
|
||||
const start = function (
|
||||
animatedValue: AnimatedValue | AnimatedValueXY,
|
||||
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
configuration: SpringAnimationConfig,
|
||||
callback?: ?EndCallback,
|
||||
): void {
|
||||
@@ -179,11 +207,11 @@ const spring = function (
|
||||
};
|
||||
|
||||
const timing = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: TimingAnimationConfig,
|
||||
): CompositeAnimation {
|
||||
const start = function (
|
||||
animatedValue: AnimatedValue | AnimatedValueXY,
|
||||
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
configuration: TimingAnimationConfig,
|
||||
callback?: ?EndCallback,
|
||||
): void {
|
||||
@@ -233,11 +261,11 @@ const timing = function (
|
||||
};
|
||||
|
||||
const decay = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: DecayAnimationConfig,
|
||||
): CompositeAnimation {
|
||||
const start = function (
|
||||
animatedValue: AnimatedValue | AnimatedValueXY,
|
||||
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
configuration: DecayAnimationConfig,
|
||||
callback?: ?EndCallback,
|
||||
): void {
|
||||
@@ -547,6 +575,10 @@ module.exports = {
|
||||
* See https://reactnative.dev/docs/animatedvaluexy
|
||||
*/
|
||||
ValueXY: AnimatedValueXY,
|
||||
/**
|
||||
* Value class for driving color animations.
|
||||
*/
|
||||
Color: AnimatedColor,
|
||||
/**
|
||||
* Exported to use the Interpolation type in flow.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,8 @@ import type {TimingAnimationConfig} from './animations/TimingAnimation';
|
||||
import type {DecayAnimationConfig} from './animations/DecayAnimation';
|
||||
import type {SpringAnimationConfig} from './animations/SpringAnimation';
|
||||
|
||||
import AnimatedColor from './nodes/AnimatedColor';
|
||||
|
||||
/**
|
||||
* Animations are a source of flakiness in snapshot testing. This mock replaces
|
||||
* animation functions from AnimatedImplementation with empty animations for
|
||||
@@ -89,7 +91,7 @@ const mockCompositeAnimation = (
|
||||
});
|
||||
|
||||
const spring = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: SpringAnimationConfig,
|
||||
): CompositeAnimation {
|
||||
const anyValue: any = value;
|
||||
@@ -103,7 +105,7 @@ const spring = function (
|
||||
};
|
||||
|
||||
const timing = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: TimingAnimationConfig,
|
||||
): CompositeAnimation {
|
||||
const anyValue: any = value;
|
||||
@@ -117,7 +119,7 @@ const timing = function (
|
||||
};
|
||||
|
||||
const decay = function (
|
||||
value: AnimatedValue | AnimatedValueXY,
|
||||
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
|
||||
config: DecayAnimationConfig,
|
||||
): CompositeAnimation {
|
||||
return emptyAnimation;
|
||||
@@ -164,6 +166,7 @@ const loop = function (
|
||||
module.exports = {
|
||||
Value: AnimatedValue,
|
||||
ValueXY: AnimatedValueXY,
|
||||
Color: AnimatedColor,
|
||||
Interpolation: AnimatedInterpolation,
|
||||
Node: AnimatedNode,
|
||||
decay,
|
||||
|
||||
@@ -972,4 +972,84 @@ describe('Animated tests', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Colors', () => {
|
||||
it('should animate colors', () => {
|
||||
const color = new Animated.Color({r: 255, g: 0, b: 0, a: 1.0});
|
||||
const callback = jest.fn();
|
||||
const node = new AnimatedProps(
|
||||
{
|
||||
style: {
|
||||
backgroundColor: color,
|
||||
transform: [
|
||||
{
|
||||
scale: color.a.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 2],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
callback,
|
||||
);
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
backgroundColor: 'rgba(255, 0, 0, 1)',
|
||||
transform: [{scale: 2}],
|
||||
},
|
||||
});
|
||||
|
||||
node.__attach();
|
||||
expect(callback.mock.calls.length).toBe(0);
|
||||
|
||||
color.setValue({r: 11, g: 22, b: 33, a: 0.5});
|
||||
expect(callback.mock.calls.length).toBe(4);
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
backgroundColor: 'rgba(11, 22, 33, 0.5)',
|
||||
transform: [{scale: 1.5}],
|
||||
},
|
||||
});
|
||||
|
||||
node.__detach();
|
||||
color.setValue({r: 255, g: 0, b: 0, a: 1.0});
|
||||
expect(callback.mock.calls.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should track colors', () => {
|
||||
const color1 = new Animated.Color();
|
||||
const color2 = new Animated.Color();
|
||||
Animated.timing(color2, {
|
||||
toValue: color1,
|
||||
duration: 0,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
color1.setValue({r: 11, g: 22, b: 33, a: 0.5});
|
||||
expect(color2.__getValue()).toEqual('rgba(11, 22, 33, 0.5)');
|
||||
|
||||
// Make sure tracking keeps working (see stopTogether in ParallelConfig used
|
||||
// by maybeVectorAnim).
|
||||
color1.setValue({r: 255, g: 0, b: 0, a: 1.0});
|
||||
expect(color2.__getValue()).toEqual('rgba(255, 0, 0, 1)');
|
||||
});
|
||||
|
||||
it('should track with springs', () => {
|
||||
const color1 = new Animated.Color();
|
||||
const color2 = new Animated.Color();
|
||||
Animated.spring(color2, {
|
||||
toValue: color1,
|
||||
tension: 3000, // faster spring for faster test
|
||||
friction: 60,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
color1.setValue({r: 11, g: 22, b: 33, a: 0.5});
|
||||
jest.runAllTimers();
|
||||
expect(color2.__getValue()).toEqual('rgba(11, 22, 33, 0.5)');
|
||||
color1.setValue({r: 44, g: 55, b: 66, a: 0.0});
|
||||
jest.runAllTimers();
|
||||
expect(color2.__getValue()).toEqual('rgba(44, 55, 66, 0)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ const {shouldUseNativeDriver} = require('../NativeAnimatedHelper');
|
||||
import type {PlatformConfig} from '../AnimatedPlatformConfig';
|
||||
import type {AnimationConfig, EndCallback} from './Animation';
|
||||
|
||||
import AnimatedColor from '../nodes/AnimatedColor';
|
||||
|
||||
export type SpringAnimationConfig = {
|
||||
...AnimationConfig,
|
||||
toValue:
|
||||
@@ -34,6 +36,14 @@ export type SpringAnimationConfig = {
|
||||
...
|
||||
}
|
||||
| AnimatedValueXY
|
||||
| {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number,
|
||||
...
|
||||
}
|
||||
| AnimatedColor
|
||||
| AnimatedInterpolation,
|
||||
overshootClamping?: boolean,
|
||||
restDisplacementThreshold?: number,
|
||||
|
||||
@@ -20,6 +20,8 @@ const {shouldUseNativeDriver} = require('../NativeAnimatedHelper');
|
||||
import type {PlatformConfig} from '../AnimatedPlatformConfig';
|
||||
import type {AnimationConfig, EndCallback} from './Animation';
|
||||
|
||||
import AnimatedColor from '../nodes/AnimatedColor';
|
||||
|
||||
export type TimingAnimationConfig = {
|
||||
...AnimationConfig,
|
||||
toValue:
|
||||
@@ -31,6 +33,14 @@ export type TimingAnimationConfig = {
|
||||
...
|
||||
}
|
||||
| AnimatedValueXY
|
||||
| {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number,
|
||||
...
|
||||
}
|
||||
| AnimatedColor
|
||||
| AnimatedInterpolation,
|
||||
easing?: (value: number) => number,
|
||||
duration?: number,
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import AnimatedValue from './AnimatedValue';
|
||||
import AnimatedWithChildren from './AnimatedWithChildren';
|
||||
import invariant from 'invariant';
|
||||
|
||||
type ColorListenerCallback = (value: string) => mixed;
|
||||
|
||||
let _uniqueId = 1;
|
||||
|
||||
export default class AnimatedColor extends AnimatedWithChildren {
|
||||
r: AnimatedValue;
|
||||
g: AnimatedValue;
|
||||
b: AnimatedValue;
|
||||
a: AnimatedValue;
|
||||
_listeners: {
|
||||
[key: string]: {
|
||||
r: string,
|
||||
g: string,
|
||||
b: string,
|
||||
a: string,
|
||||
...
|
||||
},
|
||||
...
|
||||
};
|
||||
|
||||
constructor(
|
||||
valueIn?: ?{
|
||||
+r: number | AnimatedValue,
|
||||
+g: number | AnimatedValue,
|
||||
+b: number | AnimatedValue,
|
||||
+a: number | AnimatedValue,
|
||||
...
|
||||
}, // TODO: support string color and platform color
|
||||
) {
|
||||
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);
|
||||
} 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;
|
||||
}
|
||||
this._listeners = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, ...}) {
|
||||
this.r.setValue(value.r);
|
||||
this.g.setValue(value.g);
|
||||
this.b.setValue(value.b);
|
||||
this.a.setValue(value.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an offset that is applied on top of whatever value is set, whether
|
||||
* 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, ...}) {
|
||||
this.r.setOffset(offset.r);
|
||||
this.g.setOffset(offset.g);
|
||||
this.b.setOffset(offset.b);
|
||||
this.a.setOffset(offset.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the offset value into the base value and resets the offset to zero.
|
||||
* The final output of the value is unchanged.
|
||||
*/
|
||||
flattenOffset(): void {
|
||||
this.r.flattenOffset();
|
||||
this.g.flattenOffset();
|
||||
this.b.flattenOffset();
|
||||
this.a.flattenOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset value to the base value, and resets the base value to
|
||||
* zero. The final output of the value is unchanged.
|
||||
*/
|
||||
extractOffset(): void {
|
||||
this.r.extractOffset();
|
||||
this.g.extractOffset();
|
||||
this.b.extractOffset();
|
||||
this.a.extractOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an asynchronous listener to the value so you can observe updates from
|
||||
* animations. This is useful because there is no way to synchronously read
|
||||
* the value because it might be driven natively.
|
||||
*
|
||||
* Returns a string that serves as an identifier for the listener.
|
||||
*/
|
||||
addListener(callback: ColorListenerCallback): string {
|
||||
const id = String(_uniqueId++);
|
||||
const jointCallback = ({value: number}) => {
|
||||
callback(this.__getValue());
|
||||
};
|
||||
this._listeners[id] = {
|
||||
r: this.r.addListener(jointCallback),
|
||||
g: this.g.addListener(jointCallback),
|
||||
b: this.b.addListener(jointCallback),
|
||||
a: this.a.addListener(jointCallback),
|
||||
};
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a listener. The `id` param shall match the identifier
|
||||
* previously returned by `addListener()`.
|
||||
*/
|
||||
removeListener(id: string): void {
|
||||
this.r.removeListener(this._listeners[id].r);
|
||||
this.g.removeListener(this._listeners[id].g);
|
||||
this.b.removeListener(this._listeners[id].b);
|
||||
this.a.removeListener(this._listeners[id].a);
|
||||
delete this._listeners[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all registered listeners.
|
||||
*/
|
||||
removeAllListeners(): void {
|
||||
this.r.removeAllListeners();
|
||||
this.g.removeAllListeners();
|
||||
this.b.removeAllListeners();
|
||||
this.a.removeAllListeners();
|
||||
this._listeners = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops any running animation or tracking. `callback` is invoked with the
|
||||
* final value after stopping the animation, which is useful for updating
|
||||
* state to match the animation position with layout.
|
||||
*/
|
||||
stopAnimation(callback?: (value: string) => void): void {
|
||||
this.r.stopAnimation();
|
||||
this.g.stopAnimation();
|
||||
this.b.stopAnimation();
|
||||
this.a.stopAnimation();
|
||||
callback && callback(this.__getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops any animation and resets the value to its original.
|
||||
*/
|
||||
resetAnimation(callback?: (value: string) => void): void {
|
||||
this.r.resetAnimation();
|
||||
this.g.resetAnimation();
|
||||
this.b.resetAnimation();
|
||||
this.a.resetAnimation();
|
||||
callback && callback(this.__getValue());
|
||||
}
|
||||
|
||||
__getValue(): string {
|
||||
return `rgba(${this.r.__getValue()}, ${this.g.__getValue()}, ${this.b.__getValue()}, ${this.a.__getValue()})`;
|
||||
}
|
||||
|
||||
__attach(): void {
|
||||
this.r.__addChild(this);
|
||||
this.g.__addChild(this);
|
||||
this.b.__addChild(this);
|
||||
this.a.__addChild(this);
|
||||
super.__attach();
|
||||
}
|
||||
|
||||
__detach(): void {
|
||||
this.r.__removeChild(this);
|
||||
this.g.__removeChild(this);
|
||||
this.b.__removeChild(this);
|
||||
this.a.__removeChild(this);
|
||||
super.__detach();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user