Skip to content

Latest commit

 

History

History
740 lines (538 loc) · 27.9 KB

README.md

File metadata and controls

740 lines (538 loc) · 27.9 KB

react-native-reanimated

React Native's Animated library reimplemented.

It provides a more comprehensive, low level abstraction for the Animated library API to be built on top of and hence allow for much greater flexibility especially when it comes to gesture based interactions.

OMG, why would you build this? (motivation)

Animated library has several limitations that become troubling when it comes to gesture based interactions. I started this project initially to resolve the issue of pan interaction when the object can be dragged along the screen and when released it should snap to some place on the screen. The problem was that despite using Animated.event and mapping gesture state to the position of the box, and making this whole interaction run on UI thread with useNativeDriver flag, we still had to call back into JS at the end of the gesture for us to start "snap" animation. This is because Animated.spring({}).start() cannot be used in a "declarative" manner, because when it gets executed it has a "side effect" of starting a process (an animation) that updates the value for some time. Adding "side effect" nodes into the current Animated implementation turned out to be a pretty difficult task as the execution model of the Animated API runs all the dependent nodes of each frame for the views that need to update. We don't want to run "side effects" more often than necessary as it would, for example, result in the animation starting multiple times.

Another reason why I started rethinking how the internals of Animated can be redesigned was my recent work on porting "Animated Tracking" functionality to the native driver. Apparently, even though the native driver is out for quite a while, it still does not support all the things non-native Animated lib can do. Obviously, it is far more difficult to build three versions of each feature (JS, Android and iOS) instead of one, and the same applies for fixing bugs. One of the goals of react-native-reanimated was to provide a more generic building block for the API that would allow for building more complex features only in JS and make the native codebase as minimal as possible. Taking "diffClamp" node as an example, it is currently implemented in three different places in Animated core and even though it is pretty useful it actually only has one use case (collapsible scrollview header).

On a similar topic, I come across React Native's PR #18029 and even though it provides a legitimate use case, I understand the maintainers being hesitant about merging it. The Animated API shouldn't block people from building things like this and the goal of react-native-reanimated is to provide lower level access that would allow for implementing that and many more features with no necessary changes to the core of the library.

You can watch my React Europe talk where I explain the motivation.

The goals:

  • More generic primitive node types leading to more code reuse for the library internals therefore making it easier to add new features and fix bugs.
  • The new set of base nodes can be used to implement Animated compatible API including:
  • Complex nodes such as “diffClamp”.
  • Interactions such as animated value tracking or animation staggering.
  • Conditional evaluation & nodes with side effects (set, startClock, stopClock).
  • No more “useNativeDriver” – all animations runs on the UI thread by default

Getting started

Before you get started you should definitely familiarize yourself with the original Animated API first. Refer to the API description below and to the Examples section to learn how to use this library.

Throughout this document when we refer to classes or methods prefixed with Animated we usually refer to them being imported from react-native-reanimated package instead of plain react-native.

Installation

I. First install the library from npm repository using yarn:

  yarn add react-native-reanimated

II. Link native code with react-native cli:

  react-native link react-native-reanimated

III. When you want to use "reanimated" in your project import it from react-native-reanimated package:

import Animated from 'react-native-reanimated';

Similarly when you need Easing import it from react-native-reanimated package instead of react-native:

import Animated, { Easing } from 'react-native-reanimated';

Reanimated vs Animated

We aim to bring this project to be fully compatible with Animated API. We believe that the set of base nodes we have selected should make this possible to be done only by writing JS code and does not require significant changes in the native codebases. Here is a list of things that haven't yet been ported from the original version of Animated library. All the functionality that missing elements provide in Animated can already be achieved with react-native-reanimated although a different methodology for implementing those may be required (e.g. check "Running animations" section to see how the implementation may differ).

  • using value offsets
  • value tracking (can be achieved in different way, react-native-reanimated also allows for tracking all the animation parameters not only destination params)
  • animation staggering
  • animation delays

Value

Animated.Value is a container for storing values. It's is initialized with new Value(0) constructor. For backward compatibility there's provided API for setting value after it has been initialized:

const v = new Value(0);
/// ...
v.setValue(100);

Clocks

Original Animated API makes an "animation" object a first class citizen. Animation object has many features and therefore requires quite a few JS<>Native bridge methods to be managed properly. In react-native-reanimated, clocks aim to replace that by providing more of a low level abstraction but also since clock nodes behave much like the animated values they make the implementation much less complex.

