From 9ea0002774b39d720f5d4cac7853907ba2bde331 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 8 Apr 2015 14:09:24 -0700 Subject: [PATCH] [ReactNative] fixup AnimationExperimental a bit --- Examples/2048/Game2048.js | 26 ++-- Libraries/Animation/AnimationExperimental.js | 63 +++++++-- .../Animation/AnimationExperimentalMixin.js | 58 -------- Libraries/Animation/AnimationUtils.js | 62 +++++---- Libraries/Animation/LayoutAnimation.js | 14 +- .../RCTAnimationExperimentalManager.m | 131 ++++++++++++++---- .../Components/Touchable/TouchableBounce.js | 26 ++-- 7 files changed, 223 insertions(+), 157 deletions(-) delete mode 100644 Libraries/Animation/AnimationExperimentalMixin.js diff --git a/Examples/2048/Game2048.js b/Examples/2048/Game2048.js index a6e12ff13..a6e041ceb 100644 --- a/Examples/2048/Game2048.js +++ b/Examples/2048/Game2048.js @@ -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() { diff --git a/Libraries/Animation/AnimationExperimental.js b/Libraries/Animation/AnimationExperimental.js index 79daa4550..0a32c3f44 100644 --- a/Libraries/Animation/AnimationExperimental.js +++ b/Libraries/Animation/AnimationExperimental.js @@ -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 | {[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 | EasingFunction); + property: $Enum; + 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; diff --git a/Libraries/Animation/AnimationExperimentalMixin.js b/Libraries/Animation/AnimationExperimentalMixin.js deleted file mode 100644 index 7cee9b72b..000000000 --- a/Libraries/Animation/AnimationExperimentalMixin.js +++ /dev/null @@ -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; diff --git a/Libraries/Animation/AnimationUtils.js b/Libraries/Animation/AnimationUtils.js index ae9be5ccf..d6d95f62d 100644 --- a/Libraries/Animation/AnimationUtils.js +++ b/Libraries/Animation/AnimationUtils.js @@ -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, }; diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/Animation/LayoutAnimation.js index 12128055c..c297123ba 100644 --- a/Libraries/Animation/LayoutAnimation.js +++ b/Libraries/Animation/LayoutAnimation.js @@ -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; - property?: $Enum; + type?: $Enum; + property?: $Enum; } var configChecker = createStrictShapeTypeChecker({ diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 0ce871344..eb2ddd1cd 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -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]; - [view.layer addAnimation:animation forKey:animationKey]; - }]; - + @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) { - [view.layer removeAnimationForKey:animationKey]; - } + 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 diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index ffcc8e737..7cba22164 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -22,7 +22,7 @@ var copyProperties = require('copyProperties'); var onlyChild = require('onlyChild'); type State = { - animationID: ?number; + animationID: ?number; }; /** @@ -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 + ); } },