Skip to content

Instantly share code, notes, and snippets.

@sahrens
Last active February 29, 2016 01:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sahrens/ea3c6c0c871d35ae7dfb to your computer and use it in GitHub Desktop.
Save sahrens/ea3c6c0c871d35ae7dfb to your computer and use it in GitHub Desktop.
This is almost certainly the wrong approach. I wanted to try and get CoreAnimation-based
animations working because they should be the smoothest and most performant, but I think
there are too many limitations. Most notably, it can't update layout properties like
left/top. I think a better approach would be to make a custom animation runner, similar
to POP, that drives the animations from native by manually updating the properties,
potentially on the shadow views to trigger layout updates.
changeset: 840371:5f08152ca3e1
bookmark: animated_nativeCannedAnimations
parent: 836933:5e71db0e2454
user: Spencer Ahrens <sahrens@fb.com>
date: Wed Aug 26 11:44:43 2015 -0700
files: fbobjc/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/AnimationExample/AnimatedOptimized.js fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animated/Animated.js fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animation/RCTAnimationExperimentalManager.m fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/StyleSheet/precomputeStyle.js fbobjc/Libraries/FBReactKit/js/react-native-github/React/Base/RCTLog.h
description:
[WIP] Basic transform animations working
Summary: Simple transform animations now work, e.g. `translateX: this.timing` when enabled via explicit `enableOptimizations` flag in `TimingAnimationConfig`. This includes starting/stopping/interrupting them as you would expect.
Caveats:
1) Can't control transform ordering, so I added an invariant to disallow mixing transform types.
2) Only timing animations supported, which don't maintain velocity when interrupting running animations.
3) I think this breaks non-optimized animations in a few places...needs thorough testing.
4) Flow is super unhappy (20 errors).
Would be nice to robustly determine if we can safely run native optimizations automatically.
Test Plan: Needs thorough testing to make sure there are no regressions in non-optimized animations.
Reviewers: vjeux
CC:
Task ID: #
Blame Rev:
diff --git a/fbobjc/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/AnimationExample/AnimatedOptimized.js b/fbobjc/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/AnimationExample/AnimatedOptimized.js
new file mode 100644
--- /dev/null
+++ b/fbobjc/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/AnimationExample/AnimatedOptimized.js
@@ -0,0 +1,187 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @flow-broken
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ Animated,
+ Easing,
+ Image,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} = React;
+
+class JSFrameRate extends React.Component {
+ constructor(props: Object) {
+ super(props);
+ this.state = {
+ lastStart: Date.now(),
+ fps: 0,
+ frameTime: 0,
+ framesDropped: 0,
+ };
+ var update = () => {
+ var lastStart = Date.now();
+ this.setState((state) => {
+ var alpha = 0.9;
+ var frameTime = lastStart - state.lastStart;
+ return {
+ lastStart,
+ fps: alpha * state.fps + (1 - alpha) * 1000 / frameTime, // alpha smoothing
+ frameTime: Math.round(frameTime),
+ framesDropped: state.framesDropped + Math.max(0, Math.floor(frameTime / 17 - 0.5)),
+ };
+ });
+ requestAnimationFrame(update);
+ };
+ requestAnimationFrame(update);
+ }
+ render(): ReactElement {
+ return (
+ <Text>
+ JS fps: {Math.round(this.state.fps) + '\n'}
+ JS frame time: {this.state.frameTime + '\n'}
+ JS frames dropped: {this.state.framesDropped}
+ </Text>
+ );
+ }
+}
+
+class AnimatedOptimized extends React.Component {
+ constructor(props: Object) {
+ super(props);
+ this._goingDown = false;
+ this.state = {
+ timing: new Animated.Value(0),
+ spring: new Animated.Value(1),
+ busyWorkMS: 2,
+ };
+ }
+ render(): ReactElement {
+ var start = Date.now();
+ while (Date.now() < start + this.state.busyWorkMS) { } // block JS thread -> stutter
+ this._busyWorkRAF = window.requestAnimationFrame(() => this.setState({}));
+ return (
+ <View style={{margin: 10}}>
+ <Animated.Image
+ source={{uri: CHAIN_IMGS[2]}}
+ style={[styles.image, {
+ transform: [
+ // {scale: this.state.timing.interpolate({
+ // inputRange: [0, 100],
+ // outputRange: [1, 1.5],
+ // })},
+ {translateY: this.state.timing.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 2],
+ })},
+ {translateX: this.state.timing.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ })},
+ ],
+ left: 50,
+ }]}>
+ <Text>Native</Text>
+ </Animated.Image>
+ <Animated.Image
+ source={{uri: CHAIN_IMGS[2]}}
+ style={[styles.image, {
+ transform: [
+ // {scale: this.state.spring.interpolate({
+ // inputRange: [0, 100],
+ // outputRange: [1, 1.5],
+ // })},
+ {translateY: this.state.spring.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 2],
+ })},
+ {translateX: this.state.spring.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, -1],
+ })},
+ ],
+ left: 200,
+ }]}>
+ <Text>JavaScript</Text>
+ </Animated.Image>
+ <Text style={{marginTop: 80}}>
+ Busy Work Per Render (ms):
+ </Text>
+ <TextInput
+ value={'' + this.state.busyWorkMS}
+ style={{backgroundColor: 'white', paddingLeft: 4, margin: 4, height: 40}}
+ onChangeText={(busyWorkMS) => this.setState({busyWorkMS: Number(busyWorkMS)})}
+ />
+ <JSFrameRate />
+ </View>
+ );
+ }
+ componentDidMount() {
+ var duration = 2000;
+ this._upDownInterval = window.setInterval(() => {
+ this._goingDown = !this._goingDown;
+ var toValue = this._goingDown ? 150 : 0;
+ Animated.timing(this.state.timing, {
+ toValue,
+ duration,
+ easing: Easing.elastic(1.5),
+ enableOptimizations: true,
+ }).start();
+ Animated.timing(this.state.spring, {
+ toValue,
+ duration,
+ easing: Easing.elastic(1.5),
+ enableOptimizations: false,
+ }).start();
+ // Animated.spring(this.state.spring, {
+ // toValue,
+ // friction: 1,
+ // tension: -10, // extra soft spring (rebound/origami units are weird)
+ // }).start();
+ }, duration * 1.5);
+ }
+ componentWillUnmount() {
+ window.clearInterval(this._upDownInterval);
+ window.cancelAnimationFrame(this._busyWorkRAF);
+ }
+}
+
+var styles = StyleSheet.create({
+ image: {
+ position: 'absolute',
+ top: 220,
+ height: 120,
+ width: 120,
+ backgroundColor: 'transparent',
+ },
+});
+
+var CHAIN_IMGS = [
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/p160x160/10574705_1529175770666007_724328156_n.png',
+ 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851575_392309884199657_1917957497_n.png',
+ 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851567_555288911225630_1628791128_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/p160x160/851583_531111513625557_903469595_n.png',
+ 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpa1/t39.1997-6/p160x160/851577_510515972354399_2147096990_n.png',
+];
+
+AnimatedOptimized.title = 'Animated - Optimized';
+AnimatedOptimized.description = 'Demonstrates smooth animations while doing expensive ' +
+ 'operations in JS';
+
+module.exports = AnimatedOptimized;
diff --git a/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animated/Animated.js b/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animated/Animated.js
--- a/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animated/Animated.js
+++ b/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animated/Animated.js
@@ -23,20 +23,61 @@
var invariant = require('invariant');
var flattenStyle = require('flattenStyle');
+var precomputeStyle = require('precomputeStyle');
var requestAnimationFrame = require('requestAnimationFrame');
import type InterpolationConfigType from 'Interpolation';
type EndResult = {finished: bool};
type EndCallback = (result: EndResult) => void;
+type AnimSpec = {
+ value: number;
+ values?: Array<number>; // should this also support tuples for color?
+ duration?: number;
+ toValue?: number;
+ units?: string; // Is this the right way to handle deg/rad? What about rgba?
+ registerNativeAnimation?: (tag: number, values: Array<number>) => void;
+ completeNativeAnimation?: (tag: number, finalValue: number) => void;
+};
+
+function interpolateAnimSpec(
+ input: AnimSpec,
+ interpolator: (val: number) => number
+): AnimSpec {
+ var newSpec: AnimSpec = {
+ duration: input.duration,
+ value: interpolator(input.value),
+ registerNativeAnimation: input.registerNativeAnimation,
+ completeNativeAnimation: input.completeNativeAnimation,
+ };
+ if (input.values) {
+ newSpec.values = input.values.map(interpolator);
+ newSpec.toValue = interpolator(input.toValue || 0);
+ }
+ return newSpec;
+}
+
+function findClosestIndex(values: Array<number>, value: number) {
+ var threasholdHi = value + 0.0001;
+ var threasholdLo = value - 0.0001;
+ var checked = [];
+ for (var ii = 0; ii < values.length; ii++) {
+ checked.push(values[ii]);
+ if (values[ii] >= threasholdLo &&
+ values[ii] <= threasholdHi) {
+ return ii;
+ }
+ }
+ console.log('Could not find ' + value + '(>= ' + threasholdLo + ', <= ' + threasholdHi + ') in ', values, ' checked ', checked);
+}
// Note(vjeux): this would be better as an interface but flow doesn't
// support them yet
class Animated {
__attach(): void {}
__detach(): void {}
- __getValue(): any {}
- __getAnimatedValue(): any { return this.__getValue(); }
+ __getValue(): AnimSpec {}
+ __getAnimatedValue(): AnimSpec { return this.__getValue(); }
__addChild(child: Animated) {}
__removeChild(child: Animated) {}
__getChildren(): Array<Animated> { return []; }
@@ -135,6 +176,7 @@
easing?: (value: number) => number;
duration?: number;
delay?: number;
+ enableOptimizations?: bool; // Might run the animation via native API
};
var easeInOut = Easing.inOut(Easing.ease);
@@ -149,6 +191,10 @@
_onUpdate: (value: number) => void;
_animationFrame: any;
_timeout: any;
+ _enableOptimizations: bool;
+ _nativeAnimation: ?bool;
+ _values: ?Array<number>;
+ _animations: {[tag: number]: {values: Array<number>}};
constructor(
config: TimingAnimationConfig,
@@ -158,6 +204,9 @@
this._easing = config.easing || easeInOut;
this._duration = config.duration !== undefined ? config.duration : 500;
this._delay = config.delay || 0;
+ this._animations = {};
+ this._enableOptimizations = config.enableOptimizations === undefined ?
+ false : config.enableOptimizations;
}
start(
@@ -166,6 +215,7 @@
onEnd: ?EndCallback,
): void {
this.__active = true;
+ invariant(typeof fromValue === 'number', 'fromValue must be number, got ' + JSON.stringify(fromValue));
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
@@ -175,8 +225,48 @@
this._onUpdate(this._toValue);
this.__debouncedOnEnd({finished: true});
} else {
- this._startTime = Date.now();
- this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
+ if (this._enableOptimizations &&
+ RCTAnimationManager &&
+ typeof this._toValue === 'number') {
+ var startTime = Date.now();
+ var values = [];
+ var FPS = 60;
+ var numFrames = this._duration * FPS / 1000;
+ for (var ii = 0; ii <= numFrames; ii++) {
+ values.push(this._fromValue +
+ this._easing(ii / FPS / this._duration * 1000) *
+ (this._toValue - this._fromValue)
+ );
+ }
+ this._values = values;
+ this._nativeAnimation = true;
+ this._onUpdate({
+ value: this._fromValue,
+ values,
+ duration: this._duration,
+ toValue: this._toValue,
+ registerNativeAnimation: (tag: number, values: Array<number>) => {
+ this._animations[tag] = {values};
+ },
+ completeNativeAnimation: (tag: number, finalValue: number) => {
+ // TODO: only do this once?
+ var stopIndex = findClosestIndex(
+ this._animations[tag].values,
+ finalValue
+ );
+ this._currentValue = this._values[stopIndex];
+ console.log('completeAnim ', {tag, finalValue, convertedVal: this._currentValue});
+ delete this._animations[tag];
+ this._onUpdate({value: this._currentValue});
+ }
+ });
+ console.log('computed ' + numFrames + ' frames in ' + (Date.now() - startTime) + ' ms');
+ } else {
+ console.log('not using RCTAnimationManager');
+ !RCTAnimationManager && console.log(' RCTAnimationManager not available!');
+ this._startTime = Date.now();
+ this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
+ }
}
};
if (this._delay) {
@@ -212,9 +302,40 @@
stop(): void {
this.__active = false;
- clearTimeout(this._timeout);
- window.cancelAnimationFrame(this._animationFrame);
- this.__debouncedOnEnd({finished: false});
+ if (this._nativeAnimation) {
+ // stop the native animation and call the callback with the final value
+ var sourceValue;
+ var values;
+ for (var tag in this._animations) {
+ var values = this._animations[tag].values;
+ console.log('stop anim with tag', tag);
+ RCTAnimationManager.stopAnimation(Number(tag), (value) => {
+ if (sourceValue === undefined) {
+ var stopIndex = findClosestIndex(values, value);
+ sourceValue = this._values[stopIndex];
+ this._currentValue = sourceValue;
+ this._onUpdate({value: this._currentValue});
+ console.log('stopped animation and got ', {value, stopIndex, sourceValue});
+ callback && callback(sourceValue);
+ }
+ }, (e) => { console.warn(e); });
+ }
+ if (values === undefined && callback) {
+ // All animations already finished, so just return the final value.
+ console.log('no native anims to stop, calling callback with currentValue ' + this._currentValue);
+ invariant(
+ !isNaN(this._currentValue),
+ 'currentValue not set by animation completion or other means'
+ );
+ this._onUpdate({value: this._currentValue});
+ callback(this._currentValue);
+ }
+ } else {
+ clearTimeout(this._timeout);
+ window.cancelAnimationFrame(this._animationFrame);
+ this.__debouncedOnEnd({finished: false});
+ callback && callback(this._currentValue);
+ }
}
}
@@ -483,7 +604,7 @@
if (isOvershooting || (isVelocity && isDisplacement)) {
if (this._tension !== 0) {
// Ensure that we end up with a round value
- this._onUpdate(this._toValue);
+ this._onUpdate({value: this._toValue});
}
this.__debouncedOnEnd({finished: true});
@@ -560,20 +681,27 @@
animate(animation: Animation, callback: ?EndCallback): void {
var handle = InteractionManager.createInteractionHandle();
var previousAnimation = this._animation;
- this._animation && this._animation.stop();
- this._animation = animation;
- animation.start(
- this._value,
- (value) => {
- this._updateValue(value);
- },
- (result) => {
- this._animation = null;
- InteractionManager.clearInteractionHandle(handle);
- callback && callback(result);
- },
- previousAnimation,
- );
+ var start = (endValue: number) => {
+ this._animation = animation;
+ // console.log('starting anim with value ', this._value);
+ animation.start(
+ endValue || this._value.value,
+ (value) => {
+ this._updateValue(value);
+ },
+ (result) => {
+ this._animation = null;
+ InteractionManager.clearInteractionHandle(handle);
+ callback && callback(result);
+ },
+ previousAnimation,
+ );
+ };
+ if (this._animation) {
+ this._animation.stop(start);
+ } else {
+ start(this._value.value);
+ }
}
stopAnimation(callback?: ?(value: number) => void): void {
@@ -698,17 +826,15 @@
constructor(parent: Animated, interpolation: (input: number) => number | string) {
super();
+ invariant(parent, 'no parent!');
this._parent = parent;
this._interpolation = interpolation;
}
- __getValue(): number | string {
- var parentValue: number = this._parent.__getValue();
- invariant(
- typeof parentValue === 'number',
- 'Cannot interpolate an input which is not a number.'
- );
- return this._interpolation(parentValue);
+ __getValue(): AnimSpec {
+ var val = this._parent.__getValue();
+ invariant(!isNaN(val.value), 'nan...');
+ return interpolateAnimSpec(this._parent.__getValue(), this._interpolation);
}
interpolate(config: InterpolationConfigType): AnimatedInterpolation {
@@ -738,8 +864,16 @@
for (var key in transform) {
var value = transform[key];
if (value instanceof Animated) {
- result[key] = value.__getValue();
+ var rawValue = value.__getValue();
+ if (rawValue.values) {
+ invariant(!isNaN(rawValue.toValue), 'NaN...');
+ result[key] = rawValue.toValue;//values[0];
+ } else {
+ invariant(!isNaN(rawValue.value), 'NaN...');
+ result[key] = rawValue.value;
+ }
} else {
+ invariant(!isNaN(rawValue.value), 'NaN...');
result[key] = value;
}
}
@@ -748,12 +882,105 @@
}
__getAnimatedValue(): Array<Object> {
- return this._transforms.map(transform => {
+ /*
+ // New thing that does one animation for the whole transform.
+ // Iterates through transform components and extracts values,
+ // then converts into unified matrices, one for each frame.
+ var maxValueCount = 1;
+ var plainTransform = [];
+ this._transforms.forEach((prop) => {
+ for (var transformKey in prop) {
+ // Record longest value array so we can expand others to
+ // match.
+ var val = prop[transformKey];
+ if (val instanceof Animated) {
+ var val = val.__getAnimatedValue();
+ }
+ var extractedVal;
+ if (typeof val === 'object') { // AnimSpec
+ if (Array.isArray(val.values)) {
+ if (val.values.length > maxValueCount) {
+ maxValueCount = val.values.length;
+ }
+ extractedVal = val.values[0]; // single length anim....
+ } else {
+ extractedVal = val.value;
+ }
+ } else {
+ extractedVal = val;
+ }
+ invariant(extractedVal !== undefined, 'Got a weird val %s', JSON.stringify(Object.keys(val)));
+ var component = {};
+ component[transformKey] = extractedVal;
+ plainTransform.push(component);
+ }
+ });
+ if (maxValueCount === 1) {
+ return plainTransform; // nothing actually animated
+ }
+ // TODO: factor into function mergeTransformAnimations
+ var styles = [];
+ this._transforms.forEach((prop) => {
+ for (var transformKey in prop) { // should be loop of one...
+ var val = prop[transformKey];
+ if (val instanceof Animated) {
+ var val = val.__getAnimatedValue();
+ }
+ var lastVal;
+ for (var idx = 0; idx < maxValueCount; idx++) {
+ if (typeof val === 'object') {
+ if (Array.isArray(val.values)) { // pre-computed animation frames
+ // TODO: handle case where old anim is already running
+ // on one component.
+ if (val.values.length > idx) {
+ lastVal = val.values[idx];
+ }
+ } else {
+ invariant(val.value !== undefined, 'must be AnimSpec');
+ lastVal = val.value;
+ }
+ } else {
+ lastVal = val;
+ }
+ var style = styles[idx] || {transform: []};
+ var component = {};
+ component[transformKey] = lastVal;
+ style.transform.push(component);
+ styles[idx] = style;
+ }
+ }
+ });
+ var anim: AnimSpec = {
+ duration: 0,
+ values: [],
+ property: 'transformMatrix',
+ registerNativeAnimation: () => null,
+ completeNativeAnimation: () => null,
+ };
+ styles.forEach((style) => {
+ anim.values.push(precomputeStyle(style).transformMatrix);
+ console.log({style, transformMatrix: anim.values[0]});
+ });
+ return [{transformMatrix: anim}];
+ */
+ var nativeAnimations = {};
+ var transforms = this._transforms.map(transform => {
var result = {};
for (var key in transform) {
var value = transform[key];
if (value instanceof Animated) {
- result[key] = value.__getAnimatedValue();
+ var animValue = value.__getAnimatedValue();
+ if (typeof animValue === 'object') {
+ if (Array.isArray(animValue.values)) {
+ nativeAnimations[key] = true;
+ }
+ result[key] = {
+ ...animValue,
+ property: key,
+ };
+ } else {
+ result[key] = animValue;
+ }
} else {
// All transform components needed to recompose matrix
result[key] = value;
@@ -761,6 +988,8 @@
}
return result;
});
+ checkTransformOkForNative(nativeAnimations);
+ return transforms;
}
__attach(): void {
@@ -940,8 +1169,55 @@
// forceUpdate.
var callback = () => {
if (this.refs[refName].setNativeProps) {
- var value = this._propsAnimated.__getAnimatedValue();
- this.refs[refName].setNativeProps(value);
+ var nodeHandle = React.findNodeHandle(this.refs[refName]);
+ var props = this._propsAnimated.__getAnimatedValue();
+ var extractAndRunNativeAnimations = (subProps) => {
+ var nonAnim = null;
+ if (Array.isArray(subProps)) { // pretty much just transforms
+ nonAnim = [];
+ subProps.forEach((prop) => {
+ var res = extractAndRunNativeAnimations(prop);
+ if (res) {
+ nonAnim.push(res);
+ }
+ });
+ } else if (typeof subProps === 'object') {
+ if (Array.isArray(subProps.values)) { // pre-computed animation frames
+ var anim = subProps;
+ var tag: number = AnimationUtils.allocateTag();
+ anim.registerNativeAnimation(tag, anim.values);
+ console.log('using native animation system with prop ' +
+ anim.property + ', start value ' + anim.values[0] +
+ ' and end value ' + anim.values[anim.values.length - 1]
+ );
+ RCTAnimationManager.startSimpleAnimation(
+ nodeHandle,
+ tag,
+ anim.duration,
+ anim.values,
+ anim.property,
+ (finished, finalValue) => {
+ if (finished) {
+ anim.completeNativeAnimation(tag, finalValue);
+ this.forceUpdate();
+ }
+ anim.callback && anim.callback({finished, finalValue});
+ },
+ );
+ } else {
+ nonAnim = {};
+ for (var key in subProps) {
+ // TODO: Maybe if key === 'transform' ...
+ nonAnim[key] = extractAndRunNativeAnimations(subProps[key]);
+ }
+ }
+ } else {
+ nonAnim = subProps;
+ }
+ return nonAnim;
+ };
+ var nonAnimatedProps = extractAndRunNativeAnimations(props);
+ this.refs[refName].setNativeProps(nonAnimatedProps);
} else {
this.forceUpdate();
}
@@ -1287,6 +1563,24 @@
};
};
+function checkTransformOkForNative(nativeAnimations) {
+ var nativeTranslate = nativeAnimations.translate ||
+ nativeAnimations.translateX ||
+ nativeAnimations.translateY ? 1 : 0;
+ var nativeRotate = nativeAnimations.rotate ||
+ nativeAnimations.rotateX ||
+ nativeAnimations.rotateY ||
+ nativeAnimations.rotateZ ? 1 : 0;
+ var nativeScale = nativeAnimations.scale ||
+ nativeAnimations.scaleX ||
+ nativeAnimations.scaleY ? 1 : 0;
+ invariant(
+ nativeScale + nativeTranslate + nativeRotate <= 1,
+ 'Can only do native animations with one transform type at a time, got: %s',
+ Object.keys(nativeAnimations)
+ );
+}
+
module.exports = {
delay,
sequence,
diff --git a/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animation/RCTAnimationExperimentalManager.m b/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animation/RCTAnimationExperimentalManager.m
new file mode 100644
--- /dev/null
+++ b/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/Animation/RCTAnimationExperimentalManager.m
@@ -0,0 +1,398 @@
+/**
+ * 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 "RCTAnimationExperimentalManager.h"
+
+#import <UIKit/UIKit.h>
+
+#import "RCTSparseArray.h"
+#import "RCTUIManager.h"
+#import "RCTUtils.h"
+
+#if CGFLOAT_IS_DOUBLE
+ #define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_D
+#else
+ #define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_F
+#endif
+
+@implementation RCTAnimationExperimentalManager
+{
+ RCTSparseArray *_animationRegistry; // Main thread only; animation tag -> view tag
+ RCTSparseArray *_callbackRegistry; // Main thread only; animation tag -> callback
+ NSDictionary *_keypathMapping;
+}
+
+RCT_EXPORT_MODULE()
+
+@synthesize bridge = _bridge;
+
+- (instancetype)init
+{
+ 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",
+ },
+ @"translateX": @{
+ @"keypath": @"transform.translation.x",
+ @"type": @"NSNumber",
+ },
+ @"translateY": @{
+ @"keypath": @"transform.translation.y",
+ @"type": @"NSNumber",
+ },
+ @"rotate": @{
+ @"keypath": @"transform.rotation.z",
+ @"type": @"NSNumber",
+ },
+ @"rotateX": @{
+ @"keypath": @"transform.rotation.x",
+ @"type": @"NSNumber",
+ },
+ @"rotateY": @{
+ @"keypath": @"transform.rotation.y",
+ @"type": @"NSNumber",
+ },
+ @"scale": @{
+ @"keypath": @"transform.scale",
+ @"type": @"NSNumber",
+ },
+ @"scaleXY": @{
+ @"keypath": @"transform.scale",
+ @"type": @"CGPoint",
+ },
+ @"transformMatrix": @{
+ @"keypath": @"transform",
+ @"type": @"NSArray",
+ },
+ };
+ }
+
+ return self;
+}
+
+- (dispatch_queue_t)methodQueue
+{
+ return _bridge.uiManager.methodQueue;
+}
+
+- (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName
+{
+ if (count == 1) {
+ CGFloat from = *fromArray, to = *toArray, delta = to - from;
+ return ^(CGFloat t) {
+ return @(from + t * delta);
+ };
+ }
+
+ CG_APPEND(vDSP_vsub,,D)(fromArray, 1, toArray, 1, toArray, 1, count);
+
+ const size_t size = count * sizeof(CGFloat);
+ NSData *deltaData = [NSData dataWithBytes:toArray length:size];
+ NSData *fromData = [NSData dataWithBytes:fromArray length:size];
+
+ return ^(CGFloat t) {
+ const CGFloat *delta = deltaData.bytes;
+ const CGFloat *_fromArray = fromData.bytes;
+
+ CGFloat value[count];
+ CG_APPEND(vDSP_vma,,D)(delta, 1, &t, 0, _fromArray, 1, value, 1, count);
+ return [NSValue valueWithBytes:value objCType:typeName];
+ };
+}
+
+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(startSimpleAnimation:(nonnull NSNumber *)reactTag
+ animationTag:(nonnull NSNumber *)animationTag
+ duration:(NSTimeInterval)duration
+ values:(NSNumberArray *)values
+ property:(NSString *)property
+ callback:(RCTResponseSenderBlock)callback)
+{
+ __weak RCTAnimationExperimentalManager *weakSelf = self;
+ [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ RCTAnimationExperimentalManager *strongSelf = weakSelf;
+ UIView *view = viewRegistry[reactTag];
+ if (!view) {
+ RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag);
+ return;
+ }
+ NSString *keypath = _keypathMapping[property][@"keypath"];
+ __block BOOL completionBlockSet = NO;
+ [CATransaction begin];
+ CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:keypath];
+ animation.beginTime = CACurrentMediaTime();
+ animation.duration = duration;
+ animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
+ animation.calculationMode = kCAAnimationDiscrete;
+ animation.values = values;
+ @try {
+// [view.layer setValue:values.lastObject forKey:keypath];
+ NSString *animationKey = [@"RCT" stringByAppendingString:RCTJSONStringify(@{@"tag": animationTag, @"key": keypath}, nil)];
+ if (!completionBlockSet) {
+ strongSelf->_callbackRegistry[animationTag] = callback;
+ [CATransaction setCompletionBlock:^{
+ if (strongSelf->_animationRegistry[animationTag] == nil) {
+ return; // animation was stopped;
+ }
+ RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
+// [view.layer setValue:values.lastObject forKey:keypath];
+ [view.layer setValue:values.lastObject forKeyPath:keypath]; // is this right?
+ if (cb) {
+ cb(@[@YES, values.lastObject]);
+ strongSelf->_callbackRegistry[animationTag] = nil;
+ }
+ strongSelf->_animationRegistry[animationTag] = nil;
+ }];
+ completionBlockSet = YES;
+ }
+ [view.layer addAnimation:animation forKey:animationKey];
+ }
+ @catch (NSException *exception) {
+ return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, values.lastObject);
+ }
+ [CATransaction commit];
+ NSLog(@"Commit catransaction for keypath %@", keypath);
+ strongSelf->_animationRegistry[animationTag] = reactTag;
+ }];
+}
+
+RCT_EXPORT_METHOD(stopAnimation:(nonnull NSNumber *)animationTag
+ successCallback:(RCTResponseSenderBlock)successCallback
+ errorCallback:(RCTResponseSenderBlock)errorCallback)
+{
+ __weak RCTAnimationExperimentalManager *weakSelf = self;
+ [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ RCTAnimationExperimentalManager *strongSelf = weakSelf;
+
+ NSNumber *reactTag = strongSelf->_animationRegistry[animationTag];
+ if (!reactTag) {
+ if (errorCallback) {
+ errorCallback(@[@"Tag not registered for animation"]);
+ }
+ return;
+ }
+
+ id lastValue = nil;
+ UIView *view = viewRegistry[reactTag];
+ for (NSString *animationKey in view.layer.animationKeys) {
+ if ([animationKey hasPrefix:@"RCT{"]) {
+ NSDictionary *data = RCTJSONParse([animationKey substringFromIndex:3], nil);
+ if (animationTag.integerValue == [data[@"tag"] integerValue]) {
+ lastValue = [[view.layer presentationLayer] valueForKeyPath:data[@"key"]];
+// [view.layer setValue:lastValue forKey:data[@"key"]];
+ [view.layer setValue:lastValue forKeyPath:data[@"key"]];
+ [view.layer removeAnimationForKey:animationKey];
+ break;
+ }
+ }
+ }
+ RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
+ if (cb) {
+ cb(@[@NO, lastValue]);
+ strongSelf->_callbackRegistry[animationTag] = nil;
+ }
+ strongSelf->_animationRegistry[animationTag] = nil;
+ if (lastValue != nil) {
+ if (successCallback) {
+ successCallback(@[lastValue]);
+ }
+ } else {
+ if (errorCallback) {
+ errorCallback(@[RCTErrorWithMessage(@"Internal Error: Failed to find animation with tag")]);
+ }
+ }
+ }];
+}
+
+RCT_EXPORT_METHOD(startAnimation:(NSNumber *)reactTag
+ animationTag:(NSNumber *)animationTag
+ duration:(NSTimeInterval)duration
+ delay:(NSTimeInterval)delay
+ easingSample:(NSNumberArray *)easingSample
+ properties:(NSDictionary *)properties
+ callback:(RCTResponseSenderBlock)callback)
+{
+ __weak RCTAnimationExperimentalManager *weakSelf = self;
+ [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ RCTAnimationExperimentalManager *strongSelf = weakSelf;
+
+ UIView *view = viewRegistry[reactTag];
+ if (!view) {
+ RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag);
+ return;
+ }
+ __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 ([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:
+ if (![obj respondsToSelector:@selector(objectForKeyedSubscript:)] || obj[@"x"]) {
+ toValue = [NSValue valueWithCGPoint:[RCTConvert CGPoint:obj]];
+ } else {
+ toValue = [NSValue valueWithCGSize:[RCTConvert CGSize:obj]];
+ }
+ break;
+ case 4:
+ toValue = [NSValue valueWithCGRect:[RCTConvert CGRect:obj]];
+ break;
+ 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;
+ }
+ const char *typeName = toValue.objCType;
+
+ size_t count;
+ switch (typeName[0]) {
+ case 'i':
+ case 'I':
+ case 's':
+ case 'S':
+ case 'l':
+ case 'L':
+ case 'q':
+ case 'Q':
+ count = 1;
+ break;
+
+ default: {
+ NSUInteger size;
+ NSGetSizeAndAlignment(typeName, &size, NULL);
+ count = size / sizeof(CGFloat);
+ break;
+ }
+ }
+
+ CGFloat toFields[count];
+
+ switch (typeName[0]) {
+#define CASE(encoding, type) \
+case encoding: { \
+type value; \
+[toValue getValue:&value]; \
+toFields[0] = value; \
+break; \
+}
+
+ CASE('i', int)
+ CASE('I', unsigned int)
+ CASE('s', short)
+ CASE('S', unsigned short)
+ CASE('l', long)
+ CASE('L', unsigned long)
+ CASE('q', long long)
+ CASE('Q', unsigned long long)
+
+#undef CASE
+
+ default:
+ [toValue getValue:toFields];
+ break;
+ }
+
+ NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:keypath];
+#if !CGFLOAT_IS_DOUBLE
+ if ([fromValue isKindOfClass:[NSNumber class]]) {
+ fromValue = @([(NSNumber *)fromValue doubleValue]);
+ }
+#endif
+ CGFloat fromFields[count];
+ [fromValue getValue:fromFields];
+
+ id (^interpolationBlock)(CGFloat t) = [strongSelf interpolateFrom:fromFields to:toFields count:count typeName:typeName];
+
+ NSMutableArray *sampledValues = [NSMutableArray arrayWithCapacity:easingSample.count];
+ for (NSNumber *sample in easingSample) {
+ CGFloat t = sample.CG_APPEND(, floatValue, doubleValue);
+ [sampledValues addObject:interpolationBlock(t)];
+ }
+ CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:keypath];
+ animation.beginTime = CACurrentMediaTime() + delay;
+ animation.duration = duration;
+ animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
+ animation.values = sampledValues;
+ @try {
+ [view.layer setValue:toValue forKey:keypath];
+ NSString *animationKey = [@"RCT" stringByAppendingString:RCTJSONStringify(@{@"tag": animationTag, @"key": keypath}, nil)];
+ if (!completionBlockSet) {
+ strongSelf->_callbackRegistry[animationTag] = callback;
+ [CATransaction setCompletionBlock:^{
+ RCTResponseSenderBlock cb = strongSelf->_callbackRegistry[animationTag];
+ if (cb) {
+ cb(@[@YES, toValue]);
+ strongSelf->_callbackRegistry[animationTag] = nil;
+ strongSelf->_animationRegistry[animationTag] = nil;
+ }
+ }];
+ completionBlockSet = YES;
+ }
+ [view.layer addAnimation:animation forKey:animationKey];
+ }
+ @catch (NSException *exception) {
+ return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, toValue);
+ }
+ }
+ [CATransaction commit];
+ strongSelf->_animationRegistry[animationTag] = reactTag;
+ }];
+}
+
+- (NSDictionary *)constantsToExport
+{
+ return @{@"Properties": [_keypathMapping allKeys] };
+}
+
+@end
diff --git a/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/StyleSheet/precomputeStyle.js b/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/StyleSheet/precomputeStyle.js
--- a/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/StyleSheet/precomputeStyle.js
+++ b/fbobjc/Libraries/FBReactKit/js/react-native-github/Libraries/StyleSheet/precomputeStyle.js
@@ -105,6 +105,7 @@
decomposedMatrix: MatrixMath.decomposeMatrix(result),
};
}
+ invariant(result, 'Tried to set falsey transformMatrix');
return {
...style,
transformMatrix: result,
@@ -131,12 +132,13 @@
*/
function _convertToRadians(value: string): number {
var floatValue = parseFloat(value, 10);
+ invariant(typeof floatValue === 'number', 'Failed to parse ' + value);
return value.indexOf('rad') > -1 ? floatValue : floatValue * Math.PI / 180;
}
function _validateTransform(key, value, transformation) {
invariant(
- !value.getValue,
+ !value.__getValue,
'You passed an Animated.Value to a normal component. ' +
'You need to wrap that component in an Animated. For example, ' +
'replace <View /> by <Animated.View />.'
@@ -185,9 +187,10 @@
break;
default:
invariant(
- typeof value === 'number',
- 'Transform with key of "%s" must be a number: %s',
+ typeof value === 'number' && !isNaN(value),
+ 'Transform with key of "%s" must be a number, but got: %s (%s)',
key,
+ value,
stringifySafe(transformation),
);
}
diff --git a/fbobjc/Libraries/FBReactKit/js/react-native-github/React/Base/RCTLog.h b/fbobjc/Libraries/FBReactKit/js/react-native-github/React/Base/RCTLog.h
--- a/fbobjc/Libraries/FBReactKit/js/react-native-github/React/Base/RCTLog.h
+++ b/fbobjc/Libraries/FBReactKit/js/react-native-github/React/Base/RCTLog.h
@@ -17,7 +17,7 @@
* You can override these values when debugging in order to tweak the default
* logging behavior.
*/
-#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix
+#define RCTLOG_FATAL_LEVEL RCTLogLevelError
#define RCTLOG_REDBOX_LEVEL RCTLogLevelError
/**
@godness84
Copy link

Any news about offloading animations to the main thread on the native side? Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment