New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execute Animated.js declarative animation on UIThread on Android. #6466

Closed
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
@kmagiera
Contributor

kmagiera commented Mar 15, 2016

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 that might've been updated as well as their children that may use parent's values to update themselves. At the end of the traversal algorithm we expect to reach a special type of the node: PropsAnimatedNode that is then responsible for calculating property map which can be sent to native view hierarchy to update the view.

From the "client" perspective the API is as simple as adding: useNativeDriver: true to the set of animation properties, e.g:

Animated.timing(
  this.state.fadeAnim,
  {
    toValue: 1,
    duration: 2000,
    useNativeDriver: true,
  },
).start();

This approach doesn't let us support animating layout properties as it updates the values directly on the native view hierarchy. In order to be able to handle updates of layout props we'd need to run the updates on shadow view hierarchy first (on "native" thread). However a lot of this code could be reused if we decided to implement support for layout props (my initial prototype was capable of updating layout props off the JS thread).

This PR adds support only for a limited number of Animated.js library features (mainly to keep it shorter), here is what's supported as of this PR:

  • Works only for android
  • Only one type of animation: Animated.timing
  • One can use animated values only for non-layout, numeric properties (e.g. support for transform property is disabled)
  • One can use JS driven version of Animated.js library along with "native" animated. The only limitation is that we cannot drive same value both by native and JS animated driver.

What's coming up next (have most of the work done but will be sending this later to keep this change as simple as possible):

  • More animation types: decay, spring
  • Support for transform property
  • Support for "operation" nodes: Animated.add, Animated.multiply, etc.
  • Animated.event support
  • Unit tests for the graph traversal algorithm

IMPORTANT: This PR doesn't enable animated native module by default. In order to use it you'd need to add NativeAnimatedModule to the list of yours app 'packages'

Test plan

  1. Add NativeAnimatedModule to the list of native modules for you app
  2. Add useNativeDriver: true to your animation config (Animated.timing only for now). You can use my example app: https://gist.github.com/kmagiera/bbfbac6c00077d188bc5
  3. Enable SPY_MODE in MessageQueue.js to see that setNativeProps operation are no longer passed over the bridge while animation is running and useNativeDriver is set to true.
@facebook-github-bot

This comment has been minimized.

Show comment
Hide comment
@facebook-github-bot

facebook-github-bot Mar 15, 2016

By analyzing the blame information on this pull request, we identified @sahrens, @mkonicek and @korDen to be potential reviewers.

facebook-github-bot commented Mar 15, 2016

By analyzing the blame information on this pull request, we identified @sahrens, @mkonicek and @korDen to be potential reviewers.

@kmagiera

This comment has been minimized.

Show comment
Hide comment
@kmagiera

kmagiera Mar 15, 2016

Contributor

cc: @astreet

Contributor

kmagiera commented Mar 15, 2016

cc: @astreet

@@ -187,7 +313,11 @@ class TimingAnimation extends Animation {

This comment has been minimized.

@eslint-bot

eslint-bot Mar 15, 2016

null This type is incompatible with AnimatedValue

@eslint-bot

eslint-bot Mar 15, 2016

null This type is incompatible with AnimatedValue

@@ -169,12 +279,28 @@ class TimingAnimation extends Animation {
this._duration = config.duration !== undefined ? config.duration : 500;
this._delay = config.delay !== undefined ? config.delay : 0;
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
this._useNativeDriver = !!config.useNativeDriver;
}

This comment has been minimized.

@eslint-bot

eslint-bot Mar 15, 2016

no-floating-decimal: A trailing decimal point can be confused with a dot.

@eslint-bot

eslint-bot Mar 15, 2016

no-floating-decimal: A trailing decimal point can be confused with a dot.

This comment has been minimized.

@kmagiera

kmagiera Mar 15, 2016

Contributor

wat?

@kmagiera

kmagiera Mar 15, 2016

Contributor

wat?

This comment has been minimized.

@vjeux

vjeux Mar 21, 2016

Contributor

I have no idea what's going on here, sorry for the spam :(

@vjeux

vjeux Mar 21, 2016

Contributor

I have no idea what's going on here, sorry for the spam :(

This comment has been minimized.

@kmagiera

kmagiera Mar 22, 2016

Contributor

looks like comments has been left on incorrect lines by the bot. I ended up running eslint locally to get the correct line numbers and fixed the issues cc @mkonicek

@kmagiera

kmagiera Mar 22, 2016

Contributor

looks like comments has been left on incorrect lines by the bot. I ended up running eslint locally to get the correct line numbers and fixed the issues cc @mkonicek

@mkonicek

This comment has been minimized.

Show comment
Hide comment
@mkonicek

mkonicek Mar 15, 2016

Contributor

Wow thanks Krzysztof for sending this! :) 👍 👍 👍