Animated.Clock node is a special type of Animated.Value that can be updated in each frame to the timestamp of the current frame. When we take Clock node as an input, the value it returns is the current frame timestamp in milliseconds. Using special methods, clock nodes can be stopped and started and we can also test if clock has been started.

Because Animated.Clock just extends the Animated.Value you can use it in the same places (operations) where you can pass any type of animated node.

At most once evaluation (the algorithm)

Unlike the original Animated library where each node could have been evaluated many times within a single frame, react-native-reanimated restricts each node to be evaluated at most once in a frame. This restriction is required for nodes that have side-effects to be used (e.g. set or startClock). When node is evaluated (e.g. in case of an add node we want to get a sum of the input nodes) its value is cached. If within the next frame there are other nodes that want to use the output of that node instead of evaluating we return cached value. This notion also helps with performance as we can try to evaluate as few nodes as expected. The current algorithm for making decisions of which nodes to evaluate works as follows:

  1. for each frame we first analyze the generated events (e.g. touch stream). It is possible that events may update some animated values.
  2. Then we update values that correspond to clock nodes that are "running".
  3. We traverse the node's tree starting from the nodes that have been updated in the current cycle and we look for final nodes that are connected to views.
  4. If we found nodes connected to view properties we evaluate them. This can recursively trigger evaluation for their input nodes etc.
  5. After everything is done we check if some "running" clocks exists. If so we enqueue a callback to be evaluated with the next frame and start over from pt 1. Otherwise we do nothing.

Blocks

Blocks are just an arrays of nodes that are being evaluated in a particular order and return the value of the last node. It can be created using block command but also when passed as an argument to other nodes the block command can be omitted and we can just pass a nodes array directly. See an example below:

cond(
  eq(state, State.ACTIVE),
  [
    stopClock(clock),
    set(transX, add(transX, diffX))
  ],
  runTiming(clock, state, config)
)

Passing array directly is equivalent to wrapping it with the block command.

API reference

Views, props, etc

Follow the original Animated library guides to learn how values can be connected to View attributes. Similarly with react-native-reanimated you need to use components prefixed with Animated. (remember to import Animated from reanimated package). For example:

import Animated from 'react-native-reanimated';

// use
<Animated.View/>
// instead of
<View/>

Animated.Code

Animated.Code component allows you to define reanimated nodes that you want to execute when their input nodes updates, but aren't necessarily strictly related to some view properties and hence it does not feel right to place them under translate or other prop of an Animated.View. This component renders null, so you can place it in any place you want in your render method. It is required that your code is put inside component as we rely on componentDidMount and componentWillUnmount callbacks to install and cleanup animated nodes. Note that the code you put is going to be executed only once. We currently have no way of telling if your code changes and so it will only be run in componentDidMount. If you wish for your reanimated nodes to be updated when the component updates, you can update the key property of the Animated.Code component, which will effectively unmount old and mount new versions of it in the React tree.

<Animated.Code>
  { ()=>
        block([
          set(this.transX1, add(multiply(-1, this._transX))),
          set(this.transX2, add(multiply(-2, this._transX), 120)),
          set(this.transX3, sub(multiply(2, this._transX), 120)),
          set(this.transX4, add(multiply(1, this._transX))),
        ])
  }
</Animated.Code>

or:

<Animated.Code exec={
block([
  set(this.transX1, add(multiply(-1, this._transX))),
  set(this.transX2, add(multiply(-2, this._transX), 120)),
  set(this.transX3, sub(multiply(2, this._transX), 120)),
  set(this.transX4, add(multiply(1, this._transX))),
])
}/>

Event handling with reanimated nodes

react-native-reanimated's new syntax is possible to be used with Animated.event. Instead of providing only a mapping from event fields to animated nodes, it is allowed to write a function that takes reanimated values map as an input and return a block (or any other reanimated function) that will be then used to handle the event.

This syntax allows for providing some post-processing for the event data that does not fit well as a dependency of other nodes we connect to Animated.View component props. See example

this.onGestureEvent = event([
  {
    nativeEvent: {
      translationX: x => set(this._x, x)
    },
  },
]);

