From 2a3055efa7f925897b177c66e42346d3c5073d31 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Fri, 2 Feb 2018 21:43:13 +0100 Subject: [PATCH 01/12] Implement tracking animated node in native driver for iOS --- .../Animated/src/nodes/AnimatedTracking.js | 41 +++++++ Libraries/Animated/src/nodes/AnimatedValue.js | 5 +- .../Drivers/RCTAnimationDriver.h | 1 + .../Drivers/RCTDecayAnimation.m | 23 ++-- .../Drivers/RCTFrameAnimation.m | 28 +++-- .../Drivers/RCTSpringAnimation.m | 44 ++++---- .../NativeAnimation/Nodes/RCTAnimatedNode.h | 10 +- .../NativeAnimation/Nodes/RCTAnimatedNode.m | 12 ++- .../Nodes/RCTTrackingAnimatedNode.h | 14 +++ .../Nodes/RCTTrackingAnimatedNode.m | 47 ++++++++ .../RCTAnimation.xcodeproj/project.pbxproj | 12 +++ .../RCTNativeAnimatedNodesManager.m | 19 +++- RNTester/js/NativeAnimationsExample.js | 101 ++++++++++++++++++ 13 files changed, 311 insertions(+), 46 deletions(-) create mode 100644 Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h create mode 100644 Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js index 1a54f78abb5a23..847d20d85c2efb 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'; @@ -36,10 +40,18 @@ 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(); } @@ -53,6 +65,19 @@ class AnimatedTracking extends AnimatedNode { super.__detach(); } + start() { + 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(); + } + // if the node does not use native driver we don't need to take any action + // the animation will be created when the "toValue" gets updated + } + update(): void { this._value.animate( new this._animationClass({ @@ -62,6 +87,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..33d36c438263f2 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -76,7 +76,7 @@ class AnimatedValue extends AnimatedWithChildren { _startingValue: number; _offset: number; _animation: ?Animation; - _tracking: ?AnimatedNode; + _tracking: ?AnimatedTracking; _listeners: {[key: string]: ValueListenerCallback}; __nativeAnimatedValueListener: ?any; @@ -311,9 +311,10 @@ class AnimatedValue extends AnimatedWithChildren { /** * Typically only used internally. */ - track(tracking: AnimatedNode): void { + track(tracking: AnimatedTracking): void { this.stopTracking(); this._tracking = tracking; + this._tracking.start(); } _updateValue(value: number, flush: boolean): void { 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..d5da33bf26a372 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 = -1; + _animationHasFinished = iterations.integerValue == 0; + _iterations = iterations.integerValue; + _currentLoop = 1; +} + RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)startAnimation 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..33da4db9cef674 100644 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h @@ -9,6 +9,8 @@ #import +typedef void (^AnimatedPostOperation)(id nodesManager); + @interface RCTAnimatedNode : NSObject - (instancetype)initWithTag:(NSNumber *)tag @@ -30,7 +32,13 @@ /** * The node will update its value if necesarry and only after its parents have updated. */ -- (void)updateNodeIfNecessary NS_REQUIRES_SUPER; +- (void)updateNodeIfNecessary:(NSMutableArray *)postUpdateQueue NS_REQUIRES_SUPER; + +/** + * The node can call this method to enqueue some actions to be executed after all the values + * are updated. + */ +- (void)schedulePostUpdate:(AnimatedPostOperation)operation NS_REQUIRES_SUPER; /** * Where the actual update code lives. Called internally from updateNodeIfNecessary diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m index 0b8bd7ab02f16e..3643728f0196e1 100644 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m @@ -15,6 +15,7 @@ @implementation RCTAnimatedNode { NSMapTable *_childNodes; NSMapTable *_parentNodes; + NSMutableArray *_postUpdateQueue; } - (instancetype)initWithTag:(NSNumber *)tag @@ -99,16 +100,23 @@ - (void)setNeedsUpdate } } -- (void)updateNodeIfNecessary +- (void)updateNodeIfNecessary:(NSMutableArray *)postUpdateQueue { if (_needsUpdate) { for (RCTAnimatedNode *parent in _parentNodes.objectEnumerator) { - [parent updateNodeIfNecessary]; + [parent updateNodeIfNecessary:postUpdateQueue]; } + _postUpdateQueue = postUpdateQueue; [self performUpdate]; + _postUpdateQueue = nil; } } +- (void)schedulePostUpdate:(AnimatedPostOperation)operation +{ + [_postUpdateQueue addObject:operation]; +} + - (void)performUpdate { _needsUpdate = NO; diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h new file mode 100644 index 00000000000000..eb68ceb9f5d473 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h @@ -0,0 +1,14 @@ +/** + * 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..7850d775a61378 --- /dev/null +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m @@ -0,0 +1,47 @@ +/** + * 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 + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config; +{ + if ((self = [super initWithTag:tag config:config])) { + + } + return self; +} + +- (void)performUpdate +{ + [super performUpdate]; + + NSNumber *nodeTag = self.config[@"toValue"]; + RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:nodeTag]; + + NSNumber *animationId = self.config[@"animationId"]; + NSNumber *valueNodeTag = self.config[@"value"]; + + NSMutableDictionary *config = [NSMutableDictionary dictionaryWithDictionary:self.config[@"animationConfig"]]; + [config setValue:@(node.value) forKey:@"toValue"]; + + [self schedulePostUpdate:^(RCTNativeAnimatedNodesManager *manager) { + [manager startAnimatingNode:animationId + nodeTag:valueNodeTag + config:config + 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..47f933b82bdbf6 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"]]; @@ -222,6 +224,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"]; @@ -420,11 +431,15 @@ - (void)stepAnimations:(CADisplayLink *)displaylink - (void)updateAnimations { + NSMutableArray *postUpdate = [NSMutableArray new]; [_animationNodes enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, RCTAnimatedNode *node, BOOL *stop) { if (node.needsUpdate) { - [node updateNodeIfNecessary]; + [node updateNodeIfNecessary:postUpdate]; } }]; + for (AnimatedPostOperation op in postUpdate) { + op(self); + } } @end diff --git a/RNTester/js/NativeAnimationsExample.js b/RNTester/js/NativeAnimationsExample.js index ed6f1acaf7045a..928208b6f09172 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,38 @@ exports.examples = [ return ; }, }, + { + title: 'translateX => Animated.spring (bounciness/speed)', + render: function() { + return ( + + {anim => ( + + )} + + ); + }, + }, + { + title: 'Animated Tracking - tap me many times', + render: function() { + return ; + }, + }, { title: 'Internal Settings', render: function() { From ea5353f0b6441772530c0dc896e9a7c11212e0d0 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 5 Feb 2018 13:08:26 +0100 Subject: [PATCH 02/12] Add nodes manager as ref to native node to allow proper detaching of the nods --- .../Animated/src/nodes/AnimatedTracking.js | 15 +++----- Libraries/Animated/src/nodes/AnimatedValue.js | 1 - .../NativeAnimation/Nodes/RCTAnimatedNode.h | 11 ++---- .../NativeAnimation/Nodes/RCTAnimatedNode.m | 12 ++----- .../Nodes/RCTTrackingAnimatedNode.h | 1 + .../Nodes/RCTTrackingAnimatedNode.m | 34 +++++++++++++------ .../RCTNativeAnimatedNodesManager.h | 8 +++++ .../RCTNativeAnimatedNodesManager.m | 14 ++++++-- 8 files changed, 53 insertions(+), 43 deletions(-) diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js index 847d20d85c2efb..5b8038f2f8a2cf 100644 --- a/Libraries/Animated/src/nodes/AnimatedTracking.js +++ b/Libraries/Animated/src/nodes/AnimatedTracking.js @@ -58,14 +58,6 @@ class AnimatedTracking extends AnimatedNode { __attach(): void { this._parent.__addChild(this); - } - - __detach(): void { - this._parent.__removeChild(this); - super.__detach(); - } - - start() { 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 @@ -74,8 +66,11 @@ class AnimatedTracking extends AnimatedNode { // updating node values this.__makeNative(); } - // if the node does not use native driver we don't need to take any action - // the animation will be created when the "toValue" gets updated + } + + __detach(): void { + this._parent.__removeChild(this); + super.__detach(); } update(): void { diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js index 33d36c438263f2..52082a2f09536a 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -314,7 +314,6 @@ class AnimatedValue extends AnimatedWithChildren { track(tracking: AnimatedTracking): void { this.stopTracking(); this._tracking = tracking; - this._tracking.start(); } _updateValue(value: number, flush: boolean): void { diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h index 33da4db9cef674..bef5d3c05af4d2 100644 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h @@ -9,7 +9,7 @@ #import -typedef void (^AnimatedPostOperation)(id nodesManager); +@class RCTNativeAnimatedNodesManager; @interface RCTAnimatedNode : NSObject @@ -17,6 +17,7 @@ typedef void (^AnimatedPostOperation)(id nodesManager); 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; @@ -32,13 +33,7 @@ typedef void (^AnimatedPostOperation)(id nodesManager); /** * The node will update its value if necesarry and only after its parents have updated. */ -- (void)updateNodeIfNecessary:(NSMutableArray *)postUpdateQueue NS_REQUIRES_SUPER; - -/** - * The node can call this method to enqueue some actions to be executed after all the values - * are updated. - */ -- (void)schedulePostUpdate:(AnimatedPostOperation)operation NS_REQUIRES_SUPER; +- (void)updateNodeIfNecessary; /** * Where the actual update code lives. Called internally from updateNodeIfNecessary diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m index 3643728f0196e1..0b8bd7ab02f16e 100644 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.m @@ -15,7 +15,6 @@ @implementation RCTAnimatedNode { NSMapTable *_childNodes; NSMapTable *_parentNodes; - NSMutableArray *_postUpdateQueue; } - (instancetype)initWithTag:(NSNumber *)tag @@ -100,23 +99,16 @@ - (void)setNeedsUpdate } } -- (void)updateNodeIfNecessary:(NSMutableArray *)postUpdateQueue +- (void)updateNodeIfNecessary { if (_needsUpdate) { for (RCTAnimatedNode *parent in _parentNodes.objectEnumerator) { - [parent updateNodeIfNecessary:postUpdateQueue]; + [parent updateNodeIfNecessary]; } - _postUpdateQueue = postUpdateQueue; [self performUpdate]; - _postUpdateQueue = nil; } } -- (void)schedulePostUpdate:(AnimatedPostOperation)operation -{ - [_postUpdateQueue addObject:operation]; -} - - (void)performUpdate { _needsUpdate = NO; diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h index eb68ceb9f5d473..8f3281789ddbb0 100644 --- a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h @@ -9,6 +9,7 @@ #import "RCTAnimatedNode.h" + @interface RCTTrackingAnimatedNode : RCTAnimatedNode @end diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m index 7850d775a61378..7abf35e3eca6bd 100644 --- a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m @@ -11,31 +11,43 @@ #import "RCTValueAnimatedNode.h" #import "RCTNativeAnimatedNodesManager.h" -@implementation RCTTrackingAnimatedNode +@implementation RCTTrackingAnimatedNode { + NSNumber *_animationId; + NSNumber *_nodeTag; + NSNumber *_valueNodeTag; + NSMutableDictionary *_animationConfig; +} - (instancetype)initWithTag:(NSNumber *)tag - config:(NSDictionary *)config; + config:(NSDictionary *)config { if ((self = [super initWithTag:tag config:config])) { - + _animationId = config[@"animationId"]; + _nodeTag = config[@"toValue"]; + _valueNodeTag = config[@"value"]; + _animationConfig = config[@"animationConfig"]; } return self; } +- (void)onDetachedFromNode:(RCTAnimatedNode *)parent +{ + [self.manager stopAnimation:_animationId]; + [super onDetachedFromNode:parent]; +} + - (void)performUpdate { [super performUpdate]; - NSNumber *nodeTag = self.config[@"toValue"]; - RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:nodeTag]; - - NSNumber *animationId = self.config[@"animationId"]; - NSNumber *valueNodeTag = self.config[@"value"]; - - NSMutableDictionary *config = [NSMutableDictionary dictionaryWithDictionary:self.config[@"animationConfig"]]; + // clone animation config and update "toValue" to reflect updated value of the parent node + RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_nodeTag]; + NSMutableDictionary *config = [NSMutableDictionary dictionaryWithDictionary:_animationConfig]; [config setValue:@(node.value) forKey:@"toValue"]; - [self schedulePostUpdate:^(RCTNativeAnimatedNodesManager *manager) { + NSNumber *animationId = _animationId; + NSNumber *valueNodeTag = _valueNodeTag; + [self.manager schedulePostUpdateOperation:^(RCTNativeAnimatedNodesManager * _Nonnull manager) { [manager startAnimatingNode:animationId nodeTag:valueNodeTag config:config diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h index 1a0b684c10a271..b7c4ad3effe041 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h @@ -13,6 +13,10 @@ #import #import +@class RCTNativeAnimatedNodesManager; + +typedef void (^AnimatedPostOperation)(RCTNativeAnimatedNodesManager * _Nonnull manager); + @interface RCTNativeAnimatedNodesManager : NSObject - (nonnull instancetype)initWithUIManager:(nonnull RCTUIManager *)uiManager; @@ -85,4 +89,8 @@ - (void)stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag; +// other + +- (void)schedulePostUpdateOperation:(nonnull AnimatedPostOperation)operartion; + @end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m index 47f933b82bdbf6..88dc19557475df 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m @@ -38,6 +38,7 @@ @implementation RCTNativeAnimatedNodesManager NSMutableDictionary *> *_eventDrivers; NSMutableSet> *_activeAnimations; CADisplayLink *_displayLink; + NSMutableArray *_postUpdateQueue; } - (instancetype)initWithUIManager:(nonnull RCTUIManager *)uiManager @@ -81,6 +82,7 @@ - (void)createAnimatedNode:(nonnull NSNumber *)tag } RCTAnimatedNode *node = [[nodeClass alloc] initWithTag:tag config:config]; + node.manager = self; _animationNodes[tag] = node; [node setNeedsUpdate]; } @@ -429,17 +431,23 @@ - (void)stepAnimations:(CADisplayLink *)displaylink #pragma mark -- Updates +- (void)schedulePostUpdateOperation:(AnimatedPostOperation)operartion +{ + [_postUpdateQueue addObject:operartion]; +} + - (void)updateAnimations { - NSMutableArray *postUpdate = [NSMutableArray new]; + _postUpdateQueue = [NSMutableArray new]; [_animationNodes enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, RCTAnimatedNode *node, BOOL *stop) { if (node.needsUpdate) { - [node updateNodeIfNecessary:postUpdate]; + [node updateNodeIfNecessary]; } }]; - for (AnimatedPostOperation op in postUpdate) { + for (AnimatedPostOperation op in _postUpdateQueue) { op(self); } + _postUpdateQueue = nil; } @end From ada41b010e66b2d33cf8ed48c92c36813137ca2e Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 5 Feb 2018 16:26:21 +0100 Subject: [PATCH 03/12] Implement native driver tracking for Android --- .../Nodes/RCTTrackingAnimatedNode.m | 8 +-- .../react/animated/AnimationDriver.java | 13 ++++ .../animated/NativeAnimatedNodesManager.java | 61 +++++++++++++++---- .../react/animated/SpringAnimation.java | 16 +++-- .../react/animated/TrackingAnimatedNode.java | 43 +++++++++++++ .../facebook/react/bridge/JavaOnlyArray.java | 32 +++++++++- .../facebook/react/bridge/JavaOnlyMap.java | 34 ++++++++++- 7 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m index 7abf35e3eca6bd..00431e1e51d204 100644 --- a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m @@ -25,7 +25,7 @@ - (instancetype)initWithTag:(NSNumber *)tag _animationId = config[@"animationId"]; _nodeTag = config[@"toValue"]; _valueNodeTag = config[@"value"]; - _animationConfig = config[@"animationConfig"]; + _animationConfig = [NSMutableDictionary dictionaryWithDictionary:config[@"animationConfig"]]; } return self; } @@ -40,11 +40,11 @@ - (void)performUpdate { [super performUpdate]; - // clone animation config and update "toValue" to reflect updated value of the parent node + // change animation config's "toValue" to reflect updated value of the parent node RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_nodeTag]; - NSMutableDictionary *config = [NSMutableDictionary dictionaryWithDictionary:_animationConfig]; - [config setValue:@(node.value) forKey:@"toValue"]; + [_animationConfig setValue:@(node.value) forKey:@"toValue"]; + NSDictionary *config = _animationConfig; NSNumber *animationId = _animationId; NSNumber *valueNodeTag = _valueNodeTag; [self.manager schedulePostUpdateOperation:^(RCTNativeAnimatedNodesManager * _Nonnull manager) { 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/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index d65b20fc503794..5f5d24f64e3764 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -52,6 +52,14 @@ */ /*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener { + /** + * This interface can be used to enqueue callbacks that will be executed once all the updates for + * all modified nodes are completed in a current animation loop. + */ + public interface PostUpdateCallback { + void onPostUpdate(); + } + private final SparseArray mAnimatedNodes = new SparseArray<>(); private final SparseArray mActiveAnimations = new SparseArray<>(); private final SparseArray mUpdatedNodes = new SparseArray<>(); @@ -63,6 +71,7 @@ private int mAnimatedGraphBFSColor = 0; // Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`. private final List mRunUpdateNodeList = new LinkedList<>(); + private final List mPostUpdateQueue = new ArrayList<>(5); public NativeAnimatedNodesManager(UIManagerModule uiManager) { mUIImplementation = uiManager.getUIImplementation(); @@ -105,6 +114,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 +200,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 +234,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 +254,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; } @@ -417,6 +441,9 @@ private void handleEvent(Event event) { public void runUpdates(long frameTimeNanos) { UiThreadUtil.assertOnUiThread(); boolean hasFinishedAnimations = false; + // make sure post update is clean before we start updating in case something got enqueued in + // the meantime and not as a result of AnimatedNode#update call + mPostUpdateQueue.clear(); for (int i = 0; i < mUpdatedNodes.size(); i++) { AnimatedNode node = mUpdatedNodes.valueAt(i); @@ -439,15 +466,23 @@ public void runUpdates(long frameTimeNanos) { updateNodes(mRunUpdateNodeList); mRunUpdateNodeList.clear(); + // Run post update callbacks + for (int i = 0, size = mPostUpdateQueue.size(); i < size; i++) { + mPostUpdateQueue.get(i).onPostUpdate(); + } + mPostUpdateQueue.clear(); + // Cleanup finished animations. Iterate over the array of animations and override ones that has // finished, then resize `mActiveAnimations`. if (hasFinishedAnimations) { 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); } } @@ -562,4 +597,8 @@ private void updateNodes(List nodes) { + activeNodesCount + " but toposort visited only " + updatedNodesCount); } } + + public void enqueuePostUpdateCallback(PostUpdateCallback clb) { + mPostUpdateQueue.add(clb); + } } 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..91a5cc3d3c509d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java @@ -0,0 +1,43 @@ +/** + * 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 implements + NativeAnimatedNodesManager.PostUpdateCallback { + + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private final int mAnimationId; + private final int mParentNode; + private final int mAnimatingNode; + private final JavaOnlyMap mAnimationConfig; + + TrackingAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + mAnimationId = config.getInt("animationId"); + mParentNode = config.getInt("toValue"); + mAnimatingNode = config.getInt("value"); + mAnimationConfig = JavaOnlyMap.deepClone(config.getMap("animationConfig")); + } + + @Override + public void update() { + AnimatedNode animatedNode = mNativeAnimatedNodesManager.getNodeById(mParentNode); + mAnimationConfig.putDouble("toValue", ((ValueAnimatedNode) animatedNode).getValue()); + mNativeAnimatedNodesManager.enqueuePostUpdateCallback(this); + } + + @Override + public void onPostUpdate() { + mNativeAnimatedNodesManager.startAnimatingNode(mAnimationId, mAnimatingNode, 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 From e149b17f5ef4ac6a19f8e7815421457b9f446ddf Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 5 Feb 2018 16:33:46 +0100 Subject: [PATCH 04/12] Implement resetConfig for frames and decay native drivers on Android --- .../react/animated/DecayAnimation.java | 18 +++++++++++++----- .../animated/FrameBasedAnimationDriver.java | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) 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..d507e458e0f3bd 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 From 9a34873ac01755a9944b53aa5a57f70b035ca30a Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Tue, 6 Feb 2018 20:19:28 +0100 Subject: [PATCH 05/12] Post update callback is not necessary on Android because of the proper traversal of animated nodes graph --- .../animated/NativeAnimatedNodesManager.java | 22 ------------------- .../react/animated/TrackingAnimatedNode.java | 8 +------ 2 files changed, 1 insertion(+), 29 deletions(-) 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 5f5d24f64e3764..d54d24bd668ea9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -52,14 +52,6 @@ */ /*package*/ class NativeAnimatedNodesManager implements EventDispatcherListener { - /** - * This interface can be used to enqueue callbacks that will be executed once all the updates for - * all modified nodes are completed in a current animation loop. - */ - public interface PostUpdateCallback { - void onPostUpdate(); - } - private final SparseArray mAnimatedNodes = new SparseArray<>(); private final SparseArray mActiveAnimations = new SparseArray<>(); private final SparseArray mUpdatedNodes = new SparseArray<>(); @@ -71,7 +63,6 @@ public interface PostUpdateCallback { private int mAnimatedGraphBFSColor = 0; // Used to avoid allocating a new array on every frame in `runUpdates` and `onEventDispatch`. private final List mRunUpdateNodeList = new LinkedList<>(); - private final List mPostUpdateQueue = new ArrayList<>(5); public NativeAnimatedNodesManager(UIManagerModule uiManager) { mUIImplementation = uiManager.getUIImplementation(); @@ -441,9 +432,6 @@ private void handleEvent(Event event) { public void runUpdates(long frameTimeNanos) { UiThreadUtil.assertOnUiThread(); boolean hasFinishedAnimations = false; - // make sure post update is clean before we start updating in case something got enqueued in - // the meantime and not as a result of AnimatedNode#update call - mPostUpdateQueue.clear(); for (int i = 0; i < mUpdatedNodes.size(); i++) { AnimatedNode node = mUpdatedNodes.valueAt(i); @@ -466,12 +454,6 @@ public void runUpdates(long frameTimeNanos) { updateNodes(mRunUpdateNodeList); mRunUpdateNodeList.clear(); - // Run post update callbacks - for (int i = 0, size = mPostUpdateQueue.size(); i < size; i++) { - mPostUpdateQueue.get(i).onPostUpdate(); - } - mPostUpdateQueue.clear(); - // Cleanup finished animations. Iterate over the array of animations and override ones that has // finished, then resize `mActiveAnimations`. if (hasFinishedAnimations) { @@ -597,8 +579,4 @@ private void updateNodes(List nodes) { + activeNodesCount + " but toposort visited only " + updatedNodesCount); } } - - public void enqueuePostUpdateCallback(PostUpdateCallback clb) { - mPostUpdateQueue.add(clb); - } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java index 91a5cc3d3c509d..d5b9dba331e3a8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java @@ -12,8 +12,7 @@ import com.facebook.react.bridge.JavaOnlyMap; import com.facebook.react.bridge.ReadableMap; -/* package */ class TrackingAnimatedNode extends AnimatedNode implements - NativeAnimatedNodesManager.PostUpdateCallback { +/* package */ class TrackingAnimatedNode extends AnimatedNode { private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; private final int mAnimationId; @@ -33,11 +32,6 @@ public void update() { AnimatedNode animatedNode = mNativeAnimatedNodesManager.getNodeById(mParentNode); mAnimationConfig.putDouble("toValue", ((ValueAnimatedNode) animatedNode).getValue()); - mNativeAnimatedNodesManager.enqueuePostUpdateCallback(this); - } - - @Override - public void onPostUpdate() { mNativeAnimatedNodesManager.startAnimatingNode(mAnimationId, mAnimatingNode, mAnimationConfig, null); } } From 0590afaddcb244540ad5dbaf4faca04e63788f12 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 7 Feb 2018 11:17:53 +0100 Subject: [PATCH 06/12] Tests for animated tracking on Android --- .../animated/FrameBasedAnimationDriver.java | 4 +- .../NativeAnimatedNodeTraversalTest.java | 279 +++++++++++++++--- 2 files changed, 232 insertions(+), 51 deletions(-) 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 d507e458e0f3bd..3d37846fb0c6f0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java @@ -57,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) { @@ -68,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/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index 6f244c70be4baf..bc05cb8d42605e 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,231 @@ 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 25 + 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); + + ArgumentCaptor stylesCaptor = + ArgumentCaptor.forClass(ReactStylesDiffMap.class); + + // update "toValue" to 1, we expect tracking animation to animate now from 0 to 1 + mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 1d); + mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); + 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); + 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(4); + + // we verify that the value settled at 2 + assertThat(previousValue).isEqualTo(1.5d); + } } From 9df55098bd1e2e3f4fbd570d3d654bf53382ac41 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 7 Feb 2018 16:35:26 +0100 Subject: [PATCH 07/12] Tracking unit tests for iOS --- .../Drivers/RCTFrameAnimation.m | 3 +- .../Nodes/RCTTrackingAnimatedNode.m | 13 +- .../RCTNativeAnimatedNodesManager.h | 8 - .../RCTNativeAnimatedNodesManager.m | 11 - .../RCTNativeAnimatedNodesManagerTests.m | 223 ++++++++++++++++++ .../NativeAnimatedNodeTraversalTest.java | 9 +- 6 files changed, 232 insertions(+), 35 deletions(-) diff --git a/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m b/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m index d5da33bf26a372..53846b2d0a2204 100644 --- a/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m +++ b/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m @@ -63,7 +63,7 @@ - (void)resetAnimationConfig:(NSDictionary *)config _fromValue = _lastPosition; _toValue = toValue.floatValue; _frames = [frames copy]; - _animationStartTime = -1; + _animationStartTime = _animationCurrentTime = -1; _animationHasFinished = iterations.integerValue == 0; _iterations = iterations.integerValue; _currentLoop = 1; @@ -152,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/Nodes/RCTTrackingAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m index 00431e1e51d204..a2c8ac0e44e454 100644 --- a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m @@ -44,15 +44,10 @@ - (void)performUpdate RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_nodeTag]; [_animationConfig setValue:@(node.value) forKey:@"toValue"]; - NSDictionary *config = _animationConfig; - NSNumber *animationId = _animationId; - NSNumber *valueNodeTag = _valueNodeTag; - [self.manager schedulePostUpdateOperation:^(RCTNativeAnimatedNodesManager * _Nonnull manager) { - [manager startAnimatingNode:animationId - nodeTag:valueNodeTag - config:config - endCallback:nil]; - }]; + [self.manager startAnimatingNode:_animationId + nodeTag:_valueNodeTag + config:_animationConfig + endCallback:nil]; } @end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h index b7c4ad3effe041..1a0b684c10a271 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h @@ -13,10 +13,6 @@ #import #import -@class RCTNativeAnimatedNodesManager; - -typedef void (^AnimatedPostOperation)(RCTNativeAnimatedNodesManager * _Nonnull manager); - @interface RCTNativeAnimatedNodesManager : NSObject - (nonnull instancetype)initWithUIManager:(nonnull RCTUIManager *)uiManager; @@ -89,8 +85,4 @@ typedef void (^AnimatedPostOperation)(RCTNativeAnimatedNodesManager * _Nonnull m - (void)stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag; -// other - -- (void)schedulePostUpdateOperation:(nonnull AnimatedPostOperation)operartion; - @end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m index 88dc19557475df..ee82a8008c7cf6 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.m @@ -38,7 +38,6 @@ @implementation RCTNativeAnimatedNodesManager NSMutableDictionary *> *_eventDrivers; NSMutableSet> *_activeAnimations; CADisplayLink *_displayLink; - NSMutableArray *_postUpdateQueue; } - (instancetype)initWithUIManager:(nonnull RCTUIManager *)uiManager @@ -431,23 +430,13 @@ - (void)stepAnimations:(CADisplayLink *)displaylink #pragma mark -- Updates -- (void)schedulePostUpdateOperation:(AnimatedPostOperation)operartion -{ - [_postUpdateQueue addObject:operartion]; -} - - (void)updateAnimations { - _postUpdateQueue = [NSMutableArray new]; [_animationNodes enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, RCTAnimatedNode *node, BOOL *stop) { if (node.needsUpdate) { [node updateNodeIfNecessary]; } }]; - for (AnimatedPostOperation op in _postUpdateQueue) { - op(self); - } - _postUpdateQueue = nil; } @end diff --git a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m index 70678ac2854269..905972f58a37f9 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 *invocation) { + callCount++; + }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; + + for (NSNumber *frame in frames) { + [_nodesManager stepAnimations:_displayLink]; + } + [_nodesManager stepAnimations:_displayLink]; + XCTAssertEqual(callCount, 4); + + // the animation has completed, we expect no updates to be done + [[[_uiManager stub] andDo:^(NSInvocation *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 *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 (NSNumber *frame in frames) { + [_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/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java index bc05cb8d42605e..e1d1958613e6ee 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.java @@ -1142,7 +1142,7 @@ public void testTracking() { .isEqualTo(100d * (1d - frames.getDouble(i))); } - // at this point we expect tracking value to be 25 + // at this point we expect tracking value to be at 75 assertThat(((ValueAnimatedNode) mNativeAnimatedNodesManager.getNodeById(3)).getValue()) .isEqualTo(75d); @@ -1239,13 +1239,9 @@ public void testSpringTrackingRetainsSpeed() { createAnimatedGraphWithTrackingNode(1000, 0d, springConfig); - ArgumentCaptor stylesCaptor = - ArgumentCaptor.forClass(ReactStylesDiffMap.class); - // update "toValue" to 1, we expect tracking animation to animate now from 0 to 1 mNativeAnimatedNodesManager.setAnimatedNodeValue(1, 1d); mNativeAnimatedNodesManager.runUpdates(nextFrameTime()); - 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 @@ -1265,6 +1261,7 @@ public void testSpringTrackingRetainsSpeed() { // 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; @@ -1282,7 +1279,7 @@ public void testSpringTrackingRetainsSpeed() { previousValue = currentValue; } assertThat(hasTurnedForward).isEqualTo(true); - assertThat(bounceBackInitialFrames).isGreaterThan(4); + assertThat(bounceBackInitialFrames).isGreaterThan(3); // we verify that the value settled at 2 assertThat(previousValue).isEqualTo(1.5d); From bfc6b5820c487adc5a2ad2fbf9c0403d49f8a57f Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 7 Feb 2018 17:06:09 +0100 Subject: [PATCH 08/12] Restore NS_REQUIRES_SUPER removed by accident for updateNodeIfNecessary method --- Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h index bef5d3c05af4d2..a8cad4e939c82e 100644 --- a/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h +++ b/Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h @@ -33,7 +33,7 @@ /** * The node will update its value if necesarry and only after its parents have updated. */ -- (void)updateNodeIfNecessary; +- (void)updateNodeIfNecessary NS_REQUIRES_SUPER; /** * Where the actual update code lives. Called internally from updateNodeIfNecessary From c29611b8f11a838796217a6dd86c0d9c583f9ef5 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Wed, 7 Feb 2018 17:08:06 +0100 Subject: [PATCH 09/12] Remove clone of animated.spring section from RNTester example app --- RNTester/js/NativeAnimationsExample.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/RNTester/js/NativeAnimationsExample.js b/RNTester/js/NativeAnimationsExample.js index 928208b6f09172..4aaf2063de57a0 100644 --- a/RNTester/js/NativeAnimationsExample.js +++ b/RNTester/js/NativeAnimationsExample.js @@ -609,32 +609,6 @@ exports.examples = [ return ; }, }, - { - title: 'translateX => Animated.spring (bounciness/speed)', - render: function() { - return ( - - {anim => ( - - )} - - ); - }, - }, { title: 'Animated Tracking - tap me many times', render: function() { From 27f5d439c3e3120609f6190150e1ba5020c9157d Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Fri, 9 Feb 2018 10:45:32 +0100 Subject: [PATCH 10/12] Fix flow --- Libraries/Animated/src/nodes/AnimatedTracking.js | 1 + Libraries/Animated/src/nodes/AnimatedValue.js | 1 + 2 files changed, 2 insertions(+) diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js index 5b8038f2f8a2cf..0ec517d12bd560 100644 --- a/Libraries/Animated/src/nodes/AnimatedTracking.js +++ b/Libraries/Animated/src/nodes/AnimatedTracking.js @@ -27,6 +27,7 @@ class AnimatedTracking extends AnimatedNode { _callback: ?EndCallback; _animationConfig: Object; _animationClass: any; + _useNativeDriver: boolean; constructor( value: AnimatedValue, diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js index 52082a2f09536a..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; From a64dbf6bc23498eb7b341c63d435d72db3184892 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 12 Feb 2018 09:35:15 +0100 Subject: [PATCH 11/12] Responding to comments - renaming --- .../Nodes/RCTTrackingAnimatedNode.m | 8 ++++---- .../react/animated/TrackingAnimatedNode.java | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m index a2c8ac0e44e454..e77b773e040a12 100644 --- a/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m @@ -13,7 +13,7 @@ @implementation RCTTrackingAnimatedNode { NSNumber *_animationId; - NSNumber *_nodeTag; + NSNumber *_toValueNodeTag; NSNumber *_valueNodeTag; NSMutableDictionary *_animationConfig; } @@ -23,7 +23,7 @@ - (instancetype)initWithTag:(NSNumber *)tag { if ((self = [super initWithTag:tag config:config])) { _animationId = config[@"animationId"]; - _nodeTag = config[@"toValue"]; + _toValueNodeTag = config[@"toValue"]; _valueNodeTag = config[@"value"]; _animationConfig = [NSMutableDictionary dictionaryWithDictionary:config[@"animationConfig"]]; } @@ -41,8 +41,8 @@ - (void)performUpdate [super performUpdate]; // change animation config's "toValue" to reflect updated value of the parent node - RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_nodeTag]; - [_animationConfig setValue:@(node.value) forKey:@"toValue"]; + RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_toValueNodeTag]; + _animationConfig[@"toValue"] = @(node.value); [self.manager startAnimatingNode:_animationId nodeTag:_valueNodeTag diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java index d5b9dba331e3a8..db312d23558078 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/TrackingAnimatedNode.java @@ -16,22 +16,22 @@ private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; private final int mAnimationId; - private final int mParentNode; - private final int mAnimatingNode; + private final int mToValueNode; + private final int mValueNode; private final JavaOnlyMap mAnimationConfig; TrackingAnimatedNode(ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { mNativeAnimatedNodesManager = nativeAnimatedNodesManager; mAnimationId = config.getInt("animationId"); - mParentNode = config.getInt("toValue"); - mAnimatingNode = config.getInt("value"); + mToValueNode = config.getInt("toValue"); + mValueNode = config.getInt("value"); mAnimationConfig = JavaOnlyMap.deepClone(config.getMap("animationConfig")); } @Override public void update() { - AnimatedNode animatedNode = mNativeAnimatedNodesManager.getNodeById(mParentNode); - mAnimationConfig.putDouble("toValue", ((ValueAnimatedNode) animatedNode).getValue()); - mNativeAnimatedNodesManager.startAnimatingNode(mAnimationId, mAnimatingNode, mAnimationConfig, null); + AnimatedNode toValue = mNativeAnimatedNodesManager.getNodeById(mToValueNode); + mAnimationConfig.putDouble("toValue", ((ValueAnimatedNode) toValue).getValue()); + mNativeAnimatedNodesManager.startAnimatingNode(mAnimationId, mValueNode, mAnimationConfig, null); } } From e929b04624db6f6a24266216935e2f1f78461da6 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Thu, 15 Feb 2018 16:08:57 +0100 Subject: [PATCH 12/12] Fix build warning in RCTNativeAnimatedNodesManagerTests.m --- .../RCTNativeAnimatedNodesManagerTests.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m index 905972f58a37f9..358264899fb528 100644 --- a/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m +++ b/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManagerTests.m @@ -980,18 +980,18 @@ - (void)testTrackingPausesWhenEndValueIsReached [_nodesManager stepAnimations:_displayLink]; // kick off the tracking __block int callCount = 0; - [[[_uiManager stub] andDo:^(NSInvocation *invocation) { + [[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) { callCount++; }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; - for (NSNumber *frame in frames) { + 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 *invocation) { + [[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) { XCTFail("Expected not to be called"); }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; [_nodesManager stepAnimations:_displayLink]; @@ -999,7 +999,7 @@ - (void)testTrackingPausesWhenEndValueIsReached // restore rejected method, we will use it later on callCount = 0; - [[[_uiManager stub] andDo:^(NSInvocation *invocation) { + [[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) { callCount++; }] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY]; @@ -1007,7 +1007,7 @@ - (void)testTrackingPausesWhenEndValueIsReached [_nodesManager setAnimatedNodeValue:@1 value:@200]; [_nodesManager stepAnimations:_displayLink]; // kick off the tracking - for (NSNumber *frame in frames) { + for (NSUInteger i = 0; i < frames.count; i++) { [_nodesManager stepAnimations:_displayLink]; } [_nodesManager stepAnimations:_displayLink];