Contributor

mkonicek commented Mar 15, 2016

Wow thanks Krzysztof for sending this! :) 👍 👍 👍

@sahrens

This comment has been minimized.

Show comment
Hide comment
@sahrens

sahrens Mar 15, 2016

Contributor

It's happening!

Contributor

sahrens commented Mar 15, 2016

It's happening!

@facebook-github-bot

This comment has been minimized.

Show comment
Hide comment
@facebook-github-bot

facebook-github-bot Mar 15, 2016

@kmagiera updated the pull request.

facebook-github-bot commented Mar 15, 2016

@kmagiera updated the pull request.

@facebook-github-bot

This comment has been minimized.

Show comment
Hide comment
@facebook-github-bot

facebook-github-bot Mar 16, 2016

@kmagiera updated the pull request.

facebook-github-bot commented Mar 16, 2016

@kmagiera updated the pull request.

@kmagiera

This comment has been minimized.

Show comment
Hide comment
@kmagiera

kmagiera Mar 16, 2016

Contributor

I've fixed all the lint and flow errors in my last update

Contributor

kmagiera commented Mar 16, 2016

I've fixed all the lint and flow errors in my last update

@astreet

This comment has been minimized.

Show comment
Hide comment
@astreet

astreet Mar 16, 2016

Contributor

The general idea of running animations on the main thread by interacting with the NativeViewHierarchyManager sounds like the right approach here. Couple things:

  1. We have an alternate implementation of UIImplementation internally (called 'nodes' if you remember it) -- it extends UIImplementation and overrides things like handleUpdateView and handleCreateView. We also have another implementation of NativeViewHierarchyManager (that extends NativeViewHierarchyManager, adding some methods that the other UIImplementation uses). Just giving you context because hopefully we make something that just works for both (e.g., it shouldn't matter what the UI is implemented with since both implementations respect referencing tags and setting props)
  2. Can you explain why running animations on the main thread requires this node graph/resolution algorithm? Maybe an example would be helpful.

Also happy to talk on Skype or something instead of communicating over text :)

Contributor

astreet commented Mar 16, 2016

The general idea of running animations on the main thread by interacting with the NativeViewHierarchyManager sounds like the right approach here. Couple things:

  1. We have an alternate implementation of UIImplementation internally (called 'nodes' if you remember it) -- it extends UIImplementation and overrides things like handleUpdateView and handleCreateView. We also have another implementation of NativeViewHierarchyManager (that extends NativeViewHierarchyManager, adding some methods that the other UIImplementation uses). Just giving you context because hopefully we make something that just works for both (e.g., it shouldn't matter what the UI is implemented with since both implementations respect referencing tags and setting props)
  2. Can you explain why running animations on the main thread requires this node graph/resolution algorithm? Maybe an example would be helpful.

Also happy to talk on Skype or something instead of communicating over text :)

@kmagiera

This comment has been minimized.

Show comment
Hide comment
@kmagiera

kmagiera Mar 17, 2016

Contributor

Re 1) I responded inline

Re 2) this is just a mapping of what AnimatedImplementation.js is doing to drive animations in JS. I'll try to explain it in few words:

The animated js is based on a concept of a graph where nodes are values or transform operations (such as interpolation, addition, etc) and connection are used to describe how change of the value in one node can affect other nodes. Few examples:

  • Animated.Value is a simplest type of node with numeric value which can be driven by an animation engine (spring, decay, etc) or by calling setValue on it from JS
  • Animated.add is a type of node that may have two or more input nodes. It outputs the sum of all the input node values
  • interpolate - is actually a method you can call on any node and it creates a new node that takes the parent node as an input and outputs its interpolated value (e.g. if you have value that can animate from 0 to 1 you can create interpolated node and set output range to be 0 to 100 and when the input node changes the output of interpolated node will be multiplying the values by 100)

You can mix and chain nodes however you like and this way create nodes graph with connections between them.