If you'd like to use more than one event attribute in your reanimated code, this is also supported. Instead of defining event handler methods for a single attribute you can define at the level of nativeEvent. Here is an example that takes both translation attributes and state attribute from PanGestureHandler event:

<PanGestureHandler
  onGestureEvent={event([
    {
      nativeEvent: ({ translationX: x, translationY: y, state }) =>
        block([
          set(this._transX, add(x, offsetX)), set(this._transY, add(y, offsetY)),
          cond(eq(state, State.END), [set(this.offsetX, add(this.offsetX, x)), set(this.offsetY, add(this.offsetY, y))]),
        ]),
    },
  ])}
>
  <Animated.View
    style={{ transform: [{ translateX: this._transX, translateY: this._transY }], }}
  />
</PanGestureHandler>

Available nodes


set

set(valueToBeUpdated, sourceNode)

When evaluated, it will assign the value of sourceNode to the Animated.Value passed as a first argument. In other words, it performs an assignment operation from the sourceNode to valueToBeUpdated value node.


cond

cond(conditionNode, ifNode, [elseNode])

If conditionNode evaluates to "truthy" value the node evaluates ifNode node and returns its value, otherwise it evaluates elseNode and returns its value. elseNode is optional.


call

call(argsNodes, callback)

If one of the nodes from argsNodes array updates, callback will be called in JavaScript with a list of current values of nodes from argsNodes array as the first argument.


block

block([node1, ...])

Takes an array of nodes and evaluates all of them in the order they are put in the array. It then returns the value of the last node.


debug

debug(messageString, valueNode)

When the node is evaluated, it prints a string that contains the messageString concatenated with the value of valueNode. This then returns the value of valueNode. Logs are printed in the JS debugger if it's attached, in console if Expo client is being used, or else in the native console. Logs are visible only in DEV mode and have no effect on production builds. Note that messageString should be a normal string, not an animated node.


startClock

startClock(clockNode)

When evaluated, it will make Clock node passed as an argument start updating its value each frame. Then returns 0.


stopClock

stopClock(clockNode)

When evaluated, it will make Clock node passed as an argument stop updating its value (if it has been doing that). Then returns 0.


clockRunning

clockRunning(clockNode)

