diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js index 1a54f78abb5a23..0ec517d12bd560 100644 --- a/Libraries/Animated/src/nodes/AnimatedTracking.js +++ b/Libraries/Animated/src/nodes/AnimatedTracking.js @@ -14,6 +14,10 @@ const AnimatedValue = require('./AnimatedValue'); const AnimatedNode = require('./AnimatedNode'); +const { + generateNewAnimationId, + shouldUseNativeDriver, +} = require('../NativeAnimatedHelper'); import type {EndCallback} from '../animations/Animation'; @@ -23,6 +27,7 @@ class AnimatedTracking extends AnimatedNode { _callback: ?EndCallback; _animationConfig: Object; _animationClass: any; + _useNativeDriver: boolean; constructor( value: AnimatedValue, @@ -36,16 +41,32 @@ class AnimatedTracking extends AnimatedNode { this._parent = parent; this._animationClass = animationClass; this._animationConfig = animationConfig; + this._useNativeDriver = shouldUseNativeDriver(animationConfig); this._callback = callback; this.__attach(); } + __makeNative() { + this.__isNative = true; + this._parent.__makeNative(); + super.__makeNative(); + this._value.__makeNative(); + } + __getValue(): Object { return this._parent.__getValue(); } __attach(): void { this._parent.__addChild(this); + if (this._useNativeDriver) { + // when the tracking starts we need to convert this node to a "native node" + // so that the parent node will be made "native" too. This is necessary as + // if we don't do this `update` method will get called. At that point it + // may be too late as it would mean the JS driver has already started + // updating node values + this.__makeNative(); + } } __detach(): void { @@ -62,6 +83,22 @@ class AnimatedTracking extends AnimatedNode { this._callback, ); } + + __getNativeConfig(): any { + const animation = new this._animationClass({ + ...this._animationConfig, + // remove toValue from the config as it's a ref to Animated.Value + toValue: undefined, + }); + const animationConfig = animation.__getNativeAnimationConfig(); + return { + type: 'tracking', + animationId: generateNewAnimationId(), + animationConfig, + toValue: this._parent.__getNativeTag(), + value: this._value.__getNativeTag(), + }; + } } module.exports = AnimatedTracking; diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js index 49f440471e1ba3..d5a5de9a9e48c7 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -20,6 +20,7 @@ const NativeAnimatedHelper = require('../NativeAnimatedHelper'); import type Animation, {EndCallback} from '../animations/Animation'; import type {InterpolationConfigType} from './AnimatedInterpolation'; +import type AnimatedTracking from './AnimatedTracking'; const NativeAnimatedAPI = NativeAnimatedHelper.API; @@ -76,7 +77,7 @@ class AnimatedValue extends AnimatedWithChildren { _startingValue: number; _offset: number; _animation: ?Animation; - _tracking: ?AnimatedNode; + _tracking: ?AnimatedTracking; _listeners: {[key: string]: ValueListenerCallback}; __nativeAnimatedValueListener: ?any; @@ -311,7 +312,7 @@ class AnimatedValue extends AnimatedWithChildren { /** * Typically only used internally. */ - track(tracking: AnimatedNode): void { + track(tracking: AnimatedTracking): void { this.stopTracking(); this._tracking = tracking; } diff --git a/Libraries/NativeAnimation/Drivers/RCTAnimationDriver.h b/Libraries/NativeAnimation/Drivers/RCTAnimationDriver.h index b87fd68c41381b..3364f174ae3734 100644 --- a/Libraries/NativeAnimation/Drivers/RCTAnimationDriver.h +++ b/Libraries/NativeAnimation/Drivers/RCTAnimationDriver.h @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)startAnimation; - (void)stepAnimationWithTime:(NSTimeInterval)currentTime; - (void)stopAnimation; +- (void)resetAnimationConfig:(NSDictionary *)config; NS_ASSUME_NONNULL_END diff --git a/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.m b/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.m index cb471f95ed8443..027ca81524f25f 100644 --- a/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.m +++ b/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.m @@ -41,22 +41,27 @@ - (instancetype)initWithId:(NSNumber *)animationId callBack:(nullable RCTResponseSenderBlock)callback; { if ((self = [super init])) { - NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; - + _callback = [callback copy]; _animationId = animationId; + _valueNode = valueNode; _fromValue = 0; _lastValue = 0; - _valueNode = valueNode; - _callback = [callback copy]; - _velocity = [RCTConvert CGFloat:config[@"velocity"]]; - _deceleration = [RCTConvert CGFloat:config[@"deceleration"]]; - _iterations = iterations.integerValue; - _currentLoop = 1; - _animationHasFinished = iterations.integerValue == 0; + _velocity = [RCTConvert CGFloat:config[@"velocity"]]; // initial velocity + [self resetAnimationConfig:config]; } return self; } +- (void)resetAnimationConfig:(NSDictionary *)config +{ + NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; + _fromValue = _lastValue; + _deceleration = [RCTConvert CGFloat:config[@"deceleration"]]; + _iterations = iterations.integerValue; + _currentLoop = 1; + _animationHasFinished = iterations.integerValue == 0; +} + RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)startAnimation diff --git a/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m b/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m index a46fea6bf26ec3..53846b2d0a2204 100644 --- a/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m +++ b/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m @@ -31,6 +31,7 @@ @implementation RCTFrameAnimation NSArray *_frames; CGFloat _toValue; CGFloat _fromValue; + CGFloat _lastPosition; NSTimeInterval _animationStartTime; NSTimeInterval _animationCurrentTime; RCTResponseSenderBlock _callback; @@ -44,23 +45,30 @@ - (instancetype)initWithId:(NSNumber *)animationId callBack:(nullable RCTResponseSenderBlock)callback; { if ((self = [super init])) { - NSNumber *toValue = [RCTConvert NSNumber:config[@"toValue"]] ?: @1; - NSArray *frames = [RCTConvert NSNumberArray:config[@"frames"]]; - NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; - _animationId = animationId; - _toValue = toValue.floatValue; - _fromValue = valueNode.value; + _lastPosition = _fromValue = valueNode.value; _valueNode = valueNode; - _frames = [frames copy]; _callback = [callback copy]; - _animationHasFinished = iterations.integerValue == 0; - _iterations = iterations.integerValue; - _currentLoop = 1; + [self resetAnimationConfig:config]; } return self; } +- (void)resetAnimationConfig:(NSDictionary *)config +{ + NSNumber *toValue = [RCTConvert NSNumber:config[@"toValue"]] ?: @1; + NSArray *frames = [RCTConvert NSNumberArray:config[@"frames"]]; + NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; + + _fromValue = _lastPosition; + _toValue = toValue.floatValue; + _frames = [frames copy]; + _animationStartTime = _animationCurrentTime = -1; + _animationHasFinished = iterations.integerValue == 0; + _iterations = iterations.integerValue; + _currentLoop = 1; +} + RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)startAnimation @@ -144,6 +152,7 @@ - (void)updateOutputWithFrameOutput:(CGFloat)frameOutput EXTRAPOLATE_TYPE_EXTEND, EXTRAPOLATE_TYPE_EXTEND); + _lastPosition = outputValue; _valueNode.value = outputValue; [_valueNode setNeedsUpdate]; } diff --git a/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.m b/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.m index e4811f601d2594..932433a16dae08 100644 --- a/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.m +++ b/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.m @@ -57,33 +57,37 @@ - (instancetype)initWithId:(NSNumber *)animationId callBack:(nullable RCTResponseSenderBlock)callback { if ((self = [super init])) { - NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; - _animationId = animationId; - _toValue = [RCTConvert CGFloat:config[@"toValue"]]; - _fromValue = valueNode.value; - _lastPosition = 0; + _lastPosition = valueNode.value; _valueNode = valueNode; - _overshootClamping = [RCTConvert BOOL:config[@"overshootClamping"]]; - _restDisplacementThreshold = [RCTConvert CGFloat:config[@"restDisplacementThreshold"]]; - _restSpeedThreshold = [RCTConvert CGFloat:config[@"restSpeedThreshold"]]; - _stiffness = [RCTConvert CGFloat:config[@"stiffness"]]; - _damping = [RCTConvert CGFloat:config[@"damping"]]; - _mass = [RCTConvert CGFloat:config[@"mass"]]; - _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; - + _lastVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; _callback = [callback copy]; - - _lastPosition = _fromValue; - _lastVelocity = _initialVelocity; - - _animationHasFinished = iterations.integerValue == 0; - _iterations = iterations.integerValue; - _currentLoop = 1; + [self resetAnimationConfig:config]; } return self; } +- (void)resetAnimationConfig:(NSDictionary *)config +{ + NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1; + _toValue = [RCTConvert CGFloat:config[@"toValue"]]; + _overshootClamping = [RCTConvert BOOL:config[@"overshootClamping"]]; + _restDisplacementThreshold = [RCTConvert CGFloat:config[@"restDisplacementThreshold"]]; + _restSpeedThreshold = [RCTConvert CGFloat:config[@"restSpeedThreshold"]]; + _stiffness = [RCTConvert CGFloat:config[@"stiffness"]]; + _damping = [RCTConvert CGFloat:config[@"damping"]]; + _mass = [RCTConvert CGFloat:config[@"mass"]]; + _initialVelocity = _lastVelocity; + _fromValue = _lastPosition; + _fromValue = _lastPosition; + _lastVelocity = _initialVelocity; + _animationHasFinished = iterations.integerValue == 0; + _iterations = iterations.integerValue; + _currentLoop = 1; + _animationStartTime = _animationCurrentTime = -1; + _animationHasBegun = YES; +} + RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)startAnimation diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h index 198e02c9cfc90f..a8cad4e939c82e 100644 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h @@ -9,12 +9,15 @@ #import +@class RCTNativeAnimatedNodesManager; + @interface RCTAnimatedNode : NSObject - (instancetype)initWithTag:(NSNumber *)tag config:(NSDictionary *)config NS_DESIGNATED_INITIALIZER; @property (nonatomic, readonly) NSNumber *nodeTag; +@property (nonatomic, weak) RCTNativeAnimatedNodesManager *manager; @property (nonatomic, copy, readonly) NSDictionary *config; @property (nonatomic, copy, readonly) NSMapTable *childNodes; diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h new file mode 100644 index 00000000000000..8f3281789ddbb0 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h @@ -0,0 +1,15 @@ +/** + * 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. + */ + +#import "RCTAnimatedNode.h" + + +@interface RCTTrackingAnimatedNode : RCTAnimatedNode + +@end diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m new file mode 100644 index 00000000000000..e77b773e040a12 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m @@ -0,0 +1,54 @@ +/** + * 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. + */ + +#import "RCTTrackingAnimatedNode.h" +#import "RCTValueAnimatedNode.h" +#import "RCTNativeAnimatedNodesManager.h" + +@implementation RCTTrackingAnimatedNode { + NSNumber *_animationId; + NSNumber *_toValueNodeTag; + NSNumber *_valueNodeTag; + NSMutableDictionary *_animationConfig; +} + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config +{ + if ((self = [super initWithTag:tag config:config])) { + _animationId = config[@"animationId"]; + _toValueNodeTag = config[@"toValue"]; + _valueNodeTag = config[@"value"]; + _animationConfig = [NSMutableDictionary dictionaryWithDictionary:config[@"animationConfig"]]; + } + return self; +} + +- (void)onDetachedFromNode:(RCTAnimatedNode *)parent +{ + [self.manager stopAnimation:_animationId]; + [super onDetachedFromNode:parent]; +} + +- (void)performUpdate +{ + [super performUpdate]; + + // change animation config's "toValue" to reflect updated value of the parent node + RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_toValueNodeTag]; + _animationConfig[@"toValue"] = @(node.value); + + [self.manager startAnimatingNode:_animationId + nodeTag:_valueNodeTag + config:_animationConfig + endCallback:nil]; +} + +@end + diff --git a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj index cddec7f5e67eeb..0ba2446271568a 100644 --- a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj +++ b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj @@ -111,6 +111,10 @@ 2D3B5EFE1D9B0B4800451313 /* RCTStyleAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E31D07A6C9005F35D8 /* RCTStyleAnimatedNode.m */; }; 2D3B5EFF1D9B0B4800451313 /* RCTTransformAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */; }; 2D3B5F001D9B0B4800451313 /* RCTValueAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */; }; + 44DB7D942024F74200588FCD /* RCTTrackingAnimatedNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */; }; + 44DB7D952024F74200588FCD /* RCTTrackingAnimatedNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */; }; + 44DB7D972024F75100588FCD /* RCTTrackingAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 44DB7D962024F75100588FCD /* RCTTrackingAnimatedNode.m */; }; + 44DB7D982024F75100588FCD /* RCTTrackingAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 44DB7D962024F75100588FCD /* RCTTrackingAnimatedNode.m */; }; 5C9894951D999639008027DB /* RCTDivisionAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9894941D999639008027DB /* RCTDivisionAnimatedNode.m */; }; 944244D01DB962DA0032A02B /* RCTFrameAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C1294D1D4069170025F25C /* RCTFrameAnimation.m */; }; 944244D11DB962DC0032A02B /* RCTSpringAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C1294F1D4069170025F25C /* RCTSpringAnimation.m */; }; @@ -209,6 +213,8 @@ 19F00F201DC8847500113FEE /* RCTEventAnimation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTEventAnimation.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 19F00F211DC8847500113FEE /* RCTEventAnimation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventAnimation.m; sourceTree = ""; }; 2D2A28201D9B03D100D4039D /* libRCTAnimation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTAnimation.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTrackingAnimatedNode.h; sourceTree = ""; }; + 44DB7D962024F75100588FCD /* RCTTrackingAnimatedNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTTrackingAnimatedNode.m; sourceTree = ""; }; 5C9894931D999639008027DB /* RCTDivisionAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTDivisionAnimatedNode.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 5C9894941D999639008027DB /* RCTDivisionAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDivisionAnimatedNode.m; sourceTree = ""; }; 94C1294A1D4069170025F25C /* RCTAnimationDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTAnimationDriver.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; @@ -256,6 +262,8 @@ 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */, 13E501E61D07A6C9005F35D8 /* RCTValueAnimatedNode.h */, 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */, + 44DB7D932024F74200588FCD /* RCTTrackingAnimatedNode.h */, + 44DB7D962024F75100588FCD /* RCTTrackingAnimatedNode.m */, ); path = Nodes; sourceTree = ""; @@ -314,6 +322,7 @@ 192F69891E823F4A008692C7 /* RCTDiffClampAnimatedNode.h in Headers */, 192F698A1E823F4A008692C7 /* RCTAdditionAnimatedNode.h in Headers */, 192F698B1E823F4A008692C7 /* RCTAnimatedNode.h in Headers */, + 44DB7D952024F74200588FCD /* RCTTrackingAnimatedNode.h in Headers */, 192F698C1E823F4A008692C7 /* RCTInterpolationAnimatedNode.h in Headers */, 192F698D1E823F4A008692C7 /* RCTModuloAnimatedNode.h in Headers */, 192F698E1E823F4A008692C7 /* RCTMultiplicationAnimatedNode.h in Headers */, @@ -340,6 +349,7 @@ 1980B71D1E80D1C4004DC789 /* RCTDiffClampAnimatedNode.h in Headers */, 1980B71F1E80D1C4004DC789 /* RCTAdditionAnimatedNode.h in Headers */, 1980B7211E80D1C4004DC789 /* RCTAnimatedNode.h in Headers */, + 44DB7D942024F74200588FCD /* RCTTrackingAnimatedNode.h in Headers */, 1980B7231E80D1C4004DC789 /* RCTInterpolationAnimatedNode.h in Headers */, 1980B7251E80D1C4004DC789 /* RCTModuloAnimatedNode.h in Headers */, 1980B7271E80D1C4004DC789 /* RCTMultiplicationAnimatedNode.h in Headers */, @@ -441,6 +451,7 @@ 2D3B5EFA1D9B0B4800451313 /* RCTInterpolationAnimatedNode.m in Sources */, 2D3B5EFF1D9B0B4800451313 /* RCTTransformAnimatedNode.m in Sources */, 2D3B5EFC1D9B0B4800451313 /* RCTMultiplicationAnimatedNode.m in Sources */, + 44DB7D982024F75100588FCD /* RCTTrackingAnimatedNode.m in Sources */, 2D3B5EFD1D9B0B4800451313 /* RCTPropsAnimatedNode.m in Sources */, 944244D01DB962DA0032A02B /* RCTFrameAnimation.m in Sources */, 944244D11DB962DC0032A02B /* RCTSpringAnimation.m in Sources */, @@ -466,6 +477,7 @@ 13E501EC1D07A6C9005F35D8 /* RCTMultiplicationAnimatedNode.m in Sources */, 13E501ED1D07A6C9005F35D8 /* RCTPropsAnimatedNode.m in Sources */, 13E501E91D07A6C9005F35D8 /* RCTAnimatedNode.m in Sources */, + 44DB7D972024F75100588FCD /* RCTTrackingAnimatedNode.m in Sources */, 13E501EB1D07A6C9005F35D8 /* RCTInterpolationAnimatedNode.m in Sources */, 13E501E81D07A6C9005F35D8 /* RCTAdditionAnimatedNode.m in Sources */, 5C9894951D999639008027DB /* RCTDivisionAnimatedNode.m in Sources */, diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m index f37046612cbe1d..ee82a8008c7cf6 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m @@ -27,6 +27,7 @@ #import "RCTStyleAnimatedNode.h" #import "RCTTransformAnimatedNode.h" #import "RCTValueAnimatedNode.h" +#import "RCTTrackingAnimatedNode.h" @implementation RCTNativeAnimatedNodesManager { @@ -67,7 +68,8 @@ - (void)createAnimatedNode:(nonnull NSNumber *)tag @"division" : [RCTDivisionAnimatedNode class], @"multiplication" : [RCTMultiplicationAnimatedNode class], @"modulus" : [RCTModuloAnimatedNode class], - @"transform" : [RCTTransformAnimatedNode class]}; + @"transform" : [RCTTransformAnimatedNode class], + @"tracking" : [RCTTrackingAnimatedNode class]}; }); NSString *nodeType = [RCTConvert NSString:config[@"type"]]; @@ -79,6 +81,7 @@ - (void)createAnimatedNode:(nonnull NSNumber *)tag } RCTAnimatedNode *node = [[nodeClass alloc] initWithTag:tag config:config]; + node.manager = self; _animationNodes[tag] = node; [node setNeedsUpdate]; } @@ -222,6 +225,15 @@ - (void)startAnimatingNode:(nonnull NSNumber *)animationId config:(NSDictionary *)config endCallback:(RCTResponseSenderBlock)callBack { + // check if the animation has already started + for (id driver in _activeAnimations) { + if ([driver.animationId isEqual:animationId]) { + // if the animation is running, we restart it with an updated configuration + [driver resetAnimationConfig:config]; + return; + } + } + RCTValueAnimatedNode *valueNode = (RCTValueAnimatedNode *)_animationNodes[nodeTag]; NSString *type = config[@"type"]; diff --git a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m index 70678ac2854269..358264899fb528 100644 --- a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m +++ b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m @@ -865,4 +865,227 @@ - (void)testNativeAnimatedEventDoNotUpdate [_uiManager verify]; } +/** + * Creates a following graph of nodes: + * Value(3, initialValue) ----> Style(4) ---> Props(5) ---> View(viewTag) + * + * Value(3) is set to track Value(1) via Tracking(2) node with the provided animation config + */ +- (void)createAnimatedGraphWithTrackingNode:(NSNumber *)viewTag + initialValue:(CGFloat)initialValue + animationConfig:(NSDictionary *)animationConfig +{ + [_nodesManager createAnimatedNode:@1 + config:@{@"type": @"value", @"value": @(initialValue), @"offset": @0}]; + [_nodesManager createAnimatedNode:@3 + config:@{@"type": @"value", @"value": @(initialValue), @"offset": @0}]; + + [_nodesManager createAnimatedNode:@2 + config:@{@"type": @"tracking", + @"animationId": @70, + @"value": @3, + @"toValue": @1, + @"animationConfig": animationConfig}]; + [_nodesManager createAnimatedNode:@4 + config:@{@"type": @"style", @"style": @{@"translateX": @3}}]; + [_nodesManager createAnimatedNode:@5 + config:@{@"type": @"props", @"props": @{@"style": @4}}]; + + [_nodesManager connectAnimatedNodes:@1 childTag:@2]; + [_nodesManager connectAnimatedNodes:@3 childTag:@4]; + [_nodesManager connectAnimatedNodes:@4 childTag:@5]; + [_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"]; +} + +/** + * In this test we verify that when value is being tracked we can update destination value in the + * middle of ongoing animation and the animation will update and animate to the new spot. This is + * tested using simple 5 frame backed timing animation. + */ +- (void)testTracking +{ + NSArray *frames = @[@0, @0.25, @0.5, @0.75, @1]; + NSDictionary *animationConfig = @{@"type": @"frames", @"frames": frames}; + [self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:animationConfig]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + [[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000 + viewName:@"UIView" + props:RCTPropChecker(@"translateX", 0)]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; + + // update "toValue" to 100, we expect tracking animation to animate now from 0 to 100 in 5 steps + [_nodesManager setAnimatedNodeValue:@1 value:@100]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + for (NSNumber *frame in frames) { + NSNumber *expected = @([frame doubleValue] * 100); + [[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000 + viewName:@"UIView" + props:RCTPropChecker(@"translateX", expected)]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; + } + + // update "toValue" to 0 but run only two frames from the animation, + // we expect tracking animation to animate now from 100 to 75 + [_nodesManager setAnimatedNodeValue:@1 value:@0]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + for (int i = 0; i < 2; i++) { + NSNumber *expected = @(100. * (1. - [frames[i] doubleValue])); + [[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000 + viewName:@"UIView" + props:RCTPropChecker(@"translateX", expected)]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; + } + + // at this point we expect tracking value to be at 75 + // we update "toValue" again to 100 and expect the animation to restart from the current place + [_nodesManager setAnimatedNodeValue:@1 value:@100]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + for (NSNumber *frame in frames) { + NSNumber *expected = @(50. + 50. * [frame doubleValue]); + [[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000 + viewName:@"UIView" + props:RCTPropChecker(@"translateX", expected)]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; + } + + [_nodesManager stepAnimations:_displayLink]; + [[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; +} + +/** + * In this test we verify that when tracking is set up for a given animated node and when the + * animation settles it will not be registered as an active animation and therefore will not + * consume resources on running the animation that has already completed. Then we verify that when + * the value updates the animation will resume as expected and the complete again when reaches the + * end. + */ + + - (void)testTrackingPausesWhenEndValueIsReached +{ + NSArray *frames = @[@0, @0.5, @1]; + NSDictionary *animationConfig = @{@"type": @"frames", @"frames": frames}; + [self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:animationConfig]; + + [_nodesManager setAnimatedNodeValue:@1 value:@100]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + __block int callCount = 0; + [[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) { + callCount++; + }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + + for (NSUInteger i = 0; i < frames.count; i++) { + [_nodesManager stepAnimations:_displayLink]; + } + [_nodesManager stepAnimations:_displayLink]; + XCTAssertEqual(callCount, 4); + + // the animation has completed, we expect no updates to be done + [[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) { + XCTFail("Expected not to be called"); + }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; + + // restore rejected method, we will use it later on + callCount = 0; + [[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) { + callCount++; + }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + + // we update end value and expect the animation to restart + [_nodesManager setAnimatedNodeValue:@1 value:@200]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + for (NSUInteger i = 0; i < frames.count; i++) { + [_nodesManager stepAnimations:_displayLink]; + } + [_nodesManager stepAnimations:_displayLink]; + XCTAssertEqual(callCount, 4); + + // the animation has completed, we expect no updates to be done + [[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + [_nodesManager stepAnimations:_displayLink]; + [_uiManager verify]; +} + +/** + * In this test we verify that when tracking is configured to use spring animation and when the + * destination value updates the current speed of the animated value will be taken into account + * while updating the spring animation and it will smoothly transition to the new end value. + */ +- (void) testSpringTrackingRetainsSpeed +{ + // this spring config correspomds to tension 20 and friction 0.5 which makes the spring settle + // very slowly + NSDictionary *springConfig = @{@"type": @"spring", + @"restSpeedThreshold": @0.001, + @"mass": @1, + @"restDisplacementThreshold": @0.001, + @"initialVelocity": @0.5, + @"damping": @2.5, + @"stiffness": @157.8, + @"overshootClamping": @NO}; + [self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:springConfig]; + + __block CGFloat lastTranslateX = 0; + [[[_uiManager stub] andDo:^(NSInvocation *invocation) { + __unsafe_unretained NSDictionary *props = nil; + [invocation getArgument:&props atIndex:4]; + lastTranslateX = [props[@"translateX"] doubleValue]; + }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + + // update "toValue" to 1, we expect tracking animation to animate now from 0 to 1 + [_nodesManager setAnimatedNodeValue:@1 value:@1]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + // we run several steps of animation until the value starts bouncing, has negative speed and + // passes the final point (that is 1) while going backwards + BOOL isBoucingBack = NO; + CGFloat previousValue = 0; + for (int maxFrames = 500; maxFrames > 0; maxFrames--) { + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + if (previousValue >= 1. && lastTranslateX < 1.) { + isBoucingBack = YES; + break; + } + previousValue = lastTranslateX; + } + XCTAssert(isBoucingBack); + + // we now update "toValue" to 1.5 but since the value have negative speed and has also pretty + // low friction we expect it to keep going in the opposite direction for a few more frames + [_nodesManager setAnimatedNodeValue:@1 value:@1.5]; + [_nodesManager stepAnimations:_displayLink]; // kick off the tracking + + int bounceBackInitialFrames = 0; + BOOL hasTurnedForward = NO; + + // we run 8 seconds of animation + for (int i = 0; i < 8 * 60; i++) { + [_nodesManager stepAnimations:_displayLink]; + if (!hasTurnedForward) { + if (lastTranslateX <= previousValue) { + bounceBackInitialFrames++; + } else { + hasTurnedForward = true; + } + } + previousValue = lastTranslateX; + } + XCTAssert(hasTurnedForward); + XCTAssertGreaterThan(bounceBackInitialFrames, 3); + XCTAssertEqual(lastTranslateX, 1.5); +} + @end diff --git a/RNTester/js/NativeAnimationsExample.js b/RNTester/js/NativeAnimationsExample.js index ed6f1acaf7045a..4aaf2063de57a0 100644 --- a/RNTester/js/NativeAnimationsExample.js +++ b/RNTester/js/NativeAnimationsExample.js @@ -255,6 +255,67 @@ class EventExample extends React.Component<{}, $FlowFixMeState> { } } +class TrackingExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state = { + native: new Animated.Value(0), + toNative: new Animated.Value(0), + js: new Animated.Value(0), + toJS: new Animated.Value(0), + }; + + componentDidMount() { + // we configure spring to take a bit of time to settle so that the user + // have time to click many times and see "toValue" getting updated and + const longSettlingSpring = { + tension: 20, + friction: 0.5, + }; + Animated.spring(this.state.native, { + ...longSettlingSpring, + toValue: this.state.toNative, + useNativeDriver: true, + }).start(); + Animated.spring(this.state.js, { + ...longSettlingSpring, + toValue: this.state.toJS, + useNativeDriver: false, + }).start(); + } + + onPress = () => { + // select next value to be tracked by random + const nextValue = Math.random() * 200; + this.state.toNative.setValue(nextValue); + this.state.toJS.setValue(nextValue); + }; + + renderBlock = (anim, dest) => [ + , + , + ] + + render() { + return ( + + + + Native: + + + {this.renderBlock(this.state.native, this.state.toNative)} + + + JavaScript: + + + {this.renderBlock(this.state.js, this.state.toJS)} + + + + ); + } +} + const styles = StyleSheet.create({ row: { padding: 10, @@ -265,6 +326,14 @@ const styles = StyleSheet.create({ height: 50, backgroundColor: 'blue', }, + line: { + position: 'absolute', + left: 35, + top: 0, + bottom: 0, + width: 1, + backgroundColor: 'red', + }, }); exports.framework = 'React'; @@ -540,6 +609,12 @@ exports.examples = [ return ; }, }, + { + title: 'Animated Tracking - tap me many times', + render: function() { + return ; + }, + }, { title: 'Internal Settings', render: function() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java index ad715d45c9bac1..b2ae607513a13f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java @@ -10,6 +10,8 @@ package com.facebook.react.animated; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationCausedNativeException; +import com.facebook.react.bridge.ReadableMap; /** * Base class for different types of animation drivers. Can be used to implement simple time-based @@ -27,4 +29,15 @@ * android choreographer callback. */ public abstract void runAnimationStep(long frameTimeNanos); + + /** + * This method will get called when some of the configuration gets updated while the animation is + * running. In that case animation should restart keeping its internal state to provide a smooth + * transision. E.g. in case of a spring animation we want to keep the current value and speed and + * start animating with the new properties (different destination or spring settings) + */ + public void resetConfig(ReadableMap config) { + throw new JSApplicationCausedNativeException( + "Animation config for " + getClass().getSimpleName() + " cannot be reset"); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java index 41b6d24ff31239..fb7c00d018f33d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/DecayAnimation.java @@ -18,20 +18,28 @@ public class DecayAnimation extends AnimationDriver { private final double mVelocity; - private final double mDeceleration; - private long mStartFrameTimeMillis = -1; - private double mFromValue = 0d; - private double mLastValue = 0d; + private double mDeceleration; + private long mStartFrameTimeMillis; + private double mFromValue; + private double mLastValue; private int mIterations; private int mCurrentLoop; public DecayAnimation(ReadableMap config) { - mVelocity = config.getDouble("velocity"); + mVelocity = config.getDouble("velocity"); // initial velocity + resetConfig(config); + } + + @Override + public void resetConfig(ReadableMap config) { mDeceleration = config.getDouble("deceleration"); mIterations = config.hasKey("iterations") ? config.getInt("iterations") : 1; mCurrentLoop = 1; mHasFinished = mIterations == 0; + mStartFrameTimeMillis = -1; + mFromValue = 0; + mLastValue = 0; } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java b/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java index 94b12178979326..3d37846fb0c6f0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java @@ -22,17 +22,24 @@ class FrameBasedAnimationDriver extends AnimationDriver { // 60FPS private static final double FRAME_TIME_MILLIS = 1000d / 60d; - private long mStartFrameTimeNanos = -1; - private final double[] mFrames; - private final double mToValue; + private long mStartFrameTimeNanos; + private double[] mFrames; + private double mToValue; private double mFromValue; private int mIterations; private int mCurrentLoop; FrameBasedAnimationDriver(ReadableMap config) { + resetConfig(config); + } + + @Override + public void resetConfig(ReadableMap config) { ReadableArray frames = config.getArray("frames"); int numberOfFrames = frames.size(); - mFrames = new double[numberOfFrames]; + if (mFrames == null || mFrames.length != numberOfFrames) { + mFrames = new double[numberOfFrames]; + } for (int i = 0; i < numberOfFrames; i++) { mFrames[i] = frames.getDouble(i); } @@ -40,6 +47,7 @@ class FrameBasedAnimationDriver extends AnimationDriver { mIterations = config.hasKey("iterations") ? config.getInt("iterations") : 1; mCurrentLoop = 1; mHasFinished = mIterations == 0; + mStartFrameTimeNanos = -1; } @Override @@ -49,7 +57,7 @@ public void runAnimationStep(long frameTimeNanos) { mFromValue = mAnimatedValue.mValue; } long timeFromStartMillis = (frameTimeNanos - mStartFrameTimeNanos) / 1000000; - int frameIndex = (int) (timeFromStartMillis / FRAME_TIME_MILLIS); + int frameIndex = (int) Math.round(timeFromStartMillis / FRAME_TIME_MILLIS); if (frameIndex < 0) { throw new IllegalStateException("Calculated frame index should never be lower than 0"); } else if (mHasFinished) { @@ -60,7 +68,7 @@ public void runAnimationStep(long frameTimeNanos) { if (frameIndex >= mFrames.length - 1) { nextValue = mToValue; if (mIterations == -1 || mCurrentLoop < mIterations) { // looping animation, return to start - mStartFrameTimeNanos = frameTimeNanos; + mStartFrameTimeNanos = frameTimeNanos + ((long) FRAME_TIME_MILLIS) * 1000000L; mCurrentLoop++; } else { // animation has completed, no more frames left mHasFinished = true; diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index d65b20fc503794..d54d24bd668ea9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -105,6 +105,8 @@ public void createAnimatedNode(int tag, ReadableMap config) { node = new DiffClampAnimatedNode(config, this); } else if ("transform".equals(type)) { node = new TransformAnimatedNode(config, this); + } else if ("tracking".equals(type)) { + node = new TrackingAnimatedNode(config, this); } else { throw new JSApplicationIllegalArgumentException("Unsupported node type: " + type); } @@ -189,6 +191,15 @@ public void startAnimatingNode( throw new JSApplicationIllegalArgumentException("Animated node should be of type " + ValueAnimatedNode.class.getName()); } + + final AnimationDriver existingDriver = mActiveAnimations.get(animationId); + if (existingDriver != null) { + // animation with the given ID is already running, we need to update its configuration instead + // of spawning a new one + existingDriver.resetConfig(animationConfig); + return; + } + String type = animationConfig.getString("type"); final AnimationDriver animation; if ("frames".equals(type)) { @@ -214,10 +225,12 @@ private void stopAnimationsForNode(AnimatedNode animatedNode) { for (int i = 0; i < mActiveAnimations.size(); i++) { AnimationDriver animation = mActiveAnimations.valueAt(i); if (animatedNode.equals(animation.mAnimatedValue)) { - // Invoke animation end callback with {finished: false} - WritableMap endCallbackResponse = Arguments.createMap(); - endCallbackResponse.putBoolean("finished", false); - animation.mEndCallback.invoke(endCallbackResponse); + if (animation.mEndCallback != null) { + // Invoke animation end callback with {finished: false} + WritableMap endCallbackResponse = Arguments.createMap(); + endCallbackResponse.putBoolean("finished", false); + animation.mEndCallback.invoke(endCallbackResponse); + } mActiveAnimations.removeAt(i); i--; } @@ -232,10 +245,12 @@ public void stopAnimation(int animationId) { for (int i = 0; i < mActiveAnimations.size(); i++) { AnimationDriver animation = mActiveAnimations.valueAt(i); if (animation.mId == animationId) { - // Invoke animation end callback with {finished: false} - WritableMap endCallbackResponse = Arguments.createMap(); - endCallbackResponse.putBoolean("finished", false); - animation.mEndCallback.invoke(endCallbackResponse); + if (animation.mEndCallback != null) { + // Invoke animation end callback with {finished: false} + WritableMap endCallbackResponse = Arguments.createMap(); + endCallbackResponse.putBoolean("finished", false); + animation.mEndCallback.invoke(endCallbackResponse); + } mActiveAnimations.removeAt(i); return; } @@ -445,9 +460,11 @@ public void runUpdates(long frameTimeNanos) { for (int i = mActiveAnimations.size() - 1; i >= 0; i--) { AnimationDriver animation = mActiveAnimations.valueAt(i); if (animation.mHasFinished) { - WritableMap endCallbackResponse = Arguments.createMap(); - endCallbackResponse.putBoolean("finished", true); - animation.mEndCallback.invoke(endCallbackResponse); + if (animation.mEndCallback != null) { + WritableMap endCallbackResponse = Arguments.createMap(); + endCallbackResponse.putBoolean("finished", true); + animation.mEndCallback.invoke(endCallbackResponse); + } mActiveAnimations.removeAt(i); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java index 83ccc6f74a7184..3558756149b969 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/SpringAnimation.java @@ -37,24 +37,32 @@ private static class PhysicsState { // thresholds for determining when the spring is at rest private double mRestSpeedThreshold; private double mDisplacementFromRestThreshold; - private double mTimeAccumulator = 0; + private double mTimeAccumulator; // for controlling loop private int mIterations; - private int mCurrentLoop = 0; + private int mCurrentLoop; private double mOriginalValue; SpringAnimation(ReadableMap config) { + mCurrentState.velocity = config.getDouble("initialVelocity"); + resetConfig(config); + } + + @Override + public void resetConfig(ReadableMap config) { mSpringStiffness = config.getDouble("stiffness"); mSpringDamping = config.getDouble("damping"); mSpringMass = config.getDouble("mass"); - mInitialVelocity = config.getDouble("initialVelocity"); - mCurrentState.velocity = mInitialVelocity; + mInitialVelocity = mCurrentState.velocity; mEndValue = config.getDouble("toValue"); mRestSpeedThreshold = config.getDouble("restSpeedThreshold"); mDisplacementFromRestThreshold = config.getDouble("restDisplacementThreshold"); mOvershootClampingEnabled = config.getBoolean("overshootClamping"); mIterations = config.hasKey("iterations") ? config.getInt("iterations") : 1; mHasFinished = mIterations == 0; + mCurrentLoop = 0; + mTimeAccumulator = 0; + mSpringStarted = false; } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java new file mode 100644 index 00000000000000..db312d23558078 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java @@ -0,0 +1,37 @@ +/** + * 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. + */ + +package com.facebook.react.animated; + +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReadableMap; + +/* package */ class TrackingAnimatedNode extends AnimatedNode { + + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private final int mAnimationId; + private final int mToValueNode; + private final int mValueNode; + private final JavaOnlyMap mAnimationConfig; + + TrackingAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + mAnimationId = config.getInt("animationId"); + mToValueNode = config.getInt("toValue"); + mValueNode = config.getInt("value"); + mAnimationConfig = JavaOnlyMap.deepClone(config.getMap("animationConfig")); + } + + @Override + public void update() { + AnimatedNode toValue = mNativeAnimatedNodesManager.getNodeById(mToValueNode); + mAnimationConfig.putDouble("toValue", ((ValueAnimatedNode) toValue).getValue()); + mNativeAnimatedNodesManager.startAnimatingNode(mAnimationId, mValueNode, mAnimationConfig, null); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java index ab404d9b787f90..dd648f53d1c1b8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java @@ -36,6 +36,34 @@ public static JavaOnlyArray of(Object... values) { return new JavaOnlyArray(values); } + public static JavaOnlyArray deepClone(ReadableArray ary) { + JavaOnlyArray res = new JavaOnlyArray(); + for (int i = 0, size = ary.size(); i < size; i++) { + ReadableType type = ary.getType(i); + switch (type) { + case Null: + res.pushNull(); + break; + case Boolean: + res.pushBoolean(ary.getBoolean(i)); + break; + case Number: + res.pushDouble(ary.getDouble(i)); + break; + case String: + res.pushString(ary.getString(i)); + break; + case Map: + res.pushMap(JavaOnlyMap.deepClone(ary.getMap(i))); + break; + case Array: + res.pushArray(deepClone(ary.getArray(i))); + break; + } + } + return res; + } + private JavaOnlyArray(Object... values) { mBackingList = Arrays.asList(values); } @@ -60,12 +88,12 @@ public boolean isNull(int index) { @Override public double getDouble(int index) { - return (Double) mBackingList.get(index); + return ((Number) mBackingList.get(index)).doubleValue(); } @Override public int getInt(int index) { - return (Integer) mBackingList.get(index); + return ((Number) mBackingList.get(index)).intValue(); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java index 136786c7b75666..3b9ccf6fdde9de 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java @@ -31,6 +31,36 @@ public static JavaOnlyMap of(Object... keysAndValues) { return new JavaOnlyMap(keysAndValues); } + public static JavaOnlyMap deepClone(ReadableMap map) { + JavaOnlyMap res = new JavaOnlyMap(); + ReadableMapKeySetIterator iter = map.keySetIterator(); + while (iter.hasNextKey()) { + String propKey = iter.nextKey(); + ReadableType type = map.getType(propKey); + switch (type) { + case Null: + res.putNull(propKey); + break; + case Boolean: + res.putBoolean(propKey, map.getBoolean(propKey)); + break; + case Number: + res.putDouble(propKey, map.getDouble(propKey)); + break; + case String: + res.putString(propKey, map.getString(propKey)); + break; + case Map: + res.putMap(propKey, deepClone(map.getMap(propKey))); + break; + case Array: + res.putArray(propKey, JavaOnlyArray.deepClone(map.getArray(propKey))); + break; + } + } + return res; + } + /** * @param keysAndValues keys and values, interleaved */ @@ -65,12 +95,12 @@ public boolean getBoolean(String name) { @Override public double getDouble(String name) { - return (Double) mBackingMap.get(name); + return ((Number) mBackingMap.get(name)).doubleValue(); } @Override public int getInt(String name) { - return (Integer) mBackingMap.get(name); + return ((Number) mBackingMap.get(name)).intValue(); } @Override diff --git a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index 6f244c70be4baf..e1d1958613e6ee 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -171,11 +171,6 @@ public void testFramesAnimation() { ArgumentCaptor stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); - for (int i = 0; i < frames.size(); i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); @@ -205,11 +200,6 @@ public void testFramesAnimationLoopsFiveTimes() { ArgumentCaptor stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0); - for (int iteration = 0; iteration < 5; iteration++) { for (int i = 0; i < frames.size(); i++) { reset(mUIImplementationMock); @@ -270,9 +260,6 @@ public void testNodeValueListenerIfListening() { JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d), animationCallback); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(valueListener).onValueUpdate(eq(0d)); - for (int i = 0; i < frames.size(); i++) { reset(valueListener); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); @@ -600,7 +587,6 @@ public void testAnimationCallbackFinish() { reset(animationCallback); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verifyNoMoreInteractions(animationCallback); reset(animationCallback); @@ -629,10 +615,10 @@ private void createAnimatedGraphWithAdditionNode( double secondValue) { mNativeAnimatedNodesManager.createAnimatedNode( 1, - JavaOnlyMap.of("type", "value", "value", 100d, "offset", 0d)); + JavaOnlyMap.of("type", "value", "value", firstValue, "offset", 0d)); mNativeAnimatedNodesManager.createAnimatedNode( 2, - JavaOnlyMap.of("type", "value", "value", 1000d, "offset", 0d)); + JavaOnlyMap.of("type", "value", "value", secondValue, "offset", 0d)); mNativeAnimatedNodesManager.createAnimatedNode( 3, @@ -648,7 +634,7 @@ private void createAnimatedGraphWithAdditionNode( mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3); mNativeAnimatedNodesManager.connectAnimatedNodes(3, 4); mNativeAnimatedNodesManager.connectAnimatedNodes(4, 5); - mNativeAnimatedNodesManager.connectAnimatedNodeToView(5, 50); + mNativeAnimatedNodesManager.connectAnimatedNodeToView(5, viewTag); } @Test @@ -677,12 +663,6 @@ public void testAdditionNode() { verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock) - .synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); - reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock) @@ -722,12 +702,6 @@ public void testViewReceiveUpdatesIfOneOfAnimationHasntStarted() { verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock) - .synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); - reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock) @@ -777,11 +751,6 @@ public void testViewReceiveUpdatesWhenOneOfAnimationHasFinished() { verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(1100d); - for (int i = 1; i < secondFrames.size(); i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); @@ -843,11 +812,6 @@ public void testMultiplicationNode() { verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(5d); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(5d); - reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); @@ -949,11 +913,6 @@ public void testInterpolationNode() { ArgumentCaptor stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(50), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0d); - for (int i = 0; i < frames.size(); i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); @@ -1088,11 +1047,6 @@ public void testRestoreDefaultProps() { ArgumentCaptor stylesCaptor = ArgumentCaptor.forClass(ReactStylesDiffMap.class); - reset(mUIImplementationMock); - mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()); - assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(1); - for (int i = 0; i < frames.size(); i++) { reset(mUIImplementationMock); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); @@ -1106,4 +1060,228 @@ public void testRestoreDefaultProps() { verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()); assertThat(stylesCaptor.getValue().isNull("opacity")); } + + + /** + * Creates a following graph of nodes: + * Value(3, initialValue) ----> Style(4) ---> Props(5) ---> View(viewTag) + * + * Value(3) is set to track Value(1) via Tracking(2) node with the provided animation config + */ + private void createAnimatedGraphWithTrackingNode( + int viewTag, + double initialValue, + JavaOnlyMap animationConfig) { + mNativeAnimatedNodesManager.createAnimatedNode( + 1, + JavaOnlyMap.of("type", "value", "value", initialValue, "offset", 0d)); + mNativeAnimatedNodesManager.createAnimatedNode( + 3, + JavaOnlyMap.of("type", "value", "value", initialValue, "offset", 0d)); + + mNativeAnimatedNodesManager.createAnimatedNode( + 2, + JavaOnlyMap.of("type", "tracking", "animationId", 70, "value", 3, "toValue", 1, "animationConfig", animationConfig)); + + mNativeAnimatedNodesManager.createAnimatedNode( + 4, + JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("translateX", 3))); + mNativeAnimatedNodesManager.createAnimatedNode( + 5, + JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 4))); + mNativeAnimatedNodesManager.connectAnimatedNodes(1, 2); + mNativeAnimatedNodesManager.connectAnimatedNodes(3, 4); + mNativeAnimatedNodesManager.connectAnimatedNodes(4, 5); + mNativeAnimatedNodesManager.connectAnimatedNodeToView(5, viewTag); + } + + /** + * In this test we verify that when value is being tracked we can update destination value in the + * middle of ongoing animation and the animation will update and animate to the new spot. This is + * tested using simple 5 frame backed timing animation. + */ + @Test + public void testTracking() { + JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.25d, 0.5d, 0.75d, 1d); + JavaOnlyMap animationConfig = JavaOnlyMap.of("type", "frames", "frames", frames); + + createAnimatedGraphWithTrackingNode(1000, 0d, animationConfig); + + ArgumentCaptor stylesCaptor = + ArgumentCaptor.forClass(ReactStylesDiffMap.class); + + reset(mUIImplementationMock); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); + assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)).isEqualTo(0d); + + // update "toValue" to 100, we expect tracking animation to animate now from 0 to 100 in 5 steps + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 100d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // kick off the animation + + for (int i = 0; i < frames.size(); i++) { + reset(mUIImplementationMock); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verify(mUIImplementationMock) + .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); + assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)) + .isEqualTo(frames.getDouble(i) * 100d); + } + + // update "toValue" to 0 but run only two frames from the animation, + // we expect tracking animation to animate now from 100 to 75 + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 0d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // kick off the animation + + for (int i = 0; i < 2; i++) { + reset(mUIImplementationMock); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verify(mUIImplementationMock) + .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); + assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)) + .isEqualTo(100d * (1d - frames.getDouble(i))); + } + + // at this point we expect tracking value to be at 75 + assertThat(((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue()) + .isEqualTo(75d); + + // we update "toValue" again to 100 and expect the animation to restart from the current place + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 100d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // kick off the animation + + for (int i = 0; i < frames.size(); i++) { + reset(mUIImplementationMock); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verify(mUIImplementationMock) + .synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture()); + assertThat(stylesCaptor.getValue().getDouble("translateX", Double.NaN)) + .isEqualTo(50d + 50d * frames.getDouble(i)); + } + } + + /** + * In this test we verify that when tracking is set up for a given animated node and when the + * animation settles it will not be registered as an active animation and therefore will not + * consume resources on running the animation that has already completed. Then we verify that when + * the value updates the animation will resume as expected and the complete again when reaches the + * end. + */ + @Test + public void testTrackingPausesWhenEndValueIsReached() { + JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.5d, 1d); + JavaOnlyMap animationConfig = JavaOnlyMap.of("type", "frames", "frames", frames); + + createAnimatedGraphWithTrackingNode(1000, 0d, animationConfig); + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 100d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // make sure animation starts + + reset(mUIImplementationMock); + for (int i = 0; i < frames.size(); i++) { + assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isTrue(); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + } + verify(mUIImplementationMock, times(frames.size())) + .synchronouslyUpdateViewOnUIThread(eq(1000), any(ReactStylesDiffMap.class)); + + // the animation has completed, we expect no updates to be done + reset(mUIImplementationMock); + assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isFalse(); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verifyNoMoreInteractions(mUIImplementationMock); + + + // we update end value and expect the animation to restart + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 200d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); // make sure animation starts + + reset(mUIImplementationMock); + for (int i = 0; i < frames.size(); i++) { + assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isTrue(); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + } + verify(mUIImplementationMock, times(frames.size())) + .synchronouslyUpdateViewOnUIThread(eq(1000), any(ReactStylesDiffMap.class)); + + // the animation has completed, we expect no updates to be done + reset(mUIImplementationMock); + assertThat(mNativeAnimatedNodesManager.hasActiveAnimations()).isFalse(); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + verifyNoMoreInteractions(mUIImplementationMock); + } + + /** + * In this test we verify that when tracking is configured to use spring animation and when the + * destination value updates the current speed of the animated value will be taken into account + * while updating the spring animation and it will smoothly transition to the new end value. + */ + @Test + public void testSpringTrackingRetainsSpeed() { + // this spring config correspomds to tension 20 and friction 0.5 which makes the spring settle + // very slowly + JavaOnlyMap springConfig = JavaOnlyMap.of( + "type", + "spring", + "restSpeedThreshold", + 0.001, + "mass", + 1d, + "restDisplacementThreshold", + 0.001, + "initialVelocity", + 0.5d, + "damping", + 2.5, + "stiffness", + 157.8, + "overshootClamping", + false); + + createAnimatedGraphWithTrackingNode(1000, 0d, springConfig); + + // update "toValue" to 1, we expect tracking animation to animate now from 0 to 1 + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 1d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + + // we run several steps of animation until the value starts bouncing, has negative speed and + // passes the final point (that is 1) while going backwards + boolean isBoucingBack = false; + double previousValue = ((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue(); + for (int maxFrames = 500; maxFrames > 0; maxFrames--) { + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + double currentValue = ((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue(); + if (previousValue >= 1d && currentValue < 1d) { + isBoucingBack = true; + break; + } + previousValue = currentValue; + } + assertThat(isBoucingBack).isTrue(); + + // we now update "toValue" to 1.5 but since the value have negative speed and has also pretty + // low friction we expect it to keep going in the opposite direction for a few more frames + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 1.5d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + int bounceBackInitialFrames = 0; + boolean hasTurnedForward = false; + + // we run 8 seconds of animation + for (int i = 0; i < 8 * 60; i++) { + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + double currentValue = ((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue(); + if (!hasTurnedForward) { + if (currentValue <= previousValue) { + bounceBackInitialFrames++; + } else { + hasTurnedForward = true; + } + } + previousValue = currentValue; + } + assertThat(hasTurnedForward).isEqualTo(true); + assertThat(bounceBackInitialFrames).isGreaterThan(3); + + // we verify that the value settled at 2 + assertThat(previousValue).isEqualTo(1.5d); + } }