To map animated node values to view properties there is a special type of a node: AnimatedProps. It is created by AnimatedImplementation whenever you render Animated.View and stores a mapping from the view properties to the corresponding animated values (so it's actually also a node with connections to the value nodes). Example:

var someAnimatedValue = new Animated.Value(0);

...

<Animated.View
  style={{opacity: someAnimatedValue}}>
  <Text>Stuff</Text>
</Animated.View>

The code from the above will generate 3 nodes:

  • AnimatedValue node with a value set to 0
  • AnimatedStyle node that stores a mapping from property with the name "opacity" to the value node mentioned above
  • AnimatedProps node with style property being mapped to style node from the above

Last "special" parts of the the graph are "animation drivers". Those are objects that based on some criteria updates attached values every frame (we have few types of those, e.g., spring, timing, decay). Animation objects can be "started" and "stopped". In JS they use setTimeout to run value update loop every frame. They are like "pulse generators" for the rest of the nodes graph. Those pulses then propagate along the graph from nodes to their children up to the AnimatedProps nodes. Example:

Animated.timing(
  thatAnimatedValueFromThePreviousSnippet,
  {
    toValue: 1,
    duration: 500,
  },
 ).start(); 

The code above will create an object that will be responsible for updating thatAnimatedValueFromThePreviousSnippet animated value from their current state at the moment start is called (that would be 0) to the value specified in the animation config (toValue: 1) in a linear manner every frame for 500 milliseconds (duration: 500)

The process of calculating view updates during animation works as follows (I'm describing the process from the perspective of Animated JS and how it runs in javascript, but it works very similar in native):

  1. You run all the active animations that may update attached animated value nodes
  2. For each updated node you visit their children nodes as they may also require an update
  3. Once you reach AnimatedProps node you can collect properties which needs to be updated, then we send setNativeProps to the corresponding view

So in order to calculate property updates you need to now how is the value generated by the animation driver (e.g. spring) processed before it gets transformed into a value that can be set as a property on a view. This is why I'm using the simplest possible approach and just send and maintain that animated nodes graph in native. There are definitely other ways of handling that, but I believe this one is the most flexible method as it naturally reflects the public animated.js API. It is also future proof as far as the API isn't going to change dramatically and only new type of nodes are going to be added.

Contributor

kmagiera commented Mar 17, 2016

Re 1) I responded inline

Re 2) this is just a mapping of what AnimatedImplementation.js is doing to drive animations in JS. I'll try to explain it in few words:

The animated js is based on a concept of a graph where nodes are values or transform operations (such as interpolation, addition, etc) and connection are used to describe how change of the value in one node can affect other nodes. Few examples:

  • Animated.Value is a simplest type of node with numeric value which can be driven by an animation engine (spring, decay, etc) or by calling setValue on it from JS
  • Animated.add is a type of node that may have two or more input nodes. It outputs the sum of all the input node values
  • interpolate - is actually a method you can call on any node and it creates a new node that takes the parent node as an input and outputs its interpolated value (e.g. if you have value that can animate from 0 to 1 you can create interpolated node and set output range to be 0 to 100 and when the input node changes the output of interpolated node will be multiplying the values by 100)

You can mix and chain nodes however you like and this way create nodes graph with connections between them.

