react-native/RNTester/RNTesterUnitTests/RCTNativeAnimatedNodesManag...

1090 lines
42 KiB
Mathematica
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import <RCTAnimation/RCTNativeAnimatedNodesManager.h>
#import <RCTAnimation/RCTValueAnimatedNode.h>
#import <React/RCTUIManager.h>
static const NSTimeInterval FRAME_LENGTH = 1.0 / 60.0;
@interface RCTFakeDisplayLink : CADisplayLink
@end
@implementation RCTFakeDisplayLink
{
NSTimeInterval _timestamp;
}
- (instancetype)init
{
self = [super init];
if (self) {
_timestamp = 1124.1234143251; // Random
}
return self;
}
- (NSTimeInterval)timestamp
{
_timestamp += FRAME_LENGTH;
return _timestamp;
}
@end
@interface RCTFakeValueObserver : NSObject<RCTValueAnimatedNodeObserver>
@property (nonatomic, strong) NSMutableArray<NSNumber *> *calls;
@end
@implementation RCTFakeValueObserver
- (instancetype)init
{
self = [super init];
if (self) {
_calls = [NSMutableArray new];
}
return self;
}
- (void)animatedNode:(__unused RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value
{
[_calls addObject:@(value)];
}
@end
@interface RCTFakeEvent : NSObject<RCTEvent>
@end
@implementation RCTFakeEvent
{
NSArray *_arguments;
}
@synthesize eventName = _eventName;
@synthesize viewTag = _viewTag;
@synthesize coalescingKey = _coalescingKey;
- (instancetype)initWithName:(NSString *)name viewTag:(NSNumber *)viewTag arguments:(NSArray *)arguments
{
self = [super init];
if (self) {
_eventName = name;
_viewTag = viewTag;
_arguments = arguments;
}
return self;
}
- (NSArray *)arguments
{
return _arguments;
}
RCT_NOT_IMPLEMENTED(+ (NSString *)moduleDotMethod);
RCT_NOT_IMPLEMENTED(- (BOOL)canCoalesce);
RCT_NOT_IMPLEMENTED(- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent);
@end
static id RCTPropChecker(NSString *prop, NSNumber *value)
{
return [OCMArg checkWithBlock:^BOOL(NSDictionary<NSString *, NSNumber *> *props) {
BOOL match = fabs(props[prop].doubleValue - value.doubleValue) < FLT_EPSILON;
if (!match) {
NSLog(@"Props `%@` with value `%@` is not close to `%@`", prop, props[prop], value);
}
return match;
}];
}
@interface RCTNativeAnimatedNodesManagerTests : XCTestCase
@end
@implementation RCTNativeAnimatedNodesManagerTests
{
id _uiManager;
RCTNativeAnimatedNodesManager *_nodesManager;
RCTFakeDisplayLink *_displayLink;
}
- (void)setUp
{
[super setUp];
_uiManager = [OCMockObject niceMockForClass:[RCTUIManager class]];
_nodesManager = [[RCTNativeAnimatedNodesManager alloc] initWithUIManager:_uiManager];
_displayLink = [RCTFakeDisplayLink new];
}
/**
* Generates a simple animated nodes graph and attaches the props node to a given viewTag
* Parameter opacity is used as a initial value for the "opacity" attribute.
*
* Nodes are connected as follows (nodes IDs in parens):
* ValueNode(1) -> StyleNode(2) -> PropNode(3)
*/
- (void)createSimpleAnimatedView:(NSNumber *)viewTag withOpacity:(CGFloat)opacity
{
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @(opacity), @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"style", @"style": @{@"opacity": @1}}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"props", @"props": @{@"style": @2}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodeToView:@3 viewTag:viewTag viewName:@"UIView"];
}
- (void)testFramesAnimation
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:nil];
for (NSNumber *frame in frames) {
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"opacity", frame)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @1)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testFramesAnimationLoop
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1, @"iterations": @5}
endCallback:nil];
for (NSUInteger it = 0; it < 5; it++) {
for (NSNumber *frame in frames) {
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"opacity", frame)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @1)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testNodeValueListenerIfNotListening
{
NSNumber *nodeId = @1;
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
RCTFakeValueObserver *observer = [RCTFakeValueObserver new];
[_nodesManager startListeningToAnimatedNodeValue:nodeId valueObserver:observer];
[_nodesManager startAnimatingNode:@1
nodeTag:nodeId
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:nil];
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 1UL);
XCTAssertEqualObjects(observer.calls[0], @0);
[_nodesManager stopListeningToAnimatedNodeValue:nodeId];
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 1UL);
}
- (void)testNodeValueListenerIfListening
{
NSNumber *nodeId = @1;
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
RCTFakeValueObserver *observer = [RCTFakeValueObserver new];
[_nodesManager startListeningToAnimatedNodeValue:nodeId valueObserver:observer];
[_nodesManager startAnimatingNode:@1
nodeTag:nodeId
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:nil];
for (NSUInteger i = 0; i < frames.count; i++) {
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, i + 1);
XCTAssertEqualWithAccuracy(observer.calls[i].doubleValue, frames[i].doubleValue, FLT_EPSILON);
}
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 7UL);
XCTAssertEqualObjects(observer.calls[6], @1);
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 7UL);
}
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
- (void)performSpringAnimationTestWithConfig:(NSDictionary*)config isCriticallyDamped:(BOOL)testForCriticallyDamped
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
config:config
endCallback:nil];
BOOL wasGreaterThanOne = NO;
CGFloat previousValue = 0;
__block CGFloat currentValue;
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
[invocation getArgument:&props atIndex:4];
currentValue = props[@"opacity"].doubleValue;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
// Run for 3 seconds.
for (NSUInteger i = 0; i < 3 * 60; i++) {
[_nodesManager stepAnimations:_displayLink];
if (currentValue > 1) {
wasGreaterThanOne = YES;
}
// Verify that animation step is relatively small.
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
XCTAssertLessThan(fabs(currentValue - previousValue), 0.12);
previousValue = currentValue;
}
// Verify that we've reach the final value at the end of animation.
XCTAssertEqual(previousValue, 1.0);
// Verify that value has reached some maximum value that is greater than the final value (bounce).
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
if (testForCriticallyDamped) {
XCTAssertFalse(wasGreaterThanOne);
} else {
XCTAssertTrue(wasGreaterThanOne);
}
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
- (void)testUnderdampedSpringAnimation
{
[self performSpringAnimationTestWithConfig:@{@"type": @"spring",
@"stiffness": @230.3,
@"damping": @22,
@"mass": @1,
@"initialVelocity": @0,
@"toValue": @1,
@"restSpeedThreshold": @0.001,
@"restDisplacementThreshold": @0.001,
@"overshootClamping": @NO}
isCriticallyDamped:NO];
}
- (void)testCritcallyDampedSpringAnimation
{
[self performSpringAnimationTestWithConfig:@{@"type": @"spring",
@"stiffness": @1000,
@"damping": @500,
@"mass": @3,
@"initialVelocity": @0,
@"toValue": @1,
@"restSpeedThreshold": @0.001,
@"restDisplacementThreshold": @0.001,
@"overshootClamping": @NO}
isCriticallyDamped:YES];
}
- (void)testDecayAnimation
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"decay",
@"velocity": @0.5,
@"deceleration": @0.998}
endCallback:nil];
__block CGFloat previousValue;
__block CGFloat currentValue;
CGFloat previousDiff = CGFLOAT_MAX;
[_nodesManager stepAnimations:_displayLink];
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
[invocation getArgument:&props atIndex:4];
currentValue = props[@"opacity"].doubleValue;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
// Run 3 secs of animation.
for (NSUInteger i = 0; i < 3 * 60; i++) {
[_nodesManager stepAnimations:_displayLink];
CGFloat currentDiff = currentValue - previousValue;
// Verify monotonicity.
// Greater *or equal* because the animation stops during these 3 seconds.
XCTAssertGreaterThanOrEqual(currentValue, previousValue);
// Verify decay.
XCTAssertLessThanOrEqual(currentDiff, previousDiff);
previousValue = currentValue;
previousDiff = currentDiff;
}
// Should be done in 3 secs.
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testDecayAnimationLoop
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"decay",
@"velocity": @0.5,
@"deceleration": @0.998,
@"iterations": @5}
endCallback:nil];
__block CGFloat previousValue;
__block CGFloat currentValue;
BOOL didComeToRest = NO;
NSUInteger numberOfResets = 0;
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
[invocation getArgument:&props atIndex:4];
currentValue = props[@"opacity"].doubleValue;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
// Run 3 secs of animation five times.
for (NSUInteger i = 0; i < 3 * 60 * 5; i++) {
[_nodesManager stepAnimations:_displayLink];
// Verify monotonicity when not resetting the animation.
// Greater *or equal* because the animation stops during these 3 seconds.
if (!didComeToRest) {
XCTAssertGreaterThanOrEqual(currentValue, previousValue);
}
if (didComeToRest && currentValue != previousValue) {
numberOfResets++;
didComeToRest = NO;
}
// Test if animation has come to rest using the 0.1 threshold from DecayAnimation.m.
didComeToRest = fabs(currentValue - previousValue) < 0.1;
previousValue = currentValue;
}
// The animation should have reset 4 times.
XCTAssertEqual(numberOfResets, 4u);
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testSpringAnimationLoop
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"spring",
@"iterations": @5,
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
@"stiffness": @230.2,
@"damping": @22,
@"mass": @1,
@"initialVelocity": @0,
@"toValue": @1,
@"restSpeedThreshold": @0.001,
@"restDisplacementThreshold": @0.001,
@"overshootClamping": @NO}
endCallback:nil];
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
BOOL didComeToRest = NO;
CGFloat previousValue = 0;
NSUInteger numberOfResets = 0;
__block CGFloat currentValue;
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
[invocation getArgument:&props atIndex:4];
currentValue = props[@"opacity"].doubleValue;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
// Run for 3 seconds five times.
for (NSUInteger i = 0; i < 3 * 60 * 5; i++) {
[_nodesManager stepAnimations:_displayLink];
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
if (!didComeToRest) {
// Verify that animation step is relatively small.
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
XCTAssertLessThan(fabs(currentValue - previousValue), 0.12);
}
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
// Test to see if it reset after coming to rest
if (didComeToRest && currentValue == 0) {
didComeToRest = NO;
numberOfResets++;
}
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
// Record that the animation did come to rest when it rests on toValue.
didComeToRest = fabs(currentValue - 1) < 0.001 && fabs(currentValue - previousValue) < 0.001;
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
previousValue = currentValue;
}
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
// Verify that value reset 4 times after finishing a full animation and is currently resting.
XCTAssertEqual(numberOfResets, 4u);
XCTAssertTrue(didComeToRest);
Add closed-form damped harmonic oscillator algorithm to Animated.spring Summary: As I was working on mimicking iOS animations for my ongoing work with `react-navigation`, one task I had was to match the "push from right" animation that is common in UINavigationController. I was able to grab the exact animation values for this animation with some LLDB magic, and found that the screen is animated using a `CASpringAnimation` with the parameters: - stiffness: 1000 - damping: 500 - mass: 3 After spending a considerable amount of time attempting to replicate the spring created with these values by CASpringAnimation by specifying values for tension and friction in the current `Animated.spring` implementation, I was unable to come up with mathematically equivalent values that could replicate the spring _exactly_. After doing some research, I ended up disassembling the QuartzCore framework, reading the assembly, and determined that Apple's implementation of `CASpringAnimation` does not use an integrated, numerical animation model as we do in Animated.spring, but instead solved for the closed form of the equations that govern damped harmonic oscillation (the differential equations themselves are [here](https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator), and a paper describing the math to arrive at the closed-form solution to the second-order ODE that describes the DHO is [here](http://planetmath.org/sites/default/files/texpdf/39745.pdf)). Though we can get the currently implemented RK4 integration close by tweaking some values, it is, the current model is at it's core, an approximation. It seemed that if I wanted to implement the `CASpringAnimation` behavior _exactly_, I needed to implement the analytical model (as is implemented in `CASpringAnimation`) in `Animated`. We add three new optional parameters to `Animated.spring` (to both the JS and native implementations): - `stiffness`, a value describing the spring's stiffness coefficient - `damping`, a value defining how the spring's motion should be damped due to the forces of friction (technically called the _viscous damping coefficient_). - `mass`, a value describing the mass of the object attached to the end of the simulated spring Just like if a developer were to specify `bounciness`/`speed` and `tension`/`friction` in the same config, specifying any of these new parameters while also specifying the aforementioned config values will cause an error to be thrown. ~Defaults for `Animated.spring` across all three implementations (JS/iOS/Android) stay the same, so this is intended to be *a non-breaking change*.~ ~If `stiffness`, `damping`, or `mass` are provided in the config, we switch to animating the spring with the new damped harmonic oscillator model (`DHO` as described in the code).~ We replace the old RK4 integration implementation with our new analytic implementation. Tension/friction nicely correspond directly to stiffness/damping with the mass of the spring locked at 1. This is intended to be *a non-breaking change*, but there may be very slight differences in people's springs (maybe not even noticeable to the naked eye), given the fact that this implementation is more accurate. The DHO animation algorithm will calculate the _position_ of the spring at time _t_ explicitly and in an analytical fashion, and use this calculation to update the animation's value. It will also analytically calculate the velocity at time _t_, so as to allow animated value tracking to continue to work as expected. Also, docs have been updated to cover the new configuration options (and also I added docs for Animated configuration options that were missing, such as `restDisplacementThreshold`, etc). Run tests. Run "Animated Gratuitous App" and "NativeAnimation" example in RNTester. Closes https://github.com/facebook/react-native/pull/15322 Differential Revision: D5794791 Pulled By: hramos fbshipit-source-id: 58ed9e134a097e321c85c417a142576f6a8952f8
2017-09-21 06:36:11 +00:00
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testAnimationCallbackFinish
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @1];
__block NSInteger endCallbackCalls = 0;
RCTResponseSenderBlock endCallback = ^(NSArray *response) {
endCallbackCalls++;
XCTAssertEqualObjects(response, @[@{@"finished": @YES}]);
};
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:endCallback];
[_nodesManager stepAnimations:_displayLink];
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(endCallbackCalls, 0);
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(endCallbackCalls, 1);
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(endCallbackCalls, 1);
}
/**
* Creates a following graph of nodes:
* Value(1, firstValue) ----> Add(3) ---> Style(4) ---> Props(5) ---> View(viewTag)
* |
* Value(2, secondValue) --+
*
* Add(3) node maps to a "translateX" attribute of the Style(4) node.
*/
- (void)createAnimatedGraphWithAdditionNode:(NSNumber *)viewTag
firstValue:(CGFloat)firstValue
secondValue:(CGFloat)secondValue
{
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @(firstValue), @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"value", @"value": @(secondValue), @"offset": @0}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"addition", @"input": @[@1, @2]}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
[_nodesManager createAnimatedNode:@5
config:@{@"type": @"props", @"props": @{@"style": @4}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@3];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
}
- (void)testAdditionNode
{
NSNumber *viewTag = @50;
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
NSArray<NSNumber *> *frames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @101}
endCallback:nil];
[_nodesManager startAnimatingNode:@2
nodeTag:@2
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1010}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1100)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1111)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1111)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
/**
* Verifies that views are updated properly when one of the addition input nodes has started animating
* while the other one has not.
*
* We expect that the output of the addition node will take the starting value of the second input
* node even though the node hasn't been connected to an active animation driver.
*/
- (void)testViewReceiveUpdatesIfOneOfAnimationHasntStarted
{
NSNumber *viewTag = @50;
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
NSArray<NSNumber *> *frames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @101}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1100)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1101)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1101)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
/**
* Verifies that views are updated properly when one of the addition input nodes animation finishes
* before the other.
*
* We expect that the output of the addition node after one of the animation has finished will
* take the last value of the animated node and the view will receive updates up until the second
* animation is over.
*/
- (void)testViewReceiveUpdatesWhenOneOfAnimationHasFinished
{
NSNumber *viewTag = @50;
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
NSArray<NSNumber *> *firstFrames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": firstFrames, @"toValue": @200}
endCallback:nil];
NSArray<NSNumber *> *secondFrames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@2
nodeTag:@2
config:@{@"type": @"frames", @"frames": secondFrames, @"toValue": @1010}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1100)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
for (NSUInteger i = 1; i < secondFrames.count; i++) {
CGFloat expected = 1200.0 + secondFrames[i].doubleValue * 10.0;
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @(expected))];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1210)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testMultiplicationNode
{
NSNumber *viewTag = @50;
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @1, @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"value", @"value": @5, @"offset": @0}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"multiplication", @"input": @[@1, @2]}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
[_nodesManager createAnimatedNode:@5
config:@{@"type": @"props", @"props": @{@"style": @4}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@3];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
NSArray<NSNumber *> *frames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @2}
endCallback:nil];
[_nodesManager startAnimatingNode:@2
nodeTag:@2
config:@{@"type": @"frames", @"frames": frames, @"toValue": @10}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @5)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @20)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @20)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testHandleStoppingAnimation
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
__block BOOL endCallbackCalled = NO;
RCTResponseSenderBlock endCallback = ^(NSArray *response) {
endCallbackCalled = YES;
XCTAssertEqualObjects(response, @[@{@"finished": @NO}]);
};
[_nodesManager startAnimatingNode:@404
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:endCallback];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[_nodesManager stopAnimation:@404];
XCTAssertEqual(endCallbackCalled, YES);
// Run "update" loop a few more times -> we expect no further updates nor callback calls to be
// triggered
for (NSUInteger i = 0; i < 5; i++) {
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
}
- (void)testInterpolationNode
{
NSNumber *viewTag = @50;
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @10, @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"interpolation",
@"inputRange": @[@10, @20],
@"outputRange": @[@0, @1],
@"extrapolateLeft": @"extend",
@"extrapolateRight": @"extend"}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"style", @"style": @{@"opacity": @2}}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"props", @"props": @{@"style": @3}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodeToView:@4 viewTag:viewTag viewName:@"UIView"];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @20}
endCallback:nil];
for (NSNumber *frame in frames) {
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"opacity", frame)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @1)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (id<RCTEvent>)createScrollEventWithTag:(NSNumber *)viewTag value:(CGFloat)value
{
// The event value is the 3rd argument.
NSArray *arguments = @[@1, @1, @{@"contentOffset": @{@"y": @(value)}}];
return [[RCTFakeEvent alloc] initWithName:@"topScroll"
viewTag:viewTag
arguments:arguments];
}
- (void)testNativeAnimatedEventDoUpdate
{
NSNumber *viewTag = @1000;
[self createSimpleAnimatedView:viewTag withOpacity:0];
[_nodesManager addAnimatedEventToView:viewTag
eventName:@"topScroll"
eventMapping:@{@"animatedValueTag": @1,
@"nativeEventPath": @[@"contentOffset", @"y"]}];
// Make sure that the update actually happened synchronously in `handleAnimatedEvent` and does
// not wait for the next animation loop.
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @10)];
[_nodesManager handleAnimatedEvent:[self createScrollEventWithTag:viewTag value:10]];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testNativeAnimatedEventDoNotUpdate
{
NSNumber *viewTag = @1000;
[self createSimpleAnimatedView:viewTag withOpacity:0];
[_nodesManager addAnimatedEventToView:viewTag
eventName:@"otherEvent"
eventMapping:@{@"animatedValueTag": @1,
@"nativeEventPath": @[@"contentOffset", @"y"]}];
[_nodesManager addAnimatedEventToView:@999
eventName:@"topScroll"
eventMapping:@{@"animatedValueTag": @1,
@"nativeEventPath": @[@"contentOffset", @"y"]}];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager handleAnimatedEvent:[self createScrollEventWithTag:viewTag value:10]];
[_uiManager verify];
}
Support for animated tracking in native driver 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 https://github.com/facebook/react-native/pull/17896 Differential Revision: D6974170 Pulled By: hramos fbshipit-source-id: 50e918b36ee10f80c1deb866c955661d4cc2619b
2018-02-16 19:43:34 +00:00
/**
* Creates a following graph of nodes:
* Value(3, initialValue) ----> Style(4) ---> Props(5) ---> View(viewTag)
*
* Value(3) is set to track Value(1) via Tracking(2) node with the provided animation config
*/
- (void)createAnimatedGraphWithTrackingNode:(NSNumber *)viewTag
initialValue:(CGFloat)initialValue
animationConfig:(NSDictionary *)animationConfig
{
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @(initialValue), @"offset": @0}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"value", @"value": @(initialValue), @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"tracking",
@"animationId": @70,
@"value": @3,
@"toValue": @1,
@"animationConfig": animationConfig}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
[_nodesManager createAnimatedNode:@5
config:@{@"type": @"props", @"props": @{@"style": @4}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
}
/**
* In this test we verify that when value is being tracked we can update destination value in the
* middle of ongoing animation and the animation will update and animate to the new spot. This is
* tested using simple 5 frame backed timing animation.
*/
- (void)testTracking
{
NSArray *frames = @[@0, @0.25, @0.5, @0.75, @1];
NSDictionary *animationConfig = @{@"type": @"frames", @"frames": frames};
[self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:animationConfig];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"translateX", 0)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
// update "toValue" to 100, we expect tracking animation to animate now from 0 to 100 in 5 steps
[_nodesManager setAnimatedNodeValue:@1 value:@100];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
for (NSNumber *frame in frames) {
NSNumber *expected = @([frame doubleValue] * 100);
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"translateX", expected)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
// update "toValue" to 0 but run only two frames from the animation,
// we expect tracking animation to animate now from 100 to 75
[_nodesManager setAnimatedNodeValue:@1 value:@0];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
for (int i = 0; i < 2; i++) {
NSNumber *expected = @(100. * (1. - [frames[i] doubleValue]));
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"translateX", expected)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
// at this point we expect tracking value to be at 75
// we update "toValue" again to 100 and expect the animation to restart from the current place
[_nodesManager setAnimatedNodeValue:@1 value:@100];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
for (NSNumber *frame in frames) {
NSNumber *expected = @(50. + 50. * [frame doubleValue]);
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"translateX", expected)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[_nodesManager stepAnimations:_displayLink];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
/**
* In this test we verify that when tracking is set up for a given animated node and when the
* animation settles it will not be registered as an active animation and therefore will not
* consume resources on running the animation that has already completed. Then we verify that when
* the value updates the animation will resume as expected and the complete again when reaches the
* end.
*/
- (void)testTrackingPausesWhenEndValueIsReached
{
NSArray *frames = @[@0, @0.5, @1];
NSDictionary *animationConfig = @{@"type": @"frames", @"frames": frames};
[self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:animationConfig];
[_nodesManager setAnimatedNodeValue:@1 value:@100];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
__block int callCount = 0;
[[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) {
callCount++;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
for (NSUInteger i = 0; i < frames.count; i++) {
[_nodesManager stepAnimations:_displayLink];
}
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(callCount, 4);
// the animation has completed, we expect no updates to be done
[[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) {
XCTFail("Expected not to be called");
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
// restore rejected method, we will use it later on
callCount = 0;
[[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) {
callCount++;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
// we update end value and expect the animation to restart
[_nodesManager setAnimatedNodeValue:@1 value:@200];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
for (NSUInteger i = 0; i < frames.count; i++) {
[_nodesManager stepAnimations:_displayLink];
}
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(callCount, 4);
// the animation has completed, we expect no updates to be done
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
/**
* In this test we verify that when tracking is configured to use spring animation and when the
* destination value updates the current speed of the animated value will be taken into account
* while updating the spring animation and it will smoothly transition to the new end value.
*/
- (void) testSpringTrackingRetainsSpeed
{
// this spring config correspomds to tension 20 and friction 0.5 which makes the spring settle
// very slowly
NSDictionary *springConfig = @{@"type": @"spring",
@"restSpeedThreshold": @0.001,
@"mass": @1,
@"restDisplacementThreshold": @0.001,
@"initialVelocity": @0.5,
@"damping": @2.5,
@"stiffness": @157.8,
@"overshootClamping": @NO};
[self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:springConfig];
__block CGFloat lastTranslateX = 0;
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary *props = nil;
[invocation getArgument:&props atIndex:4];
lastTranslateX = [props[@"translateX"] doubleValue];
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
// update "toValue" to 1, we expect tracking animation to animate now from 0 to 1
[_nodesManager setAnimatedNodeValue:@1 value:@1];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
// we run several steps of animation until the value starts bouncing, has negative speed and
// passes the final point (that is 1) while going backwards
BOOL isBoucingBack = NO;
CGFloat previousValue = 0;
for (int maxFrames = 500; maxFrames > 0; maxFrames--) {
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
if (previousValue >= 1. && lastTranslateX < 1.) {
isBoucingBack = YES;
break;
}
previousValue = lastTranslateX;
}
XCTAssert(isBoucingBack);
// we now update "toValue" to 1.5 but since the value have negative speed and has also pretty
// low friction we expect it to keep going in the opposite direction for a few more frames
[_nodesManager setAnimatedNodeValue:@1 value:@1.5];
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
int bounceBackInitialFrames = 0;
BOOL hasTurnedForward = NO;
// we run 8 seconds of animation
for (int i = 0; i < 8 * 60; i++) {
[_nodesManager stepAnimations:_displayLink];
if (!hasTurnedForward) {
if (lastTranslateX <= previousValue) {
bounceBackInitialFrames++;
} else {
hasTurnedForward = true;
}
}
previousValue = lastTranslateX;
}
XCTAssert(hasTurnedForward);
XCTAssertGreaterThan(bounceBackInitialFrames, 3);
XCTAssertEqual(lastTranslateX, 1.5);
}
@end