For a given Clock node, it returns 1 if the clock has been started (if it's updating each frame) or returns 0 otherwise.


event

Works the same way as with the original Animated library.


add

add(nodeOrNumber1, nodeOrNumber2, ...)

Takes two or more animated nodes or values, and when evaluated, returns their sum.


sub

sub(nodeOrNumber1, nodeOrNumber2, ...)

Takes two or more animated nodes or values, and when evaluated, returns the result of subtracting their values in the exact order.


multiply

multiply(nodeOrNumber1, nodeOrNumber2, ...)

Takes two or more animated nodes or values, and when evaluated, returns the result of multiplying their values in the exact order.


divide

divide(nodeOrNumber1, nodeOrNumber2, ...)

Takes two or more animated nodes or values, and when evaluated, returns the result of dividing their values in the exact order.


pow

pow(nodeOrNumber1, nodeOrNumber2, ...)

Takes two or more animated nodes or values, and when evaluated, returns the result of first node to the second node power. If more than two nodes are present, the result from the previous step is used as a base and the third node as exponent. This process continues onward for the following nodes if these are present.


modulo


sqrt


sin

sin(node)

Returns a sine of the value (in radians) of the given node.


cos

cos(node)

Returns a cosine of the value (in radians) of the given node


exp

exp(node)

Returns an exponent of the value of the given node.


round

round(node)

Returns a node that rounds input value to the nearest integer.


floor

floor(node)

Returns a node that rounds a number downward to its nearest integer. If the passed argument is an integer, the value will not be rounded.


ceil

ceil(node)

Returns a node that rounds a number upward to its nearest integer. If the passed argument is an integer, the value will not be rounded.


lessThan

lessThan(nodeOrValueA, nodeOrValueB)

Returns 1 if the value of the first node is less than the value of the second node. Otherwise returns 0.


eq

eq(nodeOrValueA, nodeOrValueB)

Returns 1 if the value of both nodes are equal. Otherwise returns 0.


greaterThan

greaterThan(nodeOrValueA, nodeOrValueB)

Returns 1 if the value of the first node is greater than the value of the second node. Otherwise returns 0.


lessOrEq

lessOrEq(nodeOrValueA, nodeOrValueB)

Returns 1 if the value of the first node is less or equal to the value of the second node. Otherwise returns 0.


greaterOrEq

greaterOrEq(nodeOrValueA, nodeOrValueB)

Returns 1 if the value of the first node is greater or equal to the value of the second node. Otherwise returns 0.


neq

neq(nodeOrValueA, nodeOrValueB)

Returns 1 if the value of the first node is not equal to the value of the second node. Otherwise returns 0.


and

and(nodeOrValue1, ...)

Acts as a logical AND operator. Takes one or more nodes as an input and evaluates them in sequence until some node evaluates to a "falsy" value. Then returns that value and stops evaluating further nodes. If all nodes evaluate to a "truthy" it returns the last node's value.


or

or(nodeOrValue1, ...)

Acts as a logical OR operator. Takes one or more nodes as an input and evaluates them in sequence until some node evaluates to a "truthy" value. Then returns that value and stops evaluating further nodes. If all nodes evaluate to a "falsy" value it returns the last node's value.


defined

defined(node)

Returns 1 if the given node evaluates to a "defined" value (that is to something that is non-null, non-undefined and non-NaN). Returns 0 otherwise.


not

not(node)

Returns 1 if the given node evaluates to a "falsy" value and 0 otherwise.


abs

abs(node)

Evaluates the given node and returns an absolute value of the node's value.


min

min(nodeOrValue1, ...)

Takes one or more nodes as an input and returns a minimum of all the node's values.


max

max(nodeOrValue1, ...)

Takes one or more nodes as an input and returns a maximum of all the node's values.


diff

diff(node)

Evaluates node and returns a difference between value returned at the last time it was evaluated and its value at the current time. When evaluating for the first time it returns the node's value.


acc

acc(node)

Returns an accumulated value of the given node. This node stores a sum of all evaluations of the given node and each time it gets evaluated it would add current node's value to that sum and return it.


diffClamp

Works the same way as with the original Animated library.


interpolate

interpolate(node, {
  // Input range for the interpolation. Should be monotonically increasing.
  inputRange: [nodeOrValue...],
  // Output range for the interpolation, should be the same length as the input range.
  outputRange: [nodeOrValue...],
  // Sets the left and right extrapolate modes.
  extrapolate?: Extrapolate.EXTEND | Extrapolate.CLAMP | Extrapolate.IDENTITY,
  // Set the left extrapolate mode, the behavior if the input is less than the first value in inputRange.
  extrapolateLeft?: Extrapolate.EXTEND | Extrapolate.CLAMP | Extrapolate.IDENTITY,
  // Set the right extrapolate mode, the behavior if the input is greater than the last value in inputRange.
  extrapolateRight?: Extrapolate.EXTEND | Extrapolate.CLAMP | Extrapolate.IDENTITY,
})

Extrapolate.EXTEND; // Will extend the range linearly.
Extrapolate.CLAMP; // Will clamp the input value to the range.
Extrapolate.IDENTITY; // Will return the input value if the input value is out of range.

Maps an input value within a range to an output value within a range. Also supports different types of extrapolation for when the value falls outside the range.


color

color(red, green, blue, alpha)

Creates a color node in RGBA format, where the first three input nodes should have integer values in the range 0-255 and correspond to color components Red, Green and Blue respectively. Last input node should have a value between 0 and 1 and represents alpha channel (value 1 means fully opaque and 0 completely transparent). Alpha parameter can be ommited, then 1 (fully opaque) is used as a default.

The returned node can be mapped to view properties that represents color (e.g. backgroundColor).


concat

concat(nodeOrValue1, ...)

Returns concatanation of given nodes (number or string) as string


onChange

onChange(value, action)

When evaluated, it will compare value to its previous value. If it has changed, action will be evaluated and its value will be returned.

Animations


decay

decay(clock, { finished, velocity, position, time }, { deceleration })

Updates position and velocity nodes by running a single step of animation each time this node evaluates. State variable finished is set to 1 when the animation gets to the final point (that is the velocity drops under the level of significance). The time state node is populated automatically by this node and refers to the last clock time this node got evaluated. It is expected to be reset each time we want to restart the animation. Decay animation can be configured using deceleration config param and it controls how fast the animation decelerates. The value should be between 0 and 1 but only values that are close to 1 will yield meaningful results.


timing

timing(clock, { finished, position, frameTime, time }, { toValue, duration, easing })

Updates position node by running timing based animation from a given position to a destination determined by toValue. The animation is expected to last duration milliseconds and use easing function that could be set to one of the nodes exported by the Easing object. The frameTime node will also get updated and represents the progress of animation in milliseconds (how long the animation has lasted so far), similar to the time node that just indicates the last clock time the animation node has been evaluated. Both of these variables are expected to be reset before restarting the animation. Finally finished node will be set to 1 when the position reaches the final value or when frameTime exceeds duration.


spring

spring(clock, { finished, position, velocity, time }, { damping, mass, stiffness, overshootClamping, restSpeedThreshold, restDisplacementThreshold, toValue })

When evaluated, updates position and velocity nodes by running a single step of spring based animation. Check the original Animated API docs to learn about the config parameters like damping, mass, stiffness, overshootClamping, restSpeedThreshold and restDisplacementThreshold. The finished state updates to 1 when the position reaches the destination set by toValue. The time state variable also updates when the node evaluates and it represents the clock value at the time when the node got evaluated for the last time. It is expected that time variable is reset before spring animation can be restarted.

Running animations

Declarative API

Invoking animation differs from the way it is done when using the original Animated API. Here, instead of having animation objects we operate on nodes that can perform single animation steps. In order to map an animation into a value, we will make the value to be assigned to a node that among few other things will call into the animation step node. Check timing, decay and spring nodes documentation for some details about how animation step nodes can be configured.

The example below shows a component that renders:

import Animated, { Easing } from 'react-native-reanimated';

const { Clock, Value, set, cond, startClock, clockRunning, timing, debug, stopClock, block } = Animated

function runTiming(clock, value, dest) {
  const state = {
    finished: new Value(0),
    position: new Value(0),
    time: new Value(0),
    frameTime: new Value(0),
  };

  const config = {
    duration: 5000,
    toValue: new Value(0),
    easing: Easing.inOut(Easing.ease),
  };

  return block([
    cond(clockRunning(clock), 0, [
      // If the clock isn't running we reset all the animation params and start the clock
      set(state.finished, 0),
      set(state.time, 0),
      set(state.position, value),
      set(state.frameTime, 0),
      set(config.toValue, dest),
      startClock(clock),
    ]),
    // we run the step here that is going to update position
    timing(clock, state, config),
    // if the animation is over we stop the clock
    cond(state.finished, debug('stop clock', stopClock(clock))),
    // we made the block return the updated position
    state.position,
  ]);
}

export class AnimatedBox extends Component {
  // we create a clock node
  clock = new Clock();
  // and use runTiming method defined above to create a node that is going to be mapped
  // to the translateX transform.
  transX = runTiming(this.clock, -120, 120);

  render() {
    return (
      <View style={styles.container}>
        <Animated.View
          style={[styles.box, { transform: [{ translateX: this.transX }] }]}
        />
      </View>
    );
  }
}

Backward compatible API

As it might sometimes be impractical to use the API above, there's an alternative way of invoking animation, which is similar to the original Animated API.

class Example extends Component {
  constructor(props) {
    super(props);
    this._transX = new Value(0);
    this._config = {
      duration: 5000,
      toValue: 120,
      easing: Easing.inOut(Easing.ease),
    };
    this._anim = timing(this._transX, this._config);
  }

  render() {
    return (
      <View style={styles.container}>
        <Animated.View
          style={[styles.box, { transform: [{ translateX: this._transX }] }]}
        />
        <Button
          onPress={() => {
            this._anim.start();
          }}
          title="Start"
        />
      </View>
    );
  }
}

This API gives the possibility to use animation with original Animated API. It's also a way of running animation on some interaction without necessity or rerendering view.

100% declarative gesture interactions

react-native-reanimated works best with the Gesture Handler library. Currently all the examples are made using that library, including the ultimate ImagePreview app. See it in action below:

Examples

The source code for the example (showcase) app is under the Example/ directory.

In order to run it you need to pull in the repository, enter Example/ folder and run:

  yarn install

Then run react-native run-android or react-native run-ios (depending on which platform you want to run the Example app on).

You will need to have an Android or iOS device or emulator connected as well as react-native-cli package installed globally.

License

React native reanimated library is licensed under The MIT License.

Credits

This project is supported by amazing people from Expo.io and Software Mansion

expo swm