To map animated node values to view properties there is a special type of a node: AnimatedProps. It is created by AnimatedImplementation whenever you render Animated.View and stores a mapping from the view properties to the corresponding animated values (so it's actually also a node with connections to the value nodes). Example:

var someAnimatedValue = new Animated.Value(0);

...

<Animated.View
  style={{opacity: someAnimatedValue}}>
  <Text>Stuff</Text>
</Animated.View>

The code from the above will generate 3 nodes:

  • AnimatedValue node with a value set to 0
  • AnimatedStyle node that stores a mapping from property with the name "opacity" to the value node mentioned above
  • AnimatedProps node with style property being mapped to style node from the above

Last "special" parts of the the graph are "animation drivers". Those are objects that based on some criteria updates attached values every frame (we have few types of those, e.g., spring, timing, decay). Animation objects can be "started" and "stopped". In JS they use setTimeout to run value update loop every frame. They are like "pulse generators" for the rest of the nodes graph. Those pulses then propagate along the graph from nodes to their children up to the AnimatedProps nodes. Example:

Animated.timing(
  thatAnimatedValueFromThePreviousSnippet,
  {
    toValue: 1,
    duration: 500,
  },
 ).start(); 

The code above will create an object that will be responsible for updating thatAnimatedValueFromThePreviousSnippet animated value from their current state at the moment start is called (that would be 0) to the value specified in the animation config (toValue: 1) in a linear manner every frame for 500 milliseconds (duration: 500)

The process of calculating view updates during animation works as follows (I'm describing the process from the perspective of Animated JS and how it runs in javascript, but it works very similar in native):

  1. You run all the active animations that may update attached animated value nodes
  2. For each updated node you visit their children nodes as they may also require an update
  3. Once you reach AnimatedProps node you can collect properties which needs to be updated, then we send setNativeProps to the corresponding view

So in order to calculate property updates you need to now how is the value generated by the animation driver (e.g. spring) processed before it gets transformed into a value that can be set as a property on a view. This is why I'm using the simplest possible approach and just send and maintain that animated nodes graph in native. There are definitely other ways of handling that, but I believe this one is the most flexible method as it naturally reflects the public animated.js API. It is also future proof as far as the API isn't going to change dramatically and only new type of nodes are going to be added.

@astreet

This comment has been minimized.

Show comment
Hide comment
@astreet

astreet Mar 17, 2016

Contributor

Ok, got it, thanks for the explanation. I'm guessing we're just keeping the same architecture in native? The one thing I didn't understand was

The code from the above will generate 3 nodes:

  • AnimatedValue node with a value set to 0
  • AnimatedStyle node that stores a mapping from property with the name "opacity" to the value node mentioned above
  • AnimatedProps node with style property being mapped to style node from the above

What is the purpose of having both AnimatedStyle and AnimatedProps nodes?

I think I mostly have enough context to review this now, I will try to work on it tonight or tomorrow (currently traveling in Tel Aviv so my hours are a bit weird)

Contributor

astreet commented Mar 17, 2016

Ok, got it, thanks for the explanation. I'm guessing we're just keeping the same architecture in native? The one thing I didn't understand was

The code from the above will generate 3 nodes:

  • AnimatedValue node with a value set to 0
  • AnimatedStyle node that stores a mapping from property with the name "opacity" to the value node mentioned above
  • AnimatedProps node with style property being mapped to style node from the above

What is the purpose of having both AnimatedStyle and AnimatedProps nodes?

I think I mostly have enough context to review this now, I will try to work on it tonight or tomorrow (currently traveling in Tel Aviv so my hours are a bit weird)

@kmagiera

This comment has been minimized.

Show comment
Hide comment
@kmagiera

kmagiera Mar 17, 2016

Contributor

I'm guessing we're just keeping the same architecture in native? The one thing I didn't understand was

Yes, As I mentioned earlier, IMO this will allow us to easier adapt to any changes in Animated.JS (like new node types etc).

What is the purpose of having both AnimatedStyle and AnimatedProps nodes?

That's a very good question I don't really know the answer for. So I'm just mapping this to native as supporting separate graph structure would make it all more complex. I'm guessing this has been designed this way to split up handling nested objects into an existing node-with-children abstraction (we do same thing for other non-scalar properties such as transform - where we have AnimatedTransform node) . Note that for an Animated.View you can animate any property, not only style props (doesn't make sense most of the time though). Also, from the perspective of Animated.JS, we don't care that all styles props are just flattened and sent over the bridge as they were direct properties of the view - so the structure of all the props needs to be maintained (the flattening "magic" happens here). In fact, I'd prefer for all those styles, props and transform to be mapped to a single graph node. As at the moment those are the only nodes that outputs non-scalar values (maps with stuff), so having them in a single node would simplify interface we use for the nodes to communicate.

Perhaps @vjeux can answer this better or make a suggestion whether it makes sense to join Style, Props and AnimatedTransform together.

Contributor

kmagiera commented Mar 17, 2016

I'm guessing we're just keeping the same architecture in native? The one thing I didn't understand was

Yes, As I mentioned earlier, IMO this will allow us to easier adapt to any changes in Animated.JS (like new node types etc).

What is the purpose of having both AnimatedStyle and AnimatedProps nodes?

That's a very good question I don't really know the answer for. So I'm just mapping this to native as supporting separate graph structure would make it all more complex. I'm guessing this has been designed this way to split up handling nested objects into an existing node-with-children abstraction (we do same thing for other non-scalar properties such as transform - where we have AnimatedTransform node) . Note that for an Animated.View you can animate any property, not only style props (doesn't make sense most of the time though). Also, from the perspective of Animated.JS, we don't care that all styles props are just flattened and sent over the bridge as they were direct properties of the view - so the structure of all the props needs to be maintained (the flattening "magic" happens here). In fact, I'd prefer for all those styles, props and transform to be mapped to a single graph node. As at the moment those are the only nodes that outputs non-scalar values (maps with stuff), so having them in a single node would simplify interface we use for the nodes to communicate.

Perhaps @vjeux can answer this better or make a suggestion whether it makes sense to join Style, Props and AnimatedTransform together.

@vjeux

This comment has been minimized.

Show comment
Hide comment
@vjeux

vjeux Mar 18, 2016

Contributor

What is the purpose of having both AnimatedStyle and AnimatedProps nodes?

Mix of legacy and easier to manage. I started with AnimatedStyle to represent the style object, but then realized that I needed to do something very similar for transform, so I copy and pasted it to AnimatedTransform. Then, someone wanted to animate other attributes than styles (for ART), so I copy and pasted it another time with AnimatedProps.

I have no issue if you want to flatten the three into a single AnimatedNode.

Contributor

vjeux commented Mar 18, 2016

What is the purpose of having both AnimatedStyle and AnimatedProps nodes?

Mix of legacy and easier to manage. I started with AnimatedStyle to represent the style object, but then realized that I needed to do something very similar for transform, so I copy and pasted it to AnimatedTransform. Then, someone wanted to animate other attributes than styles (for ART), so I copy and pasted it another time with AnimatedProps.

I have no issue if you want to flatten the three into a single AnimatedNode.

@kmagiera

This comment has been minimized.

Show comment
Hide comment
@kmagiera

kmagiera Mar 23, 2016

Contributor

@astreet I updated PR addressing all the remaining inlines except from the 60FPS comment (see my response here)

Contributor

kmagiera commented Mar 23, 2016

@astreet I updated PR addressing all the remaining inlines except from the 60FPS comment (see my response here)

@kmagiera

This comment has been minimized.

Show comment
Hide comment
@kmagiera

kmagiera Mar 23, 2016

Contributor

@bestander I've fixed one related test failure, but the remining circleci failure doesn't look related:

404 no such package available : shebang-regex
Contributor

kmagiera commented Mar 23, 2016

@bestander I've fixed one related test failure, but the remining circleci failure doesn't look related:

404 no such package available : shebang-regex
@bestander

This comment has been minimized.

Show comment
Hide comment
@bestander

bestander Mar 23, 2016

Contributor

Well done! tests are green.
Sometimes npm install may fail with 404, a restart usually helps

Contributor

bestander commented Mar 23, 2016

Well done! tests are green.
Sometimes npm install may fail with 404, a restart usually helps

@aleclarson

This comment has been minimized.

Show comment
Hide comment
@aleclarson

aleclarson Mar 24, 2016

Contributor

Is there an iOS implementation in the works?

Contributor

aleclarson commented Mar 24, 2016

Is there an iOS implementation in the works?

@ide

This comment has been minimized.

Show comment
Hide comment
@ide

ide Mar 24, 2016

Collaborator

Is there an iOS implementation in the works?

Not to my knowledge, but someone at FB will probably author an Obj-C port down the road.

Collaborator

ide commented Mar 24, 2016

Is there an iOS implementation in the works?

Not to my knowledge, but someone at FB will probably author an Obj-C port down the road.

@astreet

This comment has been minimized.

Show comment
Hide comment
@astreet

astreet Mar 24, 2016

Contributor

We can address the framerate issue in a follow up, let's get this in while tests are still passing ;)

@facebook-github-bot shipit

Contributor

astreet commented Mar 24, 2016

We can address the framerate issue in a follow up, let's get this in while tests are still passing ;)

@facebook-github-bot shipit

@facebook-github-bot

This comment has been minimized.

Show comment
Hide comment
@facebook-github-bot

facebook-github-bot Mar 24, 2016

Thanks for importing. If you are an FB employee go to Phabricator to review.

facebook-github-bot commented Mar 24, 2016

Thanks for importing. If you are an FB employee go to Phabricator to review.

@ghost ghost closed this in 65ccdff Mar 24, 2016

kipricker pushed a commit to PlexChat/react-native that referenced this pull request Mar 30, 2016

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

lucasfeliciano added a commit to lucasfeliciano/react-native that referenced this pull request Mar 31, 2016

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

lucasfeliciano added a commit to lucasfeliciano/react-native that referenced this pull request Mar 31, 2016

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

lucasfeliciano added a commit to lucasfeliciano/react-native that referenced this pull request Apr 8, 2016

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
@dimitrovskif

This comment has been minimized.

Show comment
Hide comment
@dimitrovskif

dimitrovskif Apr 20, 2016

Have you guys considered adding another JS executor on another thread that can calculate animations, navigation transitions and other processes that require high framerate?

For example: (pseudocode)

const Animated = {
   animateView: (viewID) => {
         // schedules an animation on the animation JS executor
         // the animation JS executor should run 60fps independently
         AppRegistry.animationThread.enqueue({ target: viewID, curve:'ease', property: 'scaleX'});
   }
}

// When the view has calculated layout, tell animation JS executor to start modifying the native props
let c = <MyView onInit={() => {Animated.animateView();} />;

And the animation thread could be a JS executor that loads animation.thread.js as entry point:

const transitions = [];
requestAnimationFrame(anim);
function anim(){
    // iterate all scheduled transitions and modify the native props every frame
    // remove transition from queue when it's finished
    for(let transition of transitions){
        ViewUtils.setProperty(transition.viewID, transition.property, t * speed * transition.range);
    }
    requestAnimationFrame(anim);
}

function onThreadEnqueue(data, callback) {
    if(data.type == 'NEW_ANIMATION_SCHEDULE'){
       // the animation will be scheduled next anim frame
       transitions.push({data, callback});
    }
};

AppRegistry.setThreadQueueReceiver(onThreadEnqueue);

It would be much easier for development and cross compatibility. Since the animation JS executor won't perform render logic, the framerate should be the same as running animations on Android UI thread (except it would be much more flexible).

dimitrovskif commented Apr 20, 2016

Have you guys considered adding another JS executor on another thread that can calculate animations, navigation transitions and other processes that require high framerate?

For example: (pseudocode)

const Animated = {
   animateView: (viewID) => {
         // schedules an animation on the animation JS executor
         // the animation JS executor should run 60fps independently
         AppRegistry.animationThread.enqueue({ target: viewID, curve:'ease', property: 'scaleX'});
   }
}

// When the view has calculated layout, tell animation JS executor to start modifying the native props
let c = <MyView onInit={() => {Animated.animateView();} />;

And the animation thread could be a JS executor that loads animation.thread.js as entry point:

const transitions = [];
requestAnimationFrame(anim);
function anim(){
    // iterate all scheduled transitions and modify the native props every frame
    // remove transition from queue when it's finished
    for(let transition of transitions){
        ViewUtils.setProperty(transition.viewID, transition.property, t * speed * transition.range);
    }
    requestAnimationFrame(anim);
}

function onThreadEnqueue(data, callback) {
    if(data.type == 'NEW_ANIMATION_SCHEDULE'){
       // the animation will be scheduled next anim frame
       transitions.push({data, callback});
    }
};

AppRegistry.setThreadQueueReceiver(onThreadEnqueue);

It would be much easier for development and cross compatibility. Since the animation JS executor won't perform render logic, the framerate should be the same as running animations on Android UI thread (except it would be much more flexible).

@ide

This comment has been minimized.

Show comment
Hide comment
@ide

ide Apr 20, 2016

Collaborator

@dimitrovskif Yes it was considered but we wanted to first pursue a solution that didn't require a second JS VM heap.

Collaborator

ide commented Apr 20, 2016

@dimitrovskif Yes it was considered but we wanted to first pursue a solution that didn't require a second JS VM heap.

@dimitrovskif

This comment has been minimized.

Show comment
Hide comment
@dimitrovskif

dimitrovskif Apr 20, 2016

@ide Oh. The benefit of cross compatible native animations should be more important than the additional JS executor overhead, in my opinion. Writing code for all animation types and curves on all platforms might become messy

On the other hand, it is a fact that JS was never meant to be used with more heaps.

dimitrovskif commented Apr 20, 2016

@ide Oh. The benefit of cross compatible native animations should be more important than the additional JS executor overhead, in my opinion. Writing code for all animation types and curves on all platforms might become messy

On the other hand, it is a fact that JS was never meant to be used with more heaps.

@skevy

This comment has been minimized.

Show comment
Hide comment
@skevy

skevy Apr 20, 2016

Collaborator

@dimitrovskif yah the overhead of the extra JS heap is significant.

An iOS version of this is not going to be too hard to write...just needs someone to crank it out. And once it's built, it's relatively self-sustaining I think. There's still plenty of logic in JS that's shared cross-platform.

Collaborator

skevy commented Apr 20, 2016

@dimitrovskif yah the overhead of the extra JS heap is significant.

An iOS version of this is not going to be too hard to write...just needs someone to crank it out. And once it's built, it's relatively self-sustaining I think. There's still plenty of logic in JS that's shared cross-platform.

@vjeux

This comment has been minimized.

Show comment
Hide comment
@vjeux

vjeux Apr 20, 2016

Contributor

For context, right now we are having a really big effort on reducing the memory overhead of react native in order to be embedded inside of another app (the main fb app). The app by itself is already taking up a bunch of memory and the more you add ontop of that, the more likely you are going to have your app killed when you switch apps as the os wants to reclaim memory or even while the app is running.

If we want to ship there, we just cannot afford a second js heap right now. It would indeed make code sharing and development much easier though, so something we may reconsider in the future.

Contributor

vjeux commented Apr 20, 2016

For context, right now we are having a really big effort on reducing the memory overhead of react native in order to be embedded inside of another app (the main fb app). The app by itself is already taking up a bunch of memory and the more you add ontop of that, the more likely you are going to have your app killed when you switch apps as the os wants to reclaim memory or even while the app is running.

If we want to ship there, we just cannot afford a second js heap right now. It would indeed make code sharing and development much easier though, so something we may reconsider in the future.

@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat May 29, 2016

I'm having problems integrating the package to my project, any help here please?

Thats what I did try, but where do I get the ReactApplicationContext from?

import com.facebook.react.animated.NativeAnimatedModule;

/**
     * A list of packages used by the app. If the app uses additional views
     * or modules besides the default ones, add more packages here.
     */
    @Override
    protected List<ReactPackage> getPackages() {
        mCallbackManager = new CallbackManager.Factory().create();
        return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new VectorIconsPackage(),
            new ImagePickerPackage(),
            new FBSDKPackage(mCallbackManager),
            new ExtraDimensionsPackage(this),
            new NativeAnimatedModule(???)
        );
    }

ms88privat commented May 29, 2016

I'm having problems integrating the package to my project, any help here please?

Thats what I did try, but where do I get the ReactApplicationContext from?

import com.facebook.react.animated.NativeAnimatedModule;

/**
     * A list of packages used by the app. If the app uses additional views
     * or modules besides the default ones, add more packages here.
     */
    @Override
    protected List<ReactPackage> getPackages() {
        mCallbackManager = new CallbackManager.Factory().create();
        return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new VectorIconsPackage(),
            new ImagePickerPackage(),
            new FBSDKPackage(mCallbackManager),
            new ExtraDimensionsPackage(this),
            new NativeAnimatedModule(???)
        );
    }
@dimitrovskif

This comment has been minimized.

Show comment
Hide comment
@dimitrovskif

dimitrovskif May 29, 2016

@ms88privat You are supposed to create your own package, and then add NativeAnimatedModule inside the package:

@Override
public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();
    modules.add(new NativeAnimatedModule(reactContext));
    return modules;
}

After doing that, you may put your package inside getPackages(). You may read more info here

dimitrovskif commented May 29, 2016

@ms88privat You are supposed to create your own package, and then add NativeAnimatedModule inside the package:

@Override
public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();
    modules.add(new NativeAnimatedModule(reactContext));
    return modules;
}

After doing that, you may put your package inside getPackages(). You may read more info here

@ms88privat

This comment has been minimized.

Show comment
Hide comment
@ms88privat

ms88privat May 29, 2016

@dimitrovskif thanks, I will try that. Another question regarding "layout animations". I got one value, which I want to animate and then interpolate this value to the properties top, left, width, height and opacity. Would this be possible right now and result into better performance? Because at the moment the performance is completely crap on Android (even with dev false).

ms88privat commented May 29, 2016

@dimitrovskif thanks, I will try that. Another question regarding "layout animations". I got one value, which I want to animate and then interpolate this value to the properties top, left, width, height and opacity. Would this be possible right now and result into better performance? Because at the moment the performance is completely crap on Android (even with dev false).

zebulgar added a commit to nightingale/react-native that referenced this pull request Jun 18, 2016

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
@nschurmann

This comment has been minimized.

Show comment
Hide comment
@nschurmann

nschurmann Jun 23, 2016

@dimitrovskif hello!, could you provide an example of the package that you need to create?, my implementation fails silently:

package com.polygonsmash;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.animated.NativeAnimatedModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Created by nicolas on 6/23/16.
 */
class NativeAnimation implements ReactPackage {

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(
            ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();

        modules.add(new NativeAnimatedModule(reactContext));

        return modules;
    }
}
package com.polygonsmash;

import com.facebook.react.ReactActivity;
import com.facebook.reactnative.androidsdk.FBSDKPackage;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;

import android.content.Intent;     // <--- import
import android.os.Bundle;

import com.facebook.CallbackManager;
import com.facebook.FacebookSdk;
import com.facebook.reactnative.androidsdk.FBSDKPackage;

import com.polygonsmash.NativeAnimation;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends ReactActivity {
    CallbackManager mCallbackManager;
    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "polygonsmash";
    }

    /**
     * Returns whether dev mode should be enabled.
     * This enables e.g. the dev menu.
     */
    @Override
    protected boolean getUseDeveloperSupport() {
        return BuildConfig.DEBUG;
    }

    /**
     * A list of packages used by the app. If the app uses additional views
     * or modules besides the default ones, add more packages here.
     */
    @Override
    protected List<ReactPackage> getPackages() {
        mCallbackManager = new CallbackManager.Factory().create();
        ReactPackage packages[] = new ReactPackage[]{
                new MainReactPackage(),
                new NativeAnimation(),
                new FBSDKPackage(mCallbackManager),
        };
        return Arrays.<ReactPackage>asList(packages);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        mCallbackManager.onActivityResult(requestCode, resultCode, data);
    }
}
animate() {
    Animated.parallel([
      Animated.spring(this.bounceValue, {
        toValue: 1,
        friction: 3,
        useNativeDriver: true, // <-- doesn't affect performance
      }),
      Animated.timing(this.pan, { 
        duration: 1500,
        easing: Easing.inOut(Easing.linear),
        toValue: -200,
        useNativeDriver: true, // <-- stops the execution of everything without throwing any error
      })
      ,
      Animated.timing(this.fadeAnim, {
        duration: 1500,
        easing: Easing.inOut(Easing.linear),
        toValue: 0,
        useNativeDriver: true, // <-- stops the execution of everything without throwing any error
      })
    ]).start()
  }

nschurmann commented Jun 23, 2016

@dimitrovskif hello!, could you provide an example of the package that you need to create?, my implementation fails silently:

package com.polygonsmash;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.animated.NativeAnimatedModule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Created by nicolas on 6/23/16.
 */
class NativeAnimation implements ReactPackage {

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(
            ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();

        modules.add(new NativeAnimatedModule(reactContext));

        return modules;
    }
}
package com.polygonsmash;

import com.facebook.react.ReactActivity;
import com.facebook.reactnative.androidsdk.FBSDKPackage;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;

import android.content.Intent;     // <--- import
import android.os.Bundle;

import com.facebook.CallbackManager;
import com.facebook.FacebookSdk;
import com.facebook.reactnative.androidsdk.FBSDKPackage;

import com.polygonsmash.NativeAnimation;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends ReactActivity {
    CallbackManager mCallbackManager;
    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "polygonsmash";
    }

    /**
     * Returns whether dev mode should be enabled.
     * This enables e.g. the dev menu.
     */
    @Override
    protected boolean getUseDeveloperSupport() {
        return BuildConfig.DEBUG;
    }

    /**
     * A list of packages used by the app. If the app uses additional views
     * or modules besides the default ones, add more packages here.
     */
    @Override
    protected List<ReactPackage> getPackages() {
        mCallbackManager = new CallbackManager.Factory().create();
        ReactPackage packages[] = new ReactPackage[]{
                new MainReactPackage(),
                new NativeAnimation(),
                new FBSDKPackage(mCallbackManager),
        };
        return Arrays.<ReactPackage>asList(packages);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        mCallbackManager.onActivityResult(requestCode, resultCode, data);
    }
}
animate() {
    Animated.parallel([
      Animated.spring(this.bounceValue, {
        toValue: 1,
        friction: 3,
        useNativeDriver: true, // <-- doesn't affect performance
      }),
      Animated.timing(this.pan, { 
        duration: 1500,
        easing: Easing.inOut(Easing.linear),
        toValue: -200,
        useNativeDriver: true, // <-- stops the execution of everything without throwing any error
      })
      ,
      Animated.timing(this.fadeAnim, {
        duration: 1500,
        easing: Easing.inOut(Easing.linear),
        toValue: 0,
        useNativeDriver: true, // <-- stops the execution of everything without throwing any error
      })
    ]).start()
  }
