Skip to content

Commit

Permalink
Support for animated tracking in native driver
Browse files Browse the repository at this point in the history
Summary:
This PR adds support for Animated tracking to Animated Native Driver implementation on Android and iOS.

Animated tracking allows for animation to be started with a "dynamic" end value. Instead of passing a fixed number as end value we can pass a reference to another Animated.Value. Then when that value changes, the animation will be reconfigured to drive the animation to the new destination point. What is important is that animation will keep its state in the process of updating "toValue". That is if it is a spring animation and the end value changes while the previous animation still hasn't settled the new animation will start from the current position and will inherit current velocity. This makes end value transitions very smooth.

Animated tracking is available in JS implementation of Animated library but not in the native implementation. Therefore until now, it wasn't possible to utilize native driver when using animated tracking. Offloading animation from JS thread turns out to be crucial for gesture driven animations. This PR is a step forward towards feature parity between JS and native implementations of Animated.

Here is a link to example video that shows how tracking can be used to implement chat heads effect: https://twitter.com/kzzzf/status/958362032650244101

In addition this PR fixes an issue with frames animation driver on Android that because of rounding issues was taking one extra frame to start. Because of that change I had to update a number of Android unit tests that were relying on that behavior and running that one additional animation step prior to performing checks.

As a part of this PR I'm adding three unit tests for each of the platforms that verifies most important aspects of this implementation. Please refer to the code and look at the test cases top level comments to learn what they do.

I'm also adding a section to "Native Animated Example" screen in RNTester app that provides a test case for tracking. In the example we have blue square that fallows the red line drawn on screen. Line uses Animated.Value for it's position while square is connected via tracking spring animation to that value. So it is ought to follow the line. When user taps in the area surrounding the button new position for the red line is selected at random and the value updates. Then we can watch blue screen animate to that position.

You can also refer to this video that I use to demonstrate how tracking can be linked with native gesture events using react-native-gesture-handler lib: https://twitter.com/kzzzf/status/958362032650244101

[GENERAL][FEATURE][Native Animated] - Added support for animated tracking to native driver. Now you can use `useNativeDriver` flag with animations that track other Animated.Values
Closes facebook#17896

Differential Revision: D6974170

Pulled By: hramos

fbshipit-source-id: 50e918b36ee10f80c1deb866c955661d4cc2619b
  • Loading branch information
kmagiera authored and Plo4ox committed Feb 17, 2018
1 parent ef3d0ce commit 8320587
Show file tree
Hide file tree
Showing 22 changed files with 899 additions and 121 deletions.
37 changes: 37 additions & 0 deletions Libraries/Animated/src/nodes/AnimatedTracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

const AnimatedValue = require('./AnimatedValue');
const AnimatedNode = require('./AnimatedNode');
const {
generateNewAnimationId,
shouldUseNativeDriver,
} = require('../NativeAnimatedHelper');

import type {EndCallback} from '../animations/Animation';

