2017-03-28 16:08:51 +00:00
|
|
|
/**
|
2017-05-06 03:50:47 +00:00
|
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
2017-03-28 16:08:51 +00:00
|
|
|
*
|
2018-02-17 02:24:55 +00:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2017-03-28 16:08:51 +00:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
#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];
|
|
|
|
}
|
|
|
|
|
2017-05-26 10:24:29 +00:00
|
|
|
- (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];
|
|
|
|
}
|
|
|
|
|
2017-03-28 16:08:51 +00:00
|
|
|
- (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
|
2017-03-28 16:08:51 +00:00
|
|
|
{
|
|
|
|
[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
|
2017-03-28 16:08:51 +00:00
|
|
|
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);
|
2017-03-28 16:08:51 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2017-03-28 16:08:51 +00:00
|
|
|
|
|
|
|
[[_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];
|
|
|
|
}
|
|
|
|
|
2017-04-24 11:27:37 +00:00
|
|
|
- (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];
|
|
|
|
}
|
|
|
|
|
2017-05-26 10:24:29 +00:00
|
|
|
- (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,
|
2017-05-26 10:24:29 +00:00
|
|
|
@"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
|
|
|
|
2017-05-26 10:24:29 +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
|
|
|
|
2017-05-26 10:24:29 +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
|
|
|
|
2017-05-26 10:24:29 +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);
|
2017-05-26 10:24:29 +00:00
|
|
|
}
|
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
|
|
|
|
2017-05-26 10:24:29 +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
|
|
|
|
2017-05-26 10:24:29 +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
|
|
|
|
2017-05-26 10:24:29 +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
|
|
|
|
2017-05-26 10:24:29 +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
|
|
|
|
2017-05-26 10:24:29 +00:00
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
|
|
[_uiManager verify];
|
|
|
|
}
|
|
|
|
|
2017-03-28 16:08:51 +00:00
|
|
|
- (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];
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-03-28 16:08:51 +00:00
|
|
|
@end
|