Skip to content

NativeAnimatedModule

Alexander Sklar edited this page Jun 6, 2020 · 5 revisions

This page describes the NativeAnimatedModule in detail. To accomplish this I will be deep-diving on all the pieces used to accomplish this animation:

JavaScript

Here is the JavaScript definition for this UI:

class Tester extends React.Component<$FlowFixMeProps, $FlowFixMeState> {
  state = {
    native: new Animated.Value(0),
    nativeTracker: new Animated.Value(0),
  };
  current = 0;
  onPress = () => {
    const animConfig = this.props.config;
    this.current = 1;
    const config: Object = {
      ...animConfig,
      toValue: this.current,
    };
    Animated.timing(this.state.native, {
      ...config,
      useNativeDriver: true,
    }).start();
    Animated.timing(this.state.nativeTracker, {
      toValue: this.state.native,
      duration: 2000,
      useNativeDriver: true
    }).start();
  };
  render() {
    return (
      <TouchableWithoutFeedback onPress={this.onPress}>
        <View>
          <View>
            <Text>Native:</Text>
          </View>
          <View style={styles.row}>
            <Animated.View
              style={[
                styles.block,
                {
                  transform: [
                    {
                      translateX: this.state.native.interpolate({
                        inputRange: [0, 1],
                        outputRange: [0, 200],
                      }),
                    },
                    {
                      translateY: this.state.native.interpolate({
                        inputRange: [0, 0.5, 1],
                        outputRange: [0, 50, 0],
                      }),
                    },
                  ],
                  opacity: Animated.subtract(
                    this.state.native.interpolate({
                      inputRange: [0, 1],
                      outputRange: [1, 1],
                    }),
                    this.state.native.interpolate({
                      inputRange: [0, 0.5, 1],
                      outputRange: [0, 0.5, 0],
                    }),
                  ),
                },
              ]}
            />
          </View>
          <View>
            <Text>Tracking:</Text>
          </View>
          <Animated.View
            style={[
              styles.block,
              {
                transform: [
                  {
                    translateX: this.state.nativeTracker.interpolate({
                      inputRange: [0, 1],
                      outputRange: [0, 200],
                    }),
                  },
                  {
                    translateY: this.state.nativeTracker.interpolate({
                      inputRange: [0, 0.5, 1],
                      outputRange: [0, 50, 0],
                    }),
                  },
                ],
                opacity: Animated.subtract(
                  this.state.nativeTracker.interpolate({
                    inputRange: [0, 1],
                    outputRange: [1, 1],
                  }),
                  this.state.nativeTracker.interpolate({
                    inputRange: [0, 0.5, 1],
                    outputRange: [0, 0.5, 0],
                  }),
                ),
              },
            ]}
          />
        </View>
      </TouchableWithoutFeedback>
    );
  }
}

