mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-06-20 04:48:18 +08:00
Summary: Not super clean, but not terrible. Unfortunately this still relies on the old Paper UIManager calling delegate methods to flush the operations queues. This will work for Marketplace You since Paper will be active, but we need to fix this, along with Animated Events which don't work at all yet. Random aside: it seems like taps are less responsive in fabric vs. paper, at least on iOS. There is a sporadic delay between the touches event coming in nativly to the JS callback invoking the native module function to start the animation - this will need some debugging. Reviewed By: shergin Differential Revision: D14143331 fbshipit-source-id: 63a17eaafa1217d77a532a2716d9f886a96fae59
1092 lines
42 KiB
Objective-C
1092 lines
42 KiB
Objective-C
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
#import <XCTest/XCTest.h>
|
|
|
|
#import <OCMock/OCMock.h>
|
|
|
|
#import <RCTAnimation/RCTNativeAnimatedNodesManager.h>
|
|
#import <RCTAnimation/RCTValueAnimatedNode.h>
|
|
#import <React/RCTUIManager.h>
|
|
|
|
static const NSTimeInterval FRAME_LENGTH = 1.0 / 60.0;
|
|
|
|
@interface RCTFakeDisplayLink : CADisplayLink
|
|
|
|
@end
|
|
|
|
@implementation RCTFakeDisplayLink
|
|
{
|
|
NSTimeInterval _timestamp;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_timestamp = 1124.1234143251; // Random
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSTimeInterval)timestamp
|
|
{
|
|
_timestamp += FRAME_LENGTH;
|
|
return _timestamp;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTFakeValueObserver : NSObject<RCTValueAnimatedNodeObserver>
|
|
|
|
@property (nonatomic, strong) NSMutableArray<NSNumber *> *calls;
|
|
|
|
@end
|
|
|
|
@implementation RCTFakeValueObserver
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_calls = [NSMutableArray new];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)animatedNode:(__unused RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value
|
|
{
|
|
[_calls addObject:@(value)];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTFakeEvent : NSObject<RCTEvent>
|
|
|
|
@end
|
|
|
|
@implementation RCTFakeEvent
|
|
{
|
|
NSArray *_arguments;
|
|
}
|
|
|
|
@synthesize eventName = _eventName;
|
|
@synthesize viewTag = _viewTag;
|
|
@synthesize coalescingKey = _coalescingKey;
|
|
|
|
- (instancetype)initWithName:(NSString *)name viewTag:(NSNumber *)viewTag arguments:(NSArray *)arguments
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_eventName = name;
|
|
_viewTag = viewTag;
|
|
_arguments = arguments;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSArray *)arguments
|
|
{
|
|
return _arguments;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(+ (NSString *)moduleDotMethod);
|
|
RCT_NOT_IMPLEMENTED(- (BOOL)canCoalesce);
|
|
RCT_NOT_IMPLEMENTED(- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent);
|
|
|
|
@end
|
|
|
|
static id RCTPropChecker(NSString *prop, NSNumber *value)
|
|
{
|
|
return [OCMArg checkWithBlock:^BOOL(NSDictionary<NSString *, NSNumber *> *props) {
|
|
BOOL match = fabs(props[prop].doubleValue - value.doubleValue) < FLT_EPSILON;
|
|
if (!match) {
|
|
NSLog(@"Props `%@` with value `%@` is not close to `%@`", prop, props[prop], value);
|
|
}
|
|
return match;
|
|
}];
|
|
}
|
|
|
|
@interface RCTNativeAnimatedNodesManagerTests : XCTestCase
|
|
|
|
@end
|
|
|
|
@implementation RCTNativeAnimatedNodesManagerTests
|
|
{
|
|
id _uiManager;
|
|
RCTNativeAnimatedNodesManager *_nodesManager;
|
|
RCTFakeDisplayLink *_displayLink;
|
|
}
|
|
|
|
- (void)setUp
|
|
{
|
|
[super setUp];
|
|
|
|
RCTBridge *bridge = [OCMockObject niceMockForClass:[RCTBridge class]];
|
|
_uiManager = [OCMockObject niceMockForClass:[RCTUIManager class]];
|
|
OCMStub([bridge uiManager]).andReturn(_uiManager);
|
|
_nodesManager = [[RCTNativeAnimatedNodesManager alloc] initWithBridge:bridge];
|
|
_displayLink = [RCTFakeDisplayLink new];
|
|
}
|
|
|
|
/**
|
|
* Generates a simple animated nodes graph and attaches the props node to a given viewTag
|
|
* Parameter opacity is used as a initial value for the "opacity" attribute.
|
|
*
|
|
* Nodes are connected as follows (nodes IDs in parens):
|
|
* ValueNode(1) -> StyleNode(2) -> PropNode(3)
|
|
*/
|
|
- (void)createSimpleAnimatedView:(NSNumber *)viewTag withOpacity:(CGFloat)opacity
|
|
{
|
|
[_nodesManager createAnimatedNode:@1
|
|
config:@{@"type": @"value", @"value": @(opacity), @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@2
|
|
config:@{@"type": @"style", @"style": @{@"opacity": @1}}];
|
|
[_nodesManager createAnimatedNode:@3
|
|
config:@{@"type": @"props", @"props": @{@"style": @2}}];
|
|
|
|
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
|
|
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
|
|
[_nodesManager connectAnimatedNodeToView:@3 viewTag:viewTag viewName:@"UIView"];
|
|
}
|
|
|
|
- (void)testFramesAnimation
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
|
|
endCallback:nil];
|
|
|
|
for (NSNumber *frame in frames) {
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", frame)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", @1)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testFramesAnimationLoop
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1, @"iterations": @5}
|
|
endCallback:nil];
|
|
|
|
for (NSUInteger it = 0; it < 5; it++) {
|
|
for (NSNumber *frame in frames) {
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", frame)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
}
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", @1)];
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testNodeValueListenerIfNotListening
|
|
{
|
|
NSNumber *nodeId = @1;
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
|
|
RCTFakeValueObserver *observer = [RCTFakeValueObserver new];
|
|
[_nodesManager startListeningToAnimatedNodeValue:nodeId valueObserver:observer];
|
|
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:nodeId
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
|
|
endCallback:nil];
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(observer.calls.count, 1UL);
|
|
XCTAssertEqualObjects(observer.calls[0], @0);
|
|
|
|
[_nodesManager stopListeningToAnimatedNodeValue:nodeId];
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(observer.calls.count, 1UL);
|
|
}
|
|
|
|
- (void)testNodeValueListenerIfListening
|
|
{
|
|
NSNumber *nodeId = @1;
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
|
|
RCTFakeValueObserver *observer = [RCTFakeValueObserver new];
|
|
[_nodesManager startListeningToAnimatedNodeValue:nodeId valueObserver:observer];
|
|
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:nodeId
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
|
|
endCallback:nil];
|
|
|
|
for (NSUInteger i = 0; i < frames.count; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(observer.calls.count, i + 1);
|
|
XCTAssertEqualWithAccuracy(observer.calls[i].doubleValue, frames[i].doubleValue, FLT_EPSILON);
|
|
}
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(observer.calls.count, 7UL);
|
|
XCTAssertEqualObjects(observer.calls[6], @1);
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(observer.calls.count, 7UL);
|
|
}
|
|
|
|
- (void)performSpringAnimationTestWithConfig:(NSDictionary*)config isCriticallyDamped:(BOOL)testForCriticallyDamped
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:config
|
|
endCallback:nil];
|
|
|
|
BOOL wasGreaterThanOne = NO;
|
|
CGFloat previousValue = 0;
|
|
__block CGFloat currentValue;
|
|
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
|
|
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
|
|
[invocation getArgument:&props atIndex:4];
|
|
currentValue = props[@"opacity"].doubleValue;
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
// Run for 3 seconds.
|
|
for (NSUInteger i = 0; i < 3 * 60; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
|
|
if (currentValue > 1) {
|
|
wasGreaterThanOne = YES;
|
|
}
|
|
|
|
// Verify that animation step is relatively small.
|
|
XCTAssertLessThan(fabs(currentValue - previousValue), 0.12);
|
|
|
|
previousValue = currentValue;
|
|
}
|
|
|
|
// Verify that we've reach the final value at the end of animation.
|
|
XCTAssertEqual(previousValue, 1.0);
|
|
|
|
// Verify that value has reached some maximum value that is greater than the final value (bounce).
|
|
if (testForCriticallyDamped) {
|
|
XCTAssertFalse(wasGreaterThanOne);
|
|
} else {
|
|
XCTAssertTrue(wasGreaterThanOne);
|
|
}
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testUnderdampedSpringAnimation
|
|
{
|
|
[self performSpringAnimationTestWithConfig:@{@"type": @"spring",
|
|
@"stiffness": @230.3,
|
|
@"damping": @22,
|
|
@"mass": @1,
|
|
@"initialVelocity": @0,
|
|
@"toValue": @1,
|
|
@"restSpeedThreshold": @0.001,
|
|
@"restDisplacementThreshold": @0.001,
|
|
@"overshootClamping": @NO}
|
|
isCriticallyDamped:NO];
|
|
}
|
|
|
|
- (void)testCritcallyDampedSpringAnimation
|
|
{
|
|
[self performSpringAnimationTestWithConfig:@{@"type": @"spring",
|
|
@"stiffness": @1000,
|
|
@"damping": @500,
|
|
@"mass": @3,
|
|
@"initialVelocity": @0,
|
|
@"toValue": @1,
|
|
@"restSpeedThreshold": @0.001,
|
|
@"restDisplacementThreshold": @0.001,
|
|
@"overshootClamping": @NO}
|
|
isCriticallyDamped:YES];
|
|
}
|
|
|
|
- (void)testDecayAnimation
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"decay",
|
|
@"velocity": @0.5,
|
|
@"deceleration": @0.998}
|
|
endCallback:nil];
|
|
|
|
|
|
__block CGFloat previousValue;
|
|
__block CGFloat currentValue;
|
|
CGFloat previousDiff = CGFLOAT_MAX;
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
|
|
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
|
|
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
|
|
[invocation getArgument:&props atIndex:4];
|
|
currentValue = props[@"opacity"].doubleValue;
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
// Run 3 secs of animation.
|
|
for (NSUInteger i = 0; i < 3 * 60; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
CGFloat currentDiff = currentValue - previousValue;
|
|
// Verify monotonicity.
|
|
// Greater *or equal* because the animation stops during these 3 seconds.
|
|
XCTAssertGreaterThanOrEqual(currentValue, previousValue);
|
|
// Verify decay.
|
|
XCTAssertLessThanOrEqual(currentDiff, previousDiff);
|
|
previousValue = currentValue;
|
|
previousDiff = currentDiff;
|
|
}
|
|
|
|
// Should be done in 3 secs.
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testDecayAnimationLoop
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"decay",
|
|
@"velocity": @0.5,
|
|
@"deceleration": @0.998,
|
|
@"iterations": @5}
|
|
endCallback:nil];
|
|
|
|
|
|
__block CGFloat previousValue;
|
|
__block CGFloat currentValue;
|
|
BOOL didComeToRest = NO;
|
|
NSUInteger numberOfResets = 0;
|
|
|
|
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
|
|
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
|
|
[invocation getArgument:&props atIndex:4];
|
|
currentValue = props[@"opacity"].doubleValue;
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
// Run 3 secs of animation five times.
|
|
for (NSUInteger i = 0; i < 3 * 60 * 5; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
|
|
// Verify monotonicity when not resetting the animation.
|
|
// Greater *or equal* because the animation stops during these 3 seconds.
|
|
if (!didComeToRest) {
|
|
XCTAssertGreaterThanOrEqual(currentValue, previousValue);
|
|
}
|
|
|
|
if (didComeToRest && currentValue != previousValue) {
|
|
numberOfResets++;
|
|
didComeToRest = NO;
|
|
}
|
|
|
|
// Test if animation has come to rest using the 0.1 threshold from DecayAnimation.m.
|
|
didComeToRest = fabs(currentValue - previousValue) < 0.1;
|
|
previousValue = currentValue;
|
|
}
|
|
|
|
// The animation should have reset 4 times.
|
|
XCTAssertEqual(numberOfResets, 4u);
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testSpringAnimationLoop
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"spring",
|
|
@"iterations": @5,
|
|
@"stiffness": @230.2,
|
|
@"damping": @22,
|
|
@"mass": @1,
|
|
@"initialVelocity": @0,
|
|
@"toValue": @1,
|
|
@"restSpeedThreshold": @0.001,
|
|
@"restDisplacementThreshold": @0.001,
|
|
@"overshootClamping": @NO}
|
|
endCallback:nil];
|
|
|
|
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];
|
|
|
|
// Run for 3 seconds five times.
|
|
for (NSUInteger i = 0; i < 3 * 60 * 5; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
|
|
if (!didComeToRest) {
|
|
// Verify that animation step is relatively small.
|
|
XCTAssertLessThan(fabs(currentValue - previousValue), 0.12);
|
|
}
|
|
|
|
// Test to see if it reset after coming to rest
|
|
if (didComeToRest && currentValue == 0) {
|
|
didComeToRest = NO;
|
|
numberOfResets++;
|
|
}
|
|
|
|
// Record that the animation did come to rest when it rests on toValue.
|
|
didComeToRest = fabs(currentValue - 1) < 0.001 && fabs(currentValue - previousValue) < 0.001;
|
|
|
|
previousValue = currentValue;
|
|
}
|
|
|
|
// Verify that value reset 4 times after finishing a full animation and is currently resting.
|
|
XCTAssertEqual(numberOfResets, 4u);
|
|
XCTAssertTrue(didComeToRest);
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testAnimationCallbackFinish
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
NSArray<NSNumber *> *frames = @[@0, @1];
|
|
|
|
__block NSInteger endCallbackCalls = 0;
|
|
|
|
RCTResponseSenderBlock endCallback = ^(NSArray *response) {
|
|
endCallbackCalls++;
|
|
XCTAssertEqualObjects(response, @[@{@"finished": @YES}]);
|
|
};
|
|
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
|
|
endCallback:endCallback];
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(endCallbackCalls, 0);
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(endCallbackCalls, 1);
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(endCallbackCalls, 1);
|
|
}
|
|
|
|
/**
|
|
* Creates a following graph of nodes:
|
|
* Value(1, firstValue) ----> Add(3) ---> Style(4) ---> Props(5) ---> View(viewTag)
|
|
* |
|
|
* Value(2, secondValue) --+
|
|
*
|
|
* Add(3) node maps to a "translateX" attribute of the Style(4) node.
|
|
*/
|
|
- (void)createAnimatedGraphWithAdditionNode:(NSNumber *)viewTag
|
|
firstValue:(CGFloat)firstValue
|
|
secondValue:(CGFloat)secondValue
|
|
{
|
|
[_nodesManager createAnimatedNode:@1
|
|
config:@{@"type": @"value", @"value": @(firstValue), @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@2
|
|
config:@{@"type": @"value", @"value": @(secondValue), @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@3
|
|
config:@{@"type": @"addition", @"input": @[@1, @2]}];
|
|
[_nodesManager createAnimatedNode:@4
|
|
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
|
|
[_nodesManager createAnimatedNode:@5
|
|
config:@{@"type": @"props", @"props": @{@"style": @4}}];
|
|
|
|
[_nodesManager connectAnimatedNodes:@1 childTag:@3];
|
|
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
|
|
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
|
|
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
|
|
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
|
|
}
|
|
|
|
- (void)testAdditionNode
|
|
{
|
|
NSNumber *viewTag = @50;
|
|
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
|
|
|
|
NSArray<NSNumber *> *frames = @[@0, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @101}
|
|
endCallback:nil];
|
|
[_nodesManager startAnimatingNode:@2
|
|
nodeTag:@2
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1010}
|
|
endCallback:nil];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1100)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1111)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1111)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
/**
|
|
* Verifies that views are updated properly when one of the addition input nodes has started animating
|
|
* while the other one has not.
|
|
*
|
|
* We expect that the output of the addition node will take the starting value of the second input
|
|
* node even though the node hasn't been connected to an active animation driver.
|
|
*/
|
|
- (void)testViewReceiveUpdatesIfOneOfAnimationHasntStarted
|
|
{
|
|
NSNumber *viewTag = @50;
|
|
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
|
|
|
|
NSArray<NSNumber *> *frames = @[@0, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @101}
|
|
endCallback:nil];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1100)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1101)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1101)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
/**
|
|
* Verifies that views are updated properly when one of the addition input nodes animation finishes
|
|
* before the other.
|
|
*
|
|
* We expect that the output of the addition node after one of the animation has finished will
|
|
* take the last value of the animated node and the view will receive updates up until the second
|
|
* animation is over.
|
|
*/
|
|
- (void)testViewReceiveUpdatesWhenOneOfAnimationHasFinished
|
|
{
|
|
NSNumber *viewTag = @50;
|
|
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
|
|
|
|
NSArray<NSNumber *> *firstFrames = @[@0, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": firstFrames, @"toValue": @200}
|
|
endCallback:nil];
|
|
NSArray<NSNumber *> *secondFrames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
[_nodesManager startAnimatingNode:@2
|
|
nodeTag:@2
|
|
config:@{@"type": @"frames", @"frames": secondFrames, @"toValue": @1010}
|
|
endCallback:nil];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1100)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
for (NSUInteger i = 1; i < secondFrames.count; i++) {
|
|
CGFloat expected = 1200.0 + secondFrames[i].doubleValue * 10.0;
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @(expected))];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @1210)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testMultiplicationNode
|
|
{
|
|
NSNumber *viewTag = @50;
|
|
[_nodesManager createAnimatedNode:@1
|
|
config:@{@"type": @"value", @"value": @1, @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@2
|
|
config:@{@"type": @"value", @"value": @5, @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@3
|
|
config:@{@"type": @"multiplication", @"input": @[@1, @2]}];
|
|
[_nodesManager createAnimatedNode:@4
|
|
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
|
|
[_nodesManager createAnimatedNode:@5
|
|
config:@{@"type": @"props", @"props": @{@"style": @4}}];
|
|
|
|
[_nodesManager connectAnimatedNodes:@1 childTag:@3];
|
|
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
|
|
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
|
|
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
|
|
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
|
|
|
|
NSArray<NSNumber *> *frames = @[@0, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @2}
|
|
endCallback:nil];
|
|
[_nodesManager startAnimatingNode:@2
|
|
nodeTag:@2
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @10}
|
|
endCallback:nil];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @5)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @20)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", @20)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testHandleStoppingAnimation
|
|
{
|
|
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
|
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
|
|
__block BOOL endCallbackCalled = NO;
|
|
|
|
RCTResponseSenderBlock endCallback = ^(NSArray *response) {
|
|
endCallbackCalled = YES;
|
|
XCTAssertEqualObjects(response, @[@{@"finished": @NO}]);
|
|
};
|
|
|
|
[_nodesManager startAnimatingNode:@404
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
|
|
endCallback:endCallback];
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
[_nodesManager stopAnimation:@404];
|
|
XCTAssertEqual(endCallbackCalled, YES);
|
|
|
|
// Run "update" loop a few more times -> we expect no further updates nor callback calls to be
|
|
// triggered
|
|
for (NSUInteger i = 0; i < 5; i++) {
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
}
|
|
|
|
- (void)testInterpolationNode
|
|
{
|
|
NSNumber *viewTag = @50;
|
|
[_nodesManager createAnimatedNode:@1
|
|
config:@{@"type": @"value", @"value": @10, @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@2
|
|
config:@{@"type": @"interpolation",
|
|
@"inputRange": @[@10, @20],
|
|
@"outputRange": @[@0, @1],
|
|
@"extrapolateLeft": @"extend",
|
|
@"extrapolateRight": @"extend"}];
|
|
[_nodesManager createAnimatedNode:@3
|
|
config:@{@"type": @"style", @"style": @{@"opacity": @2}}];
|
|
[_nodesManager createAnimatedNode:@4
|
|
config:@{@"type": @"props", @"props": @{@"style": @3}}];
|
|
|
|
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
|
|
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
|
|
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
|
|
[_nodesManager connectAnimatedNodeToView:@4 viewTag:viewTag viewName:@"UIView"];
|
|
|
|
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
|
[_nodesManager startAnimatingNode:@1
|
|
nodeTag:@1
|
|
config:@{@"type": @"frames", @"frames": frames, @"toValue": @20}
|
|
endCallback:nil];
|
|
|
|
for (NSNumber *frame in frames) {
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", frame)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", @1)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (id<RCTEvent>)createScrollEventWithTag:(NSNumber *)viewTag value:(CGFloat)value
|
|
{
|
|
// The event value is the 3rd argument.
|
|
NSArray *arguments = @[@1, @1, @{@"contentOffset": @{@"y": @(value)}}];
|
|
return [[RCTFakeEvent alloc] initWithName:@"topScroll"
|
|
viewTag:viewTag
|
|
arguments:arguments];
|
|
}
|
|
|
|
- (void)testNativeAnimatedEventDoUpdate
|
|
{
|
|
NSNumber *viewTag = @1000;
|
|
[self createSimpleAnimatedView:viewTag withOpacity:0];
|
|
|
|
[_nodesManager addAnimatedEventToView:viewTag
|
|
eventName:@"topScroll"
|
|
eventMapping:@{@"animatedValueTag": @1,
|
|
@"nativeEventPath": @[@"contentOffset", @"y"]}];
|
|
|
|
|
|
|
|
// Make sure that the update actually happened synchronously in `handleAnimatedEvent` and does
|
|
// not wait for the next animation loop.
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"opacity", @10)];
|
|
[_nodesManager handleAnimatedEvent:[self createScrollEventWithTag:viewTag value:10]];
|
|
[_uiManager verify];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
- (void)testNativeAnimatedEventDoNotUpdate
|
|
{
|
|
NSNumber *viewTag = @1000;
|
|
[self createSimpleAnimatedView:viewTag withOpacity:0];
|
|
|
|
[_nodesManager addAnimatedEventToView:viewTag
|
|
eventName:@"otherEvent"
|
|
eventMapping:@{@"animatedValueTag": @1,
|
|
@"nativeEventPath": @[@"contentOffset", @"y"]}];
|
|
|
|
[_nodesManager addAnimatedEventToView:@999
|
|
eventName:@"topScroll"
|
|
eventMapping:@{@"animatedValueTag": @1,
|
|
@"nativeEventPath": @[@"contentOffset", @"y"]}];
|
|
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager handleAnimatedEvent:[self createScrollEventWithTag:viewTag value:10]];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
/**
|
|
* Creates a following graph of nodes:
|
|
* Value(3, initialValue) ----> Style(4) ---> Props(5) ---> View(viewTag)
|
|
*
|
|
* Value(3) is set to track Value(1) via Tracking(2) node with the provided animation config
|
|
*/
|
|
- (void)createAnimatedGraphWithTrackingNode:(NSNumber *)viewTag
|
|
initialValue:(CGFloat)initialValue
|
|
animationConfig:(NSDictionary *)animationConfig
|
|
{
|
|
[_nodesManager createAnimatedNode:@1
|
|
config:@{@"type": @"value", @"value": @(initialValue), @"offset": @0}];
|
|
[_nodesManager createAnimatedNode:@3
|
|
config:@{@"type": @"value", @"value": @(initialValue), @"offset": @0}];
|
|
|
|
[_nodesManager createAnimatedNode:@2
|
|
config:@{@"type": @"tracking",
|
|
@"animationId": @70,
|
|
@"value": @3,
|
|
@"toValue": @1,
|
|
@"animationConfig": animationConfig}];
|
|
[_nodesManager createAnimatedNode:@4
|
|
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
|
|
[_nodesManager createAnimatedNode:@5
|
|
config:@{@"type": @"props", @"props": @{@"style": @4}}];
|
|
|
|
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
|
|
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
|
|
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
|
|
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
|
|
}
|
|
|
|
/**
|
|
* In this test we verify that when value is being tracked we can update destination value in the
|
|
* middle of ongoing animation and the animation will update and animate to the new spot. This is
|
|
* tested using simple 5 frame backed timing animation.
|
|
*/
|
|
- (void)testTracking
|
|
{
|
|
NSArray *frames = @[@0, @0.25, @0.5, @0.75, @1];
|
|
NSDictionary *animationConfig = @{@"type": @"frames", @"frames": frames};
|
|
[self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:animationConfig];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", 0)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
// update "toValue" to 100, we expect tracking animation to animate now from 0 to 100 in 5 steps
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@100];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
for (NSNumber *frame in frames) {
|
|
NSNumber *expected = @([frame doubleValue] * 100);
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", expected)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
// update "toValue" to 0 but run only two frames from the animation,
|
|
// we expect tracking animation to animate now from 100 to 75
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@0];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
NSNumber *expected = @(100. * (1. - [frames[i] doubleValue]));
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", expected)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
// at this point we expect tracking value to be at 75
|
|
// we update "toValue" again to 100 and expect the animation to restart from the current place
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@100];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
for (NSNumber *frame in frames) {
|
|
NSNumber *expected = @(50. + 50. * [frame doubleValue]);
|
|
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
|
viewName:@"UIView"
|
|
props:RCTPropChecker(@"translateX", expected)];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
/**
|
|
* In this test we verify that when tracking is set up for a given animated node and when the
|
|
* animation settles it will not be registered as an active animation and therefore will not
|
|
* consume resources on running the animation that has already completed. Then we verify that when
|
|
* the value updates the animation will resume as expected and the complete again when reaches the
|
|
* end.
|
|
*/
|
|
|
|
- (void)testTrackingPausesWhenEndValueIsReached
|
|
{
|
|
NSArray *frames = @[@0, @0.5, @1];
|
|
NSDictionary *animationConfig = @{@"type": @"frames", @"frames": frames};
|
|
[self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:animationConfig];
|
|
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@100];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
__block int callCount = 0;
|
|
[[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) {
|
|
callCount++;
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
for (NSUInteger i = 0; i < frames.count; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
}
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(callCount, 4);
|
|
|
|
// the animation has completed, we expect no updates to be done
|
|
[[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) {
|
|
XCTFail("Expected not to be called");
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
|
|
// restore rejected method, we will use it later on
|
|
callCount = 0;
|
|
[[[_uiManager stub] andDo:^(NSInvocation* __unused invocation) {
|
|
callCount++;
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
// we update end value and expect the animation to restart
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@200];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
for (NSUInteger i = 0; i < frames.count; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
}
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
XCTAssertEqual(callCount, 4);
|
|
|
|
// the animation has completed, we expect no updates to be done
|
|
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
[_uiManager verify];
|
|
}
|
|
|
|
/**
|
|
* In this test we verify that when tracking is configured to use spring animation and when the
|
|
* destination value updates the current speed of the animated value will be taken into account
|
|
* while updating the spring animation and it will smoothly transition to the new end value.
|
|
*/
|
|
- (void) testSpringTrackingRetainsSpeed
|
|
{
|
|
// this spring config correspomds to tension 20 and friction 0.5 which makes the spring settle
|
|
// very slowly
|
|
NSDictionary *springConfig = @{@"type": @"spring",
|
|
@"restSpeedThreshold": @0.001,
|
|
@"mass": @1,
|
|
@"restDisplacementThreshold": @0.001,
|
|
@"initialVelocity": @0.5,
|
|
@"damping": @2.5,
|
|
@"stiffness": @157.8,
|
|
@"overshootClamping": @NO};
|
|
[self createAnimatedGraphWithTrackingNode:@1000 initialValue:0 animationConfig:springConfig];
|
|
|
|
__block CGFloat lastTranslateX = 0;
|
|
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
|
|
__unsafe_unretained NSDictionary *props = nil;
|
|
[invocation getArgument:&props atIndex:4];
|
|
lastTranslateX = [props[@"translateX"] doubleValue];
|
|
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
|
|
|
// update "toValue" to 1, we expect tracking animation to animate now from 0 to 1
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@1];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
// we run several steps of animation until the value starts bouncing, has negative speed and
|
|
// passes the final point (that is 1) while going backwards
|
|
BOOL isBoucingBack = NO;
|
|
CGFloat previousValue = 0;
|
|
for (int maxFrames = 500; maxFrames > 0; maxFrames--) {
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
if (previousValue >= 1. && lastTranslateX < 1.) {
|
|
isBoucingBack = YES;
|
|
break;
|
|
}
|
|
previousValue = lastTranslateX;
|
|
}
|
|
XCTAssert(isBoucingBack);
|
|
|
|
// we now update "toValue" to 1.5 but since the value have negative speed and has also pretty
|
|
// low friction we expect it to keep going in the opposite direction for a few more frames
|
|
[_nodesManager setAnimatedNodeValue:@1 value:@1.5];
|
|
[_nodesManager stepAnimations:_displayLink]; // kick off the tracking
|
|
|
|
int bounceBackInitialFrames = 0;
|
|
BOOL hasTurnedForward = NO;
|
|
|
|
// we run 8 seconds of animation
|
|
for (int i = 0; i < 8 * 60; i++) {
|
|
[_nodesManager stepAnimations:_displayLink];
|
|
if (!hasTurnedForward) {
|
|
if (lastTranslateX <= previousValue) {
|
|
bounceBackInitialFrames++;
|
|
} else {
|
|
hasTurnedForward = true;
|
|
}
|
|
}
|
|
previousValue = lastTranslateX;
|
|
}
|
|
XCTAssert(hasTurnedForward);
|
|
XCTAssertGreaterThan(bounceBackInitialFrames, 3);
|
|
XCTAssertEqual(lastTranslateX, 1.5);
|
|
}
|
|
|
|
@end
|