[ReactNative] fixup AnimationExperimental a bit

This commit is contained in:
Spencer Ahrens 2015-04-08 14:09:24 -07:00
parent bd7b9da64a
commit 9ea0002774
7 changed files with 223 additions and 157 deletions

View File

@ -73,19 +73,29 @@ class Tile extends React.Component {
if (tile.isNew()) {
offset.opacity = 0;
} else {
var point = [
animationPosition(tile.toColumn()),
animationPosition(tile.toRow()),
];
AnimationExperimental.startAnimation(this.refs['this'], 100, 0, 'easeInOutQuad', {position: point});
var point = {
x: animationPosition(tile.toColumn()),
y: animationPosition(tile.toRow()),
};
AnimationExperimental.startAnimation({
node: this.refs['this'],
duration: 100,
easing: 'easeInOutQuad',
property: 'position',
toValue: point,
});
}
return offset;
}
componentDidMount() {
AnimationExperimental.startAnimation(this.refs['this'], 100, 0, 'easeInOutQuad', {opacity: 1});
AnimationExperimental.startAnimation({
node: this.refs['this'],
duration: 100,
easing: 'easeInOutQuad',
property: 'opacity',
toValue: 1,
});
}
render() {

View File

@ -16,6 +16,17 @@ var AnimationUtils = require('AnimationUtils');
type EasingFunction = (t: number) => number;
var Properties = {
opacity: true,
position: true,
positionX: true,
positionY: true,
rotation: true,
scaleXY: true,
};
type ValueType = number | Array<number> | {[key: string]: number};
/**
* This is an experimental module that is under development, incomplete,
* potentially buggy, not used in any production apps, and will probably change
@ -24,24 +35,34 @@ type EasingFunction = (t: number) => number;
* Use at your own risk.
*/
var AnimationExperimental = {
Mixin: require('AnimationExperimentalMixin'),
startAnimation: function(
node: any,
duration: number,
delay: number,
easing: (string | EasingFunction),
properties: {[key: string]: any}
anim: {
node: any;
duration: number;
easing: ($Enum<typeof AnimationUtils.Defaults> | EasingFunction);
property: $Enum<typeof Properties>;
toValue: ValueType;
fromValue?: ValueType;
delay?: number;
},
callback?: ?(finished: bool) => void
): number {
var nodeHandle = +node.getNodeHandle();
var easingSample = AnimationUtils.evaluateEasingFunction(duration, easing);
var tag: number = RCTAnimationManager.startAnimation(
var nodeHandle = anim.node.getNodeHandle();
var easingSample = AnimationUtils.evaluateEasingFunction(
anim.duration,
anim.easing
);
var tag: number = AnimationUtils.allocateTag();
var props = {};
props[anim.property] = {to: anim.toValue};
RCTAnimationManager.startAnimation(
nodeHandle,
AnimationUtils.allocateTag(),
duration,
delay,
tag,
anim.duration,
anim.delay,
easingSample,
properties
props,
callback
);
return tag;
},
@ -51,4 +72,18 @@ var AnimationExperimental = {
},
};
if (__DEV__) {
if (RCTAnimationManager && RCTAnimationManager.Properties) {
var a = Object.keys(Properties);
var b = RCTAnimationManager.Properties;
var diff = a.filter((i) => b.indexOf(i) < 0).concat(
b.filter((i) => a.indexOf(i) < 0)
);
if (diff.length > 0) {
throw new Error('JS animation properties don\'t match native properties.' +
JSON.stringify(diff, null, ' '));
}
}
}
module.exports = AnimationExperimental;

View File

@ -1,58 +0,0 @@
/**
* 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.
*
* @providesModule AnimationExperimentalMixin
* @flow
*/
'use strict';
var AnimationUtils = require('AnimationUtils');
var RCTAnimationManager = require('NativeModules').AnimationExperimentalManager;
var invariant = require('invariant');
type EasingFunction = (t: number) => number;
/**
* This is an experimental module that is under development, incomplete,
* potentially buggy, not used in any production apps, and will probably change
* in non-backward compatible ways.
*
* Use at your own risk.
*/
var AnimationExperimentalMixin = {
getInitialState: function(): Object {
return {};
},
startAnimation: function(
refKey: string,
duration: number,
delay: number,
easing: (string | EasingFunction),
properties: {[key: string]: any}
): number {
var ref = this.refs[refKey];
invariant(
ref,
'Invalid refKey ' + refKey + '; ' +
'valid refs: ' + JSON.stringify(Object.keys(this.refs))
);
var nodeHandle = +ref.getNodeHandle();
var easingSample = AnimationUtils.evaluateEasingFunction(duration, easing);
var tag: number = RCTAnimationManager.startAnimation(nodeHandle, AnimationUtils.allocateTag(), duration, delay, easingSample, properties);
return tag;
},
stopAnimation: function(tag: number) {
RCTAnimationManager.stopAnimation(tag);
},
};
module.exports = AnimationExperimentalMixin;

View File

@ -20,27 +20,27 @@
type EasingFunction = (t: number) => number;
var defaults = {
easeInQuad: function(t) {
easeInQuad: function(t: number): number {
return t * t;
},
easeOutQuad: function(t) {
easeOutQuad: function(t: number): number {
return -t * (t - 2);
},
easeInOutQuad: function(t) {
easeInOutQuad: function(t: number): number {
t = t * 2;
if (t < 1) {
return 0.5 * t * t;
}
return -((t - 1) * (t - 3) - 1) / 2;
},
easeInCubic: function(t) {
easeInCubic: function(t: number): number {
return t * t * t;
},
easeOutCubic: function(t) {
easeOutCubic: function(t: number): number {
t -= 1;
return t * t * t + 1;
},
easeInOutCubic: function(t) {
easeInOutCubic: function(t: number): number {
t *= 2;
if (t < 1) {
return 0.5 * t * t * t;
@ -48,14 +48,14 @@ var defaults = {
t -= 2;
return (t * t * t + 2) / 2;
},
easeInQuart: function(t) {
easeInQuart: function(t: number): number {
return t * t * t * t;
},
easeOutQuart: function(t) {
easeOutQuart: function(t: number): number {
t -= 1;
return -(t * t * t * t - 1);
},
easeInOutQuart: function(t) {
easeInOutQuart: function(t: number): number {
t *= 2;
if (t < 1) {
return 0.5 * t * t * t * t;
@ -63,14 +63,14 @@ var defaults = {
t -= 2;
return -(t * t * t * t - 2) / 2;
},
easeInQuint: function(t) {
easeInQuint: function(t: number): number {
return t * t * t * t * t;
},
easeOutQuint: function(t) {
easeOutQuint: function(t: number): number {
t -= 1;
return t * t * t * t * t + 1;
},
easeInOutQuint: function(t) {
easeInOutQuint: function(t: number): number {
t *= 2;
if (t < 1) {
return (t * t * t * t * t) / 2;
@ -78,22 +78,22 @@ var defaults = {
t -= 2;
return (t * t * t * t * t + 2) / 2;
},
easeInSine: function(t) {
easeInSine: function(t: number): number {
return -Math.cos(t * (Math.PI / 2)) + 1;
},
easeOutSine: function(t) {
easeOutSine: function(t: number): number {
return Math.sin(t * (Math.PI / 2));
},
easeInOutSine: function(t) {
easeInOutSine: function(t: number): number {
return -(Math.cos(Math.PI * t) - 1) / 2;
},
easeInExpo: function(t) {
easeInExpo: function(t: number): number {
return (t === 0) ? 0 : Math.pow(2, 10 * (t - 1));
},
easeOutExpo: function(t) {
easeOutExpo: function(t: number): number {
return (t === 1) ? 1 : (-Math.pow(2, -10 * t) + 1);
},
easeInOutExpo: function(t) {
easeInOutExpo: function(t: number): number {
if (t === 0) {
return 0;
}
@ -106,14 +106,14 @@ var defaults = {
}
return (-Math.pow(2, -10 * (t - 1)) + 2) / 2;
},
easeInCirc: function(t) {
easeInCirc: function(t: number): number {
return -(Math.sqrt(1 - t * t) - 1);
},
easeOutCirc: function(t) {
easeOutCirc: function(t: number): number {
t -= 1;
return Math.sqrt(1 - t * t);
},
easeInOutCirc: function(t) {
easeInOutCirc: function(t: number): number {
t *= 2;
if (t < 1) {
return -(Math.sqrt(1 - t * t) - 1) / 2;
@ -121,7 +121,7 @@ var defaults = {
t -= 2;
return (Math.sqrt(1 - t * t) + 1) / 2;
},
easeInElastic: function(t) {
easeInElastic: function(t: number): number {
var s = 1.70158;
var p = 0.3;
if (t === 0) {
@ -134,7 +134,7 @@ var defaults = {
t -= 1;
return -(Math.pow(2, 10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
},
easeOutElastic: function(t) {
easeOutElastic: function(t: number): number {
var s = 1.70158;
var p = 0.3;
if (t === 0) {
@ -146,7 +146,7 @@ var defaults = {
var s = p / (2 * Math.PI) * Math.asin(1);
return Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
},
easeInOutElastic: function(t) {
easeInOutElastic: function(t: number): number {
var s = 1.70158;
var p = 0.3 * 1.5;
if (t === 0) {
@ -164,16 +164,16 @@ var defaults = {
t -= 1;
return Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) / 2 + 1;
},
easeInBack: function(t) {
easeInBack: function(t: number): number {
var s = 1.70158;
return t * t * ((s + 1) * t - s);
},
easeOutBack: function(t) {
easeOutBack: function(t: number): number {
var s = 1.70158;
t -= 1;
return (t * t * ((s + 1) * t + s) + 1);
},
easeInOutBack: function(t) {
easeInOutBack: function(t: number): number {
var s = 1.70158 * 1.525;
t *= 2;
if (t < 1) {
@ -182,10 +182,10 @@ var defaults = {
t -= 2;
return (t * t * ((s + 1) * t + s) + 2) / 2;
},
easeInBounce: function(t) {
easeInBounce: function(t: number): number {
return 1 - this.easeOutBounce(1 - t);
},
easeOutBounce: function(t) {
easeOutBounce: function(t: number): number {
if (t < (1 / 2.75)) {
return 7.5625 * t * t;
} else if (t < (2 / 2.75)) {
@ -199,7 +199,7 @@ var defaults = {
return 7.5625 * t * t + 0.984375;
}
},
easeInOutBounce: function(t) {
easeInOutBounce: function(t: number): number {
if (t < 0.5) {
return this.easeInBounce(t * 2) / 2;
}
@ -234,4 +234,6 @@ module.exports = {
return samples;
},
Defaults: defaults,
};

View File

@ -17,18 +17,20 @@ var RCTUIManager = require('NativeModules').UIManager;
var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker');
var keyMirror = require('keyMirror');
var Types = keyMirror({
var TypesEnum = {
spring: true,
linear: true,
easeInEaseOut: true,
easeIn: true,
easeOut: true,
});
};
var Types = keyMirror(TypesEnum);
var Properties = keyMirror({
var PropertiesEnum = {
opacity: true,
scaleXY: true,
});
};
var Properties = keyMirror(PropertiesEnum);
var animChecker = createStrictShapeTypeChecker({
duration: PropTypes.number,
@ -48,8 +50,8 @@ type Anim = {
delay?: number;
springDamping?: number;
initialVelocity?: number;
type?: $Enum<typeof Types>;
property?: $Enum<typeof Properties>;
type?: $Enum<typeof TypesEnum>;
property?: $Enum<typeof PropertiesEnum>;
}
var configChecker = createStrictShapeTypeChecker({

View File

@ -13,6 +13,7 @@
#import "RCTSparseArray.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
#if CGFLOAT_IS_DOUBLE
#define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_D
@ -23,6 +24,8 @@
@implementation RCTAnimationExperimentalManager
{
RCTSparseArray *_animationRegistry; // Main thread only; animation tag -> view tag
RCTSparseArray *_callbackRegistry; // Main thread only; animation tag -> callback
NSDictionary *_keypathMapping;
}
RCT_EXPORT_MODULE()
@ -33,6 +36,33 @@ RCT_EXPORT_MODULE()
{
if ((self = [super init])) {
_animationRegistry = [[RCTSparseArray alloc] init];
_callbackRegistry = [[RCTSparseArray alloc] init];
_keypathMapping = @{
@"opacity": @{
@"keypath": @"opacity",
@"type": @"NSNumber",
},
@"position": @{
@"keypath": @"position",
@"type": @"CGPoint",
},
@"positionX": @{
@"keypath": @"position.x",
@"type": @"NSNumber",
},
@"positionY": @{
@"keypath": @"position.y",
@"type": @"NSNumber",
},
@"rotation": @{
@"keypath": @"transform.rotation.z",
@"type": @"NSNumber",
},
@"scaleXY": @{
@"keypath": @"transform.scale",
@"type": @"CGPoint",
},
};
}
return self;
@ -63,12 +93,25 @@ RCT_EXPORT_MODULE()
};
}
RCT_EXPORT_METHOD(startAnimationForTag:(NSNumber *)reactTag
static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NSString *key, id value)
{
RCTResponseSenderBlock callback = callbacks[tag];
RCTLogError(@"Invalid animation property `%@ = %@`", key, value);
if (callback) {
callback(@[@NO]);
callbacks[tag] = nil;
}
[CATransaction commit];
return;
}
RCT_EXPORT_METHOD(startAnimation:(NSNumber *)reactTag
animationTag:(NSNumber *)animationTag
duration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
easingSample:(NSArray *)easingSample
properties:(NSDictionary *)properties)
properties:(NSDictionary *)properties
callback:(RCTResponseSenderBlock)callback)
{
__weak RCTAnimationExperimentalManager *weakSelf = self;
[_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
@ -79,12 +122,21 @@ RCT_EXPORT_METHOD(startAnimationForTag:(NSNumber *)reactTag
RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag);
return;
}
[properties enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
__block BOOL completionBlockSet = NO;
[CATransaction begin];
for (NSString *prop in properties) {
NSString *keypath = _keypathMapping[prop][@"keypath"];
id obj = properties[prop][@"to"];
if (!keypath) {
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
}
NSValue *toValue = nil;
if ([key isEqualToString:@"scaleXY"]) {
key = @"transform.scale";
toValue = obj[0];
if ([keypath isEqualToString:@"transform.scale"]) {
CGPoint point = [RCTConvert CGPoint:obj];
if (point.x != point.y) {
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
}
toValue = @(point.x);
} else if ([obj respondsToSelector:@selector(count)]) {
switch ([obj count]) {
case 2:
@ -100,11 +152,15 @@ RCT_EXPORT_METHOD(startAnimationForTag:(NSNumber *)reactTag
case 16:
toValue = [NSValue valueWithCGAffineTransform:[RCTConvert CGAffineTransform:obj]];
break;
default:
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
}
} else if (![obj respondsToSelector:@selector(objCType)]) {
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, obj);
}
if (!toValue) {
toValue = obj;
}
if (!toValue) toValue = obj;
const char *typeName = toValue.objCType;
size_t count;
@ -155,7 +211,7 @@ RCT_EXPORT_METHOD(startAnimationForTag:(NSNumber *)reactTag
break;
}
NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:key];
NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:keypath];
CGFloat fromFields[count];
[fromValue getValue:fromFields];
@ -166,19 +222,32 @@ RCT_EXPORT_METHOD(startAnimationForTag:(NSNumber *)reactTag
CGFloat t = sample.CG_APPEND(, floatValue, doubleValue);
[sampledValues addObject:interpolationBlock(t)];
}
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:key];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:keypath];
animation.beginTime = CACurrentMediaTime() + delay;
animation.duration = duration;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.values = sampledValues;
[view.layer setValue:toValue forKey:key];
NSString *animationKey = [NSString stringWithFormat:@"RCT.%@.%@", animationTag, key];
@try {
[view.layer setValue:toValue forKey:keypath];
NSString *animationKey = [@"RCT" stringByAppendingString:RCTJSONStringify(@{@"tag": animationTag, @"key": keypath}, nil)];
[view.layer addAnimation:animation forKey:animationKey];
if (!completionBlockSet) {
strongSelf->_callbackRegistry[animationTag] = callback;
[CATransaction setCompletionBlock:^{
RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
if (cb) {
cb(@[@YES]);
strongSelf->_callbackRegistry[animationTag] = nil;
}
}];
completionBlockSet = YES;
}
}
@catch (NSException *exception) {
return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, toValue);
}
}
[CATransaction commit];
strongSelf->_animationRegistry[animationTag] = reactTag;
}];
}
@ -194,19 +263,25 @@ RCT_EXPORT_METHOD(stopAnimation:(NSNumber *)animationTag)
UIView *view = viewRegistry[reactTag];
for (NSString *animationKey in view.layer.animationKeys) {
if ([animationKey hasPrefix:@"RCT"]) {
NSRange periodLocation = [animationKey rangeOfString:@"." options:0 range:(NSRange){3, animationKey.length - 3}];
if (periodLocation.location != NSNotFound) {
NSInteger integerTag = [[animationKey substringWithRange:(NSRange){3, periodLocation.location}] integerValue];
if (animationTag.integerValue == integerTag) {
if ([animationKey hasPrefix:@"RCT{"]) {
NSDictionary *data = RCTJSONParse([animationKey substringFromIndex:3], nil);
if (animationTag.integerValue == [data[@"tag"] integerValue]) {
[view.layer removeAnimationForKey:animationKey];
}
}
}
RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
if (cb) {
cb(@[@NO]);
strongSelf->_callbackRegistry[animationTag] = nil;
}
strongSelf->_animationRegistry[animationTag] = nil;
}];
}
- (NSDictionary *)constantsToExport
{
return @{@"Properties": [_keypathMapping allKeys] };
}
@end

View File

@ -60,7 +60,7 @@ var TouchableBounce = React.createClass({
value: number,
velocity: number,
bounciness: number,
fromValue?: ?Function | number,
fromValue?: ?number,
callback?: ?Function
) {
if (POPAnimation) {
@ -71,21 +71,21 @@ var TouchableBounce = React.createClass({
toValue: [value, value],
velocity: [velocity, velocity],
springBounciness: bounciness,
fromValue: (undefined: ?any),
fromValue: fromValue ? [fromValue, fromValue] : undefined,
};
if (fromValue) {
anim.fromValue = [fromValue, fromValue];
}
this.state.animationID = POPAnimation.createSpringAnimation(anim);
this.addAnimation(this.state.animationID, callback);
} else {
AnimationExperimental.startAnimation(this, 300, 0, 'easeOutBack', {scaleXY: [value, value]});
if (fromValue && typeof fromValue === 'function') {
callback = fromValue;
}
if (callback) {
setTimeout(callback, 300);
}
AnimationExperimental.startAnimation(
{
node: this,
duration: 300,
easing: 'easeOutBack',
property: 'scaleXY',
toValue: { x: value, y: value},
},
callback
);
}
},