There is quite a bit going on here, I will describe the pieces order of dependence:

  1. First we have an Animated.Value named native, which holds a scalar value which will be animated and starts at 0.
  2. Next we have an Animated.Timing which is run in the OnPress handler. It animates (1) to the value of 1 over the course of 1 second. Crucially we also set the useNativeDriver value to true, without this the animation will not use the native module and will instead be executed entirely on the JS thread.
  3. Next we have an Animated.View which has a style with a custom translateX, translateY, and opacity properties, all of which are tied to (1).
    1. The translate properties are tied to the (1) by a couple of interpolation objects. These node define a function which takes a single input and linearly converts to an output. The function is piece-wise defined by a collection of input and output ranges. You can specify the behavior for input values which fall outside of the specified input range. These nodes are very useful for switching units (such as to degrees for a rotation animation, or in this case from a notion of 'progress' to distance in pixels.
    2. The opacity property is tied to (1) by a subtraction node. A subtraction node is a part of a family of animation 'math' operations that can be performed. They allow you to describe an expression of animated values that determines your end value. In this case we are determining the opacity by subtracting one interpolated value from another, but multiplication, addition, division, and modulus are options as well.
  4. Next we have a second Animated.Value named nativeTracker
  5. This second value is animated by a new Animated.Timing except this has a toValue of (1) the animated value. This is called a tracking animation. For tracking animations the duration property specifies how far behind the leading animation the tracker will follow, so in this case this new Animated.Value (4) will follow (1) two seconds behind. Again we also set the useNativeDriver: true.
  6. This new value is tied to a view in the same fashion as (3)

Native Module

First a brief description of the overall structure of the module. Animations are defined and described using a graph. There are 5 types of nodes that can exist in this graph: Value, style, props, transform, tracking, and animation nodes.

  • Value nodes are responsible for maintaining a scalar value which will be animated.
  • Style nodes hold a collection of transform and props nodes which are associated with this same view.
  • Props nodes are responsible for tying the an animation to the appropriate property on the target view, these are the nodes that actually animate the view.
  • Transform nodes are a specialized version of props nodes which work with the transform property.
  • Tracking nodes are used to define tracking relationships between value nodes.
  • Animation nodes have the sole responsibility of animating a value node, they do not animated anything visual.

I will now describe what pieces (1) through (6) result in at the NativeAnimatedModule level.

  1. The module creates a new Value node an initializes its rawValue to 0. The value node maintains a composition property set with two values, a rawValue and an Offset. Animations targeting this node will animate the Offset and upon completion with collapse the offset into the raw value.
  2. The module creates a new FrameAnimationDriver, the type of animation node associated with Animated.Timing. The config passed to the module by JS contains 60 values per second of animation and an identifier for which value node the animation is targeting (1). From this information we are able to discern the animations duration, and linearly easing between the frame values is close enough to a smooth curve for our purposes. The node creates a composition key frame animation which targets the value nodes offset property and has a key frame for each of the values handed across the bridge.
  3. The module creates three Props nodes, one for each of the TranslateX, TranslateY and Opacity properties. It also creates one style node which contains these properties. These Props nodes create composition expression animations which take as input the a value node and output that value to their target XAML facade.
    1. The module creates two interpolate nodes. These are specialized value nodes which attach to (1) and have an internal composition expression animation which converts the value from (1) into the interpolated value specified by the provided ranges. There are three different extrapolation options for these nodes which dictates the behavior when the input value is outside the provided range. The lower and upper bounds are also individually customizable:
      • Clamp clamps the output to the provided max/min.
      • Identity outputs the input without modification.
      • Extend linearly extends the input and output range to encompass the value.
    2. The module creates 2 interpolate nodes, similar to above and also a subtraction node. A subtraction node is a specialized Value node which contains an internal composition expression animation which is used to calculate its value from the provided reference values. All of the 'math' nodes behave this way, with their only difference being the expression they use to perform the operation.
  4. Another value node, same as (1).
  5. The module creates a tracking node. This node creates a new composition key frame animation by examining all of the animations targeting the leading node and constructing new animations from the frames of those animations. The complexities of this can get quite high, especially when animations other than timing animations are involved on either the leading or following value nodes, but in the example case it isn't that bad. We simply pad the start of the animation with 0 for the delay duration then copy the frame values of the leading nodes animations. This animation is then played on (4)
  6. The same as (3) we now tie this value node to the view via props and style nodes.

Here is a flow chart showing how the pieces fit together:

Quirky bits

  1. XAML façades have a restriction that you cannot have two animations which target different sub-channels of the same property. This affects us when we try to animate TranslateX and TranslateY properties at the same time. To get around this we add another layer of indirection, when the developer asks for an animation which targets a sub-channel we play that animation targeting another property set which contains properties for the other sub-channels as well and then have a new expression animation which combines the sub channels into a single vector to be placed in the façade.

Open Issues

  1. Event based animations. React native has a notion of event based animations which allows the developer to use UI events to drive there animations. For example you can use a scroll views scrolling event to drive an animation, which is how you could achieve a parallax effect. I believe this feature is going to require having the native module subscribe to JS events to be notified of changes it needs to update. We might be able to avoid using the JS bridge by attaching to the corresponding native event directly. More investigation is needed.
  2. Start/StopListeningToAnimatedNodeValue This API exists on the native module but we don't have a good way of implementing it. It asks that the module raise an event to JS every time the specified value node's value is changed. However this value is being animated off the UI thread in our implementation and there isn't a (good) way of being notified when the value changes. Even if we did have a way of detecting this change it would still involve processing on the UI thread 60 times per second. This API has significant cost, we should think hard about what the correct approach is.
  3. Custom animation targets. ReactNative animations can target custom control properties, for instance you can animate Slider's value property. The issue here is that we are missing the last step in the animation chain which connects the value to the view. For more typical target properties we make this connection via the corresponding XAML façade property. This XAML API takes care of pointing the composition animation at the appropriate composition object and property. We can get part of the way there by having view managers forward declare additional animatable properties, like value for slider, but we still have the issue of not know when the value in a value node updates.
  4. Perspective. The default perspective for React Native does not match the default perspective for Xaml, which results in animations that use the Z axis not looking properly. We should be able to change the default just before we kick off an animation which cares without too much effort. Additionally in react native it is possible to animate the perspective associated with an element, this is going to be much harder.
  5. Downlevel support. The module currently assumes the existence of the façade properties which weren't included until Redstone 5 (17763). This function could be implemented using ElementCompositionPreview down to RS2 if that was desired.