@Introvertuous

This comment has been minimized.

Show comment
Hide comment
@Introvertuous

Introvertuous Sep 14, 2016

I don't mean to revive a dead thread but is this not implemented yet? Is there support for transform?

Introvertuous commented Sep 14, 2016

I don't mean to revive a dead thread but is this not implemented yet? Is there support for transform?

@DannyvanderJagt

This comment has been minimized.

Show comment
Hide comment
@DannyvanderJagt

DannyvanderJagt Nov 25, 2016

Contributor

Is there any eta/roadmap on this subject?

React native is an awesome project and it is really useful but due to the nature of js (single threaded) the performance does not compare to truly native apps. RN could receive a huge boost when animations are off loaded to a UI thread. It would benefit the current (Experimental) Navigator (animations) which is a huge bottle neck at the moment.

Would love to help.

Contributor

DannyvanderJagt commented Nov 25, 2016

Is there any eta/roadmap on this subject?

React native is an awesome project and it is really useful but due to the nature of js (single threaded) the performance does not compare to truly native apps. RN could receive a huge boost when animations are off loaded to a UI thread. It would benefit the current (Experimental) Navigator (animations) which is a huge bottle neck at the moment.

Would love to help.

@DannyvanderJagt

This comment has been minimized.

Show comment
Hide comment
@DannyvanderJagt

DannyvanderJagt Nov 25, 2016

Contributor

Never mind, after some more digging I found out that the RN team is working on this and that parts are already in RN. See here

Contributor

DannyvanderJagt commented Nov 25, 2016

Never mind, after some more digging I found out that the RN team is working on this and that parts are already in RN. See here

This issue was closed.

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