Expand All @@ -23,6 +27,7 @@ class AnimatedTracking extends AnimatedNode {
_callback: ?EndCallback;
_animationConfig: Object;
_animationClass: any;
_useNativeDriver: boolean;

constructor(
value: AnimatedValue,
Expand All @@ -36,16 +41,32 @@ class AnimatedTracking extends AnimatedNode {
this._parent = parent;
this._animationClass = animationClass;
this._animationConfig = animationConfig;
this._useNativeDriver = shouldUseNativeDriver(animationConfig);
this._callback = callback;
this.__attach();
}

__makeNative() {
this.__isNative = true;
this._parent.__makeNative();
super.__makeNative();
this._value.__makeNative();
}

__getValue(): Object {
return this._parent.__getValue();
}

__attach(): void {
this._parent.__addChild(this);
if (this._useNativeDriver) {
// when the tracking starts we need to convert this node to a "native node"
// so that the parent node will be made "native" too. This is necessary as
// if we don't do this `update` method will get called. At that point it
// may be too late as it would mean the JS driver has already started
// updating node values
this.__makeNative();
}
}

__detach(): void {
Expand All @@ -62,6 +83,22 @@ class AnimatedTracking extends AnimatedNode {
this._callback,
);
}

__getNativeConfig(): any {
const animation = new this._animationClass({
...this._animationConfig,
// remove toValue from the config as it's a ref to Animated.Value
toValue: undefined,
});
const animationConfig = animation.__getNativeAnimationConfig();
return {
type: 'tracking',
animationId: generateNewAnimationId(),
animationConfig,
toValue: this._parent.__getNativeTag(),
value: this._value.__getNativeTag(),
};
}
}

module.exports = AnimatedTracking;
5 changes: 3 additions & 2 deletions Libraries/Animated/src/nodes/AnimatedValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -76,7 +77,7 @@ class AnimatedValue extends AnimatedWithChildren {
_startingValue: number;
_offset: number;
_animation: ?Animation;
_tracking: ?AnimatedNode;
_tracking: ?AnimatedTracking;
_listeners: {[key: string]: ValueListenerCallback};
__nativeAnimatedValueListener: ?any;

Expand Down Expand Up @@ -311,7 +312,7 @@ class AnimatedValue extends AnimatedWithChildren {
/**
* Typically only used internally.
*/
track(tracking: AnimatedNode): void {
track(tracking: AnimatedTracking): void {
this.stopTracking();
this._tracking = tracking;
}
Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeAnimation/Drivers/RCTAnimationDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)startAnimation;
- (void)stepAnimationWithTime:(NSTimeInterval)currentTime;
- (void)stopAnimation;
- (void)resetAnimationConfig:(NSDictionary *)config;

NS_ASSUME_NONNULL_END

Expand Down
23 changes: 14 additions & 9 deletions Libraries/NativeAnimation/Drivers/RCTDecayAnimation.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 19 additions & 10 deletions Libraries/NativeAnimation/Drivers/RCTFrameAnimation.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ @implementation RCTFrameAnimation
NSArray<NSNumber *> *_frames;
CGFloat _toValue;
CGFloat _fromValue;
CGFloat _lastPosition;
NSTimeInterval _animationStartTime;
NSTimeInterval _animationCurrentTime;
RCTResponseSenderBlock _callback;
Expand All @@ -44,23 +45,30 @@ - (instancetype)initWithId:(NSNumber *)animationId
callBack:(nullable RCTResponseSenderBlock)callback;
{
if ((self = [super init])) {
NSNumber *toValue = [RCTConvert NSNumber:config[@"toValue"]] ?: @1;
NSArray<NSNumber *> *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<NSNumber *> *frames = [RCTConvert NSNumberArray:config[@"frames"]];
NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1;

_fromValue = _lastPosition;
_toValue = toValue.floatValue;
_frames = [frames copy];
_animationStartTime = _animationCurrentTime = -1;
_animationHasFinished = iterations.integerValue == 0;
_iterations = iterations.integerValue;
_currentLoop = 1;
}

RCT_NOT_IMPLEMENTED(- (instancetype)init)

- (void)startAnimation
Expand Down Expand Up @@ -144,6 +152,7 @@ - (void)updateOutputWithFrameOutput:(CGFloat)frameOutput
EXTRAPOLATE_TYPE_EXTEND,
EXTRAPOLATE_TYPE_EXTEND);

_lastPosition = outputValue;
_valueNode.value = outputValue;
[_valueNode setNeedsUpdate];
}
Expand Down
44 changes: 24 additions & 20 deletions Libraries/NativeAnimation/Drivers/RCTSpringAnimation.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Libraries/NativeAnimation/Nodes/RCTAnimatedNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

#import <Foundation/Foundation.h>

@class RCTNativeAnimatedNodesManager;

@interface RCTAnimatedNode : NSObject

- (instancetype)initWithTag:(NSNumber *)tag
config:(NSDictionary<NSString *, id> *)config NS_DESIGNATED_INITIALIZER;

@property (nonatomic, readonly) NSNumber *nodeTag;
@property (nonatomic, weak) RCTNativeAnimatedNodesManager *manager;
@property (nonatomic, copy, readonly) NSDictionary<NSString *, id> *config;

@property (nonatomic, copy, readonly) NSMapTable<NSNumber *, RCTAnimatedNode *> *childNodes;
Expand Down
15 changes: 15 additions & 0 deletions Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import "RCTAnimatedNode.h"


@interface RCTTrackingAnimatedNode : RCTAnimatedNode

@end
54 changes: 54 additions & 0 deletions Libraries/NativeAnimation/Nodes/RCTTrackingAnimatedNode.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import "RCTTrackingAnimatedNode.h"
#import "RCTValueAnimatedNode.h"
#import "RCTNativeAnimatedNodesManager.h"

@implementation RCTTrackingAnimatedNode {
NSNumber *_animationId;
NSNumber *_toValueNodeTag;
NSNumber *_valueNodeTag;
NSMutableDictionary *_animationConfig;
}

- (instancetype)initWithTag:(NSNumber *)tag
config:(NSDictionary<NSString *, id> *)config
{
if ((self = [super initWithTag:tag config:config])) {
_animationId = config[@"animationId"];
_toValueNodeTag = config[@"toValue"];
_valueNodeTag = config[@"value"];
_animationConfig = [NSMutableDictionary dictionaryWithDictionary:config[@"animationConfig"]];
}
return self;
}

- (void)onDetachedFromNode:(RCTAnimatedNode *)parent
{
[self.manager stopAnimation:_animationId];
[super onDetachedFromNode:parent];
}

- (void)performUpdate
{
[super performUpdate];

// change animation config's "toValue" to reflect updated value of the parent node
RCTValueAnimatedNode *node = (RCTValueAnimatedNode *)[self.parentNodes objectForKey:_toValueNodeTag];
_animationConfig[@"toValue"] = @(node.value);

[self.manager startAnimatingNode:_animationId
nodeTag:_valueNodeTag
config:_animationConfig
endCallback:nil];
}

@end

Loading

0 comments on commit 8320587

Please sign in to comment.