forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Execute Animated.js declarative animation on UIThread on Android.
Summary:This is the first from the series of PRs I'm going to be sending shorty that would let Animated.js animations to run off the JS thread (for Android only). This PR introduce a new native module that will be used for offloading animations - NativeAnimatedModule. It has a simple API that allows for animated nodes management via methods like: create/drop animated node, connect/disconnect nodes, start animation of a value node, attach/detach animated from a native view. Similarly to how we handle UIManager view hierarchy updates we create a queue of animated graph operations that are then executed on the UI thread. This isolates us from problems that may be caused by concurrent updates of animated graph while UI thread is "executing" the animation. The most important class NativeAnimatedNodesManager.java implements a management interface for animated nodes graph as well as implements a graph traversal algorithm that is run for each animation frame. For each animation frame we visit animated nodes th Closes facebook#6466 Differential Revision: D3092739 Pulled By: astreet fb-gh-sync-id: 665b49900b7367c91a93b9d8864f78fb90bb36ba shipit-source-id: 665b49900b7367c91a93b9d8864f78fb90bb36ba
- Loading branch information
Showing
14 changed files
with
1,290 additions
and
9 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/** | ||
* 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 NativeAnimatedHelper | ||
* @flow | ||
*/ | ||
'use strict'; | ||
|
||
var NativeAnimatedModule = require('NativeModules').NativeAnimatedModule; | ||
|
||
var invariant = require('fbjs/lib/invariant'); | ||
|
||
var __nativeAnimatedNodeTagCount = 1; /* used for animated nodes */ | ||
var __nativeAnimationTagCount = 1; /* used for started animations */ | ||
|
||
type EndResult = {finished: bool}; | ||
type EndCallback = (result: EndResult) => void; | ||
|
||
/** | ||
* Simple wrappers around NativeANimatedModule to provide flow and autocmplete support for | ||
* the native module methods | ||
*/ | ||
var API = { | ||
createAnimatedNode: function(tag: number, config: Object): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.createAnimatedNode(tag, config); | ||
}, | ||
connectAnimatedNodes: function(parentTag: number, childTag: number): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag); | ||
}, | ||
disconnectAnimatedNodes: function(parentTag: number, childTag: number): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.disconnectAnimatedNodes(parentTag, childTag); | ||
}, | ||
startAnimatingNode: function(animationTag: number, nodeTag: number, config: Object, endCallback: EndCallback) { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.startAnimatingNode(nodeTag, config, endCallback); | ||
}, | ||
setAnimatedNodeValue: function(nodeTag: number, value: number): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.setAnimatedNodeValue(nodeTag, value); | ||
}, | ||
connectAnimatedNodeToView: function(nodeTag: number, viewTag: number): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.connectAnimatedNodeToView(nodeTag, viewTag); | ||
}, | ||
disconnectAnimatedNodeFromView: function(nodeTag: number, viewTag: number): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.disconnectAnimatedNodeFromView(nodeTag, viewTag); | ||
}, | ||
dropAnimatedNode: function(tag: number): void { | ||
assertNativeAnimatedModule(); | ||
NativeAnimatedModule.dropAnimatedNode(tag); | ||
}, | ||
}; | ||
|
||
/** | ||
* Properties allowed by the native animated implementation. | ||
* | ||
* In general native animated implementation should support any numeric property that doesn't need | ||
* to be updated through the shadow view hierarchy (all non-layout properties). This list is limited | ||
* to the properties that will perform best when animated off the JS thread. | ||
*/ | ||
var PROPS_WHITELIST = { | ||
style: { | ||
opacity: true, | ||
|
||
/* legacy android transform properties */ | ||
scaleX: true, | ||
scaleY: true, | ||
rotation: true, | ||
translateX: true, | ||
translateY: true, | ||
}, | ||
}; | ||
|
||
function validateProps(params: Object): void { | ||
for (var key in params) { | ||
if (!PROPS_WHITELIST.hasOwnProperty(key)) { | ||
throw new Error(`Property '${key}' is not supported by native animated module`); | ||
} | ||
} | ||
} | ||
|
||
function validateStyles(styles: Object): void { | ||
var STYLES_WHITELIST = PROPS_WHITELIST.style || {}; | ||
for (var key in styles) { | ||
if (!STYLES_WHITELIST.hasOwnProperty(key)) { | ||
throw new Error(`Style property '${key}' is not supported by native animated module`); | ||
} | ||
} | ||
} | ||
|
||
function generateNewNodeTag(): number { | ||
return __nativeAnimatedNodeTagCount++; | ||
} | ||
|
||
function generateNewAnimationTag(): number { | ||
return __nativeAnimationTagCount++; | ||
} | ||
|
||
function assertNativeAnimatedModule(): void { | ||
invariant(NativeAnimatedModule, 'Native animated module is not available'); | ||
} | ||
|
||
module.exports = { | ||
API, | ||
validateProps, | ||
validateStyles, | ||
generateNewNodeTag, | ||
generateNewAnimationTag, | ||
assertNativeAnimatedModule, | ||
}; |
71 changes: 71 additions & 0 deletions
71
ReactAndroid/src/main/java/com/facebook/react/animated/AnimatedNode.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* 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. | ||
*/ | ||
|
||
package com.facebook.react.animated; | ||
|
||
import com.facebook.infer.annotation.Assertions; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import javax.annotation.Nullable; | ||
|
||
/** | ||
* Base class for all Animated.js library node types that can be created on the "native" side. | ||
*/ | ||
/*package*/ abstract class AnimatedNode { | ||
|
||
public static final int INITIAL_BFS_COLOR = 0; | ||
|
||
private static final int DEFAULT_ANIMATED_NODE_CHILD_COUNT = 1; | ||
|
||
/*package*/ @Nullable List<AnimatedNode> mChildren; /* lazy-initialized when a child is added */ | ||
/*package*/ int mActiveIncomingNodes = 0; | ||
/*package*/ int mBFSColor = INITIAL_BFS_COLOR; | ||
/*package*/ int mTag = -1; | ||
|
||
public final void addChild(AnimatedNode child) { | ||
if (mChildren == null) { | ||
mChildren = new ArrayList<>(DEFAULT_ANIMATED_NODE_CHILD_COUNT); | ||
} | ||
Assertions.assertNotNull(mChildren).add(child); | ||
child.onAttachedToNode(this); | ||
} | ||
|
||
public final void removeChild(AnimatedNode child) { | ||
if (mChildren == null) { | ||
return; | ||
} | ||
child.onDetachedFromNode(this); | ||
mChildren.remove(child); | ||
} | ||
|
||
/** | ||
* Subclasses may want to override this method in order to store a reference to the parent of a | ||
* given node that can then be used to calculate current node's value in {@link #update}. | ||
* In that case it is important to also override {@link #onDetachedFromNode} to clear that | ||
* reference once current node gets detached. | ||
*/ | ||
public void onAttachedToNode(AnimatedNode parent) { | ||
} | ||
|
||
/** | ||
* See {@link #onAttachedToNode} | ||
*/ | ||
public void onDetachedFromNode(AnimatedNode parent) { | ||
} | ||
|
||
/** | ||
* This method will be run on each node at most once every repetition of the animation loop. It | ||
* will be executed on a node only when all the node's parent has already been updated. Therefore | ||
* it can be used to calculate node's value. | ||
*/ | ||
public void update() { | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/** | ||
* 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. | ||
*/ | ||
|
||
package com.facebook.react.animated; | ||
|
||
import com.facebook.react.bridge.Callback; | ||
|
||
/** | ||
* Base class for different types of animation drivers. Can be used to implement simple time-based | ||
* animations as well as spring based animations. | ||
*/ | ||
/*package*/ abstract class AnimationDriver { | ||
|
||
boolean mHasFinished = false; | ||
ValueAnimatedNode mAnimatedValue; | ||
Callback mEndCallback; | ||
|
||
/** | ||
* This method gets called in the main animation loop with a frame time passed down from the | ||
* android choreographer callback. | ||
*/ | ||
public abstract void runAnimationStep(long frameTimeNanos); | ||
} |
63 changes: 63 additions & 0 deletions
63
ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/** | ||
* 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. | ||
*/ | ||
|
||
package com.facebook.react.animated; | ||
|
||
import com.facebook.react.bridge.ReadableArray; | ||
import com.facebook.react.bridge.ReadableMap; | ||
|
||
/** | ||
* Implementation of {@link AnimationDriver} which provides a support for simple time-based | ||
* animations that are pre-calculate on the JS side. For each animation frame JS provides a value | ||
* from 0 to 1 that indicates a progress of the animation at that frame. | ||
*/ | ||
class FrameBasedAnimationDriver extends AnimationDriver { | ||
|
||
private long mStartFrameTimeNanos = -1; | ||
private final double[] mFrames; | ||
private final double mToValue; | ||
private double mFromValue; | ||
|
||
FrameBasedAnimationDriver(ReadableMap config) { | ||
ReadableArray frames = config.getArray("frames"); | ||
int numberOfFrames = frames.size(); | ||
mFrames = new double[numberOfFrames]; | ||
for (int i = 0; i < numberOfFrames; i++) { | ||
mFrames[i] = frames.getDouble(i); | ||
} | ||
mToValue = config.getDouble("toValue"); | ||
} | ||
|
||
@Override | ||
public void runAnimationStep(long frameTimeNanos) { | ||
if (mStartFrameTimeNanos < 0) { | ||
mStartFrameTimeNanos = frameTimeNanos; | ||
mFromValue = mAnimatedValue.mValue; | ||
} | ||
long timeFromStartNanos = (frameTimeNanos - mStartFrameTimeNanos); | ||
// frames are calculated at 60FPS, to get index by a given time offset from the start of the | ||
// animation, we take the time diff in millisecond and divide it by 60 frames per 1000ms. | ||
int frameIndex = (int) (timeFromStartNanos / 1000000L * 60L / 1000L); | ||
if (frameIndex < 0) { | ||
throw new IllegalStateException("Calculated frame index should never be lower than 0"); | ||
} else if (mHasFinished) { | ||
// nothing to do here | ||
return; | ||
} | ||
double nextValue; | ||
if (frameIndex >= mFrames.length - 1) { | ||
// animation has completed, no more frames left | ||
mHasFinished = true; | ||
nextValue = mToValue; | ||
} else { | ||
nextValue = mFromValue + mFrames[frameIndex] * (mToValue - mFromValue); | ||
} | ||
mAnimatedValue.mValue = nextValue; | ||
} | ||
} |
Oops, something went wrong.