react-native/Libraries/NativeAnimation/Drivers/RCTSpringAnimation.m

208 lines
6.0 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTSpringAnimation.h"
#import <UIKit/UIKit.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import "RCTValueAnimatedNode.h"
@interface RCTSpringAnimation ()
@property (nonatomic, strong) NSNumber *animationId;
@property (nonatomic, strong) RCTValueAnimatedNode *valueNode;
@property (nonatomic, assign) BOOL animationHasBegun;
@property (nonatomic, assign) BOOL animationHasFinished;
@end
@implementation RCTSpringAnimation
{
CGFloat _toValue;
CGFloat _fromValue;
BOOL _overshootClamping;
CGFloat _restDisplacementThreshold;
CGFloat _restSpeedThreshold;
CGFloat _tension;
CGFloat _friction;
CGFloat _initialVelocity;
NSTimeInterval _animationStartTime;
NSTimeInterval _animationCurrentTime;
RCTResponseSenderBlock _callback;
CGFloat _lastPosition;
CGFloat _lastVelocity;
NSInteger _iterations;
NSInteger _currentLoop;
}
- (instancetype)initWithId:(NSNumber *)animationId
config:(NSDictionary *)config
forNode:(RCTValueAnimatedNode *)valueNode
callBack:(nullable RCTResponseSenderBlock)callback
{
if ((self = [super init])) {
NSNumber *iterations = [RCTConvert NSNumber:config[@"iterations"]] ?: @1;
_animationId = animationId;
_toValue = [RCTConvert CGFloat:config[@"toValue"]];
_fromValue = valueNode.value;
_valueNode = valueNode;
_overshootClamping = [RCTConvert BOOL:config[@"overshootClamping"]];
_restDisplacementThreshold = [RCTConvert CGFloat:config[@"restDisplacementThreshold"]];
_restSpeedThreshold = [RCTConvert CGFloat:config[@"restSpeedThreshold"]];
_tension = [RCTConvert CGFloat:config[@"tension"]];
_friction = [RCTConvert CGFloat:config[@"friction"]];
_initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]];
_callback = [callback copy];
_lastPosition = _fromValue;
_lastVelocity = _initialVelocity;
_animationHasFinished = iterations.integerValue == 0;
_iterations = iterations.integerValue;
_currentLoop = 1;
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
- (void)startAnimation
{
_animationStartTime = _animationCurrentTime = -1;
_animationHasBegun = YES;
}
- (void)stopAnimation
{
_valueNode = nil;
if (_callback) {
_callback(@[@{
@"finished": @(_animationHasFinished)
}]);
}
}
- (void)stepAnimationWithTime:(NSTimeInterval)currentTime
{
if (!_animationHasBegun || _animationHasFinished) {
// Animation has not begun or animation has already finished.
return;
}
if (_animationStartTime == -1) {
_animationStartTime = _animationCurrentTime = currentTime;
}
// We are using a fixed time step and a maximum number of iterations.
// The following post provides a lot of thoughts into how to build this
// loop: http://gafferongames.com/game-physics/fix-your-timestep/
CGFloat TIMESTEP_MSEC = 1;
// Velocity is based on seconds instead of milliseconds
CGFloat step = TIMESTEP_MSEC / 1000;
NSInteger numSteps = floorf((currentTime - _animationCurrentTime) / step);
_animationCurrentTime = currentTime;
if (numSteps == 0) {
return;
}
CGFloat position = _lastPosition;
CGFloat velocity = _lastVelocity;
CGFloat tempPosition = _lastPosition;
CGFloat tempVelocity = _lastVelocity;
for (NSInteger i = 0; i < numSteps; ++i) {
// This is using RK4. A good blog post to understand how it works:
// http://gafferongames.com/game-physics/integration-basics/
CGFloat aVelocity = velocity;
CGFloat aAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
tempPosition = position + aVelocity * step / 2;
tempVelocity = velocity + aAcceleration * step / 2;
CGFloat bVelocity = tempVelocity;
CGFloat bAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
tempPosition = position + bVelocity * step / 2;
tempVelocity = velocity + bAcceleration * step / 2;
CGFloat cVelocity = tempVelocity;
CGFloat cAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
tempPosition = position + cVelocity * step / 2;
tempVelocity = velocity + cAcceleration * step / 2;
CGFloat dVelocity = tempVelocity;
CGFloat dAcceleration = _tension * (_toValue - tempPosition) - _friction * tempVelocity;
tempPosition = position + cVelocity * step / 2;
tempVelocity = velocity + cAcceleration * step / 2;
CGFloat dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6;
CGFloat dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6;
position += dxdt * step;
velocity += dvdt * step;
}
_lastPosition = position;
_lastVelocity = velocity;
[self onUpdate:position];
if (_animationHasFinished) {
return;
}
// Conditions for stopping the spring animation
BOOL isOvershooting = NO;
if (_overshootClamping && _tension != 0) {
if (_fromValue < _toValue) {
isOvershooting = position > _toValue;
} else {
isOvershooting = position < _toValue;
}
}
BOOL isVelocity = ABS(velocity) <= _restSpeedThreshold;
BOOL isDisplacement = YES;
if (_tension != 0) {
isDisplacement = ABS(_toValue - position) <= _restDisplacementThreshold;
}
if (isOvershooting || (isVelocity && isDisplacement)) {
if (_tension != 0) {
// Ensure that we end up with a round value
if (_animationHasFinished) {
return;
}
[self onUpdate:_toValue];
}
if (_iterations == -1 || _currentLoop < _iterations) {
_lastPosition = _fromValue;
_lastVelocity = _initialVelocity;
_currentLoop++;
[self onUpdate:_fromValue];
} else {
_animationHasFinished = YES;
}
}
}
- (void)onUpdate:(CGFloat)outputValue
{
_valueNode.value = outputValue;
[_valueNode setNeedsUpdate];
}
@end