Course Menu

4 Corners

Intro

In this demo we'll walk through sequence of animating to 4 corners of the screen. We'll take advantage of Dimensions, Animated.ValueXY, Animated.sequence, and Animated.spring. We will gather the layout dynamically and animate the view to all the corners.

Setup

We'll setup a basic React Native component. We'll add 3 additional imports, Animated, TouchableWithoutFeedback, and Dimensions.

We'll create a state variable called animation to hold our Animated.ValueXY.

import React, { Component } from "react";
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  Animated,
  TouchableWithoutFeedback,
  Dimensions,
} from "react-native";

export default class animations extends Component {
  state = {
    animation: new Animated.ValueXY(),
  };

  render() {
    return (
      <View style={styles.container}>
        <TouchableWithoutFeedback>
          <Animated.View style={[styles.box]} />
        </TouchableWithoutFeedback>
      </View>
    );
  }
}

Our TouchableWithoutFeedback will use the React.cloneElement command and apply properties to our child view without adding any additional layout items. This means that we should apply our box styling to the Animated.View. This is different than other Touchable elements as they usually wrap the child element in an Animated.View which will effect positioning.

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  box: {
    width: 150,
    height: 150,
    backgroundColor: "tomato",
    position: "absolute",
    top: 0,
    left: 0,
  },
});

Get the Box Layout

A key concept in animating some complex animations is having to know the current dimensions of a particular element and or a target element. There are a few methods but one of the most basic is using the onLayout prop. Due to the layout calculations occurring on the native side, we need to pass a callback so we can get the values once the view layout completes.

We will also need to pass them to our TouchableWithoutFeedback because of the way it is structured to use React.cloneElement and it explicitly overrides the onLayout of the child view. So if we passed it to our Animated.View it would never be called.

<TouchableWithoutFeedback
  onPress={this.startAnimation}
  onLayout={this.saveDimensions}
>

Here we simply save off the width and height so we can access it later.

saveDimensions = (e) => {
  this._width = e.nativeEvent.layout.width;
  this._height = e.nativeEvent.layout.height;
};

Animate to the corners

Now we'll need to also know the height and width of the window, so we'll use our Dimensions import and get the width and height of our window.

For our first corner animation, we'll go down the left side of the screen. Meaning we'll need to animate the y value of our Animated.ValueXY. We need to animate to the height of the screen minus the size of our box which we put at this._height.

startAnimation = () => {
  const { width, height } = Dimensions.get("window");

  Animated.spring(this.state.animation.y, {
    toValue: height - this._height,
  });
};

The next corner will be the bottom right. Which means we need to animate across our screen. We already animated our y position so now we just need to animate our x position. Just like height we'll need to use our screen width minus the box width we saved at this._width.

Animated.spring(this.state.animation.x, {
  toValue: width - this._width,
});

Now we're in the bottom right, and we'll move our box back up to the top right. We don't need to know the height as we are just animating our y back to the beginning which is 0.

Animated.spring(this.state.animation.y, {
  toValue: 0,
});

Finally we animate our x back to 0 where it started and we're officially at the start.

Animated.spring(this.state.animation.x, {
  toValue: 0,
});

Now to combine all of our animations to happen one after the other we can simply use Animated.sequence and pass the entire series of animations in as an array.

startAnimation = () => {
  const { width, height } = Dimensions.get("window");

  Animated.sequence([
    Animated.spring(this.state.animation.y, {
      toValue: height - this._height,
    }),
    Animated.spring(this.state.animation.x, {
      toValue: width - this._width,
    }),
    Animated.spring(this.state.animation.y, {
      toValue: 0,
    }),
    Animated.spring(this.state.animation.x, {
      toValue: 0,
    }),
  ]).start();
};

Apply Style

We need to apply our style, and Animated.ValueXY comes with a handy transform shorthand. The getTranslateTransform will return an array of transforms for translateX and translateY.

The equivalent would be

[
  { translateX: this.state.animation.x },
  { translateY: this.state.animation.y },
];
  render() {
    const animatedStyles = {
      transform: this.state.animation.getTranslateTransform()
    }
    return (
      <View style={styles.container}>
        <TouchableWithoutFeedback
          onPress={this.startAnimation}
          onLayout={this.saveDimensions}
        >
          <Animated.View style={[styles.box, animatedStyles]} />
        </TouchableWithoutFeedback>
      </View>
    );
  }

Ending

This demo is not complex by any means, the purpose is to focus on how we can use the Dimensions of the screen and dynamically measure the view so we can move it around the screen. The animation doesn't have to be completed all at the same time either. Using sequence and other animation combinators like parallel and stagger we can progressively move items around using the same animated value. This is a crucial concept to understand especially when it comes to dealing with interpolations.

Live Demo

Code