DEV Community

Álvaro
Álvaro

Posted on

React Native Animation Series #2

Hi everybody! Alvaro here.
The part 1 of this post it's in medium, but I'll make the rest here from now.

Today we'll design a music player I found on dribble. All the credits to Charles Patterson, he inspired me to do this.

So, at the end of the post we'll have this:

Note that no audio will be played or the bar progressed, but if you want to, we can make it in another post!

To start, you can clone the repo from here and work on the master branch, but if you want to see the final code, switch to animations/music-player.

If you have the repo, you need to install one dependency, "react-native-paper" (yarn add react-native-paper / npm i react-native-paper). We are using the ProgressBar from this UI library.

Now, expo start, and... start!

In App.js I'm loading custom fonts, you can download roboto from google fonts, then put the files in assets/fonts.

To load the fonts, we'll use "expo-font", wait the component to be mounted, then render the music player.

If you never used custom fonts, in the expo docs are very well redacted how to load them!

import React, { useEffect, useState } from "react";

import * as Font from "expo-font";
import styled from "styled-components";

import MusicPlayer from "./src/MusicPlayer";

function App() {
  const [fontLoaded, setLoaded] = useState(false);

  useEffect(() => {
    loadFonts();
  }, []);

  const loadFonts = async () => {
    await Font.loadAsync({
      "roboto-bold": require("./assets/fonts/Roboto-Bold.ttf"),
      "roboto-light": require("./assets/fonts/Roboto-Light.ttf"),
      "roboto-medium": require("./assets/fonts/Roboto-Medium.ttf"),
      "roboto-thin": require("./assets/fonts/Roboto-Thin.ttf")
    });
    setLoaded(true);
  };

  return <Container>{fontLoaded && <MusicPlayer />}</Container>;
}

export default App;

const Container = styled.View`
  flex: 1;
  align-items: center;
  justify-content: center;
  background: #fff2f6;
`;

Enter fullscreen mode Exit fullscreen mode

It's not mandatory to load this fonts, you can use others!

If we save this file, we'll get an error because expo can't find the MusicPlayer, so, let's create it!

In src/ create MusicPlayer.js and make a dummy component to dismiss the error.

In today's tutorial to follow the design we won't use spring, but nevermind. And I'll introduce new methods on the Animated API called, parallel, to execute all the animations at the same time and loop, to repeat the same animation in loop.

Also, in the first tutorial I used classes, now we will use hooks (woho!).

I'll explain everything we need to do then at the end you'll find the code, so you can challenge yourself to make it without looking the solution :P.

1 - We need to import React and useState, styled, ProgressBar, TouchableOpacity, Animated and Easing to make our rotation animation without cuts.

import React, { useState } from "react";
import styled from "styled-components";
import { ProgressBar } from "react-native-paper";
import { TouchableOpacity, Animated, Easing } from "react-native";
Enter fullscreen mode Exit fullscreen mode

2 - We need 4 animations:

  • Move the info from the song to the top
  • Scale the disk when we press play
  • Rotate the disk when we press play
  • A bit opacity for the song's info

3 - A way to switch or toggle (state) between playing a song and not playing a song.

4 - Know how to interpolate the opacity and the rotation, but I'll give you the code here:

const spin = rotation.interpolate({
  inputRange: [0, 1],
  outputRange: ["0deg", "360deg"]
});

const opacityInterpolate = opacity.interpolate({
  inputRange: [0, 0.85, 1],
  outputRange: [0, 0, 1]
});
Enter fullscreen mode Exit fullscreen mode

The rotation and opacity can have 2 values, 0 and 1, and will progressively increasing to 0 to 1. So for the rotation, in example, when the value is 0.5, the output (the degrees) will be 180. In this case, the opacity, from 0 to 0.85 will be 0, and in that 0.15 the opacity will increase from 0 to 1.

5 - You need to choose a song! This step is very important, and I hope you choose a good one. The icons for back, next, play and pause are free to choose too, I'm using the ones on the designs, but you can import vector-icons from expo, or use your own pngs.

6 - Render conditionally the play/pause button, remember that we have a state telling us what we are doing!

7 - All the components that have animations need to be animated components, you can declare them as normal styled components then animated them with Animated:

const Image = styled.Image`
  width: 100px;
  height: 100px;
  position: absolute;
  left: 20px;
  top: -30px;
  border-radius: 50px;
`;

const AnimatedImage = Animated.createAnimatedComponent(Image);
Enter fullscreen mode Exit fullscreen mode

8 - Be patient if things goes wrong 1, 2 ... N time you try them, in the end we all learn.

Animated.parallel
This methods accepts an array of animations and execute all of them in parallel, there is a hint:

Animated.parallel([
  Animated.timing(translateY, { toValue: -70 }),
  Animated.timing(scale, { toValue: 1.2 }),
  rotationLoop(),
  Animated.timing(opacity, { toValue: 1 })
]).start();
Enter fullscreen mode Exit fullscreen mode

Animated.loop
This one accepts an animation to loop, and this is our rotation animation:

Animated.loop(
  Animated.timing(rotation, {
    toValue: 1,
    duration: 2500,
    easing: Easing.linear
  })
).start();
Enter fullscreen mode Exit fullscreen mode

Once we know how to do it, we need to toggle between playing or not playing the song... so how we do it? with state!

  const [toggled, setToggled] = useState(true);
Enter fullscreen mode Exit fullscreen mode

and we handle this with specific animations:

const onPress = () => {
  setToggled(!toggled);
  if (toggled) {
    Animated.parallel([
      Animated.timing(translateY, { toValue: -70 }),
      Animated.timing(scale, { toValue: 1.2 }),
      rotationLoop(),
      Animated.timing(opacity, { toValue: 1 })
    ]).start();
  } else {
    Animated.parallel([
      Animated.timing(translateY, { toValue: 0 }),
      Animated.timing(scale, { toValue: 1 }),
      Animated.timing(rotation, { toValue: 0 }),
      Animated.timing(opacity, { toValue: 0 })
    ]).start();
  }
};
Enter fullscreen mode Exit fullscreen mode

If you see, the rotation is in their own method, rotationLoop(), to make it more readable:

const rotationLoop = () => {
  return Animated.loop(
    Animated.timing(rotation, {
      toValue: 1,
      duration: 2500,
      easing: Easing.linear
    })
  ).start();
};
Enter fullscreen mode Exit fullscreen mode

If you followed the designs you have all the css there, but in case that not, these are the components I made:

const Container = styled.View`
  width: 326px;
  height: 99.5px;
  background: #ffffff;
  border-radius: 14px;
  box-shadow: 0 50px 57px #6f535b;
  justify-content: center;
  align-items: center;
`;

const Image = styled.Image`
  width: 100px;
  height: 100px;
  position: absolute;
  left: 20px;
  top: -30px;
  border-radius: 50px;
`;

const AnimatedImage = Animated.createAnimatedComponent(Image);

const DiskCenter = styled.View`
  width: 20px;
  height: 20px;
  border-radius: 10px;
  position: absolute;
  left: 60px;
  top: 10px;
  z-index: 10;
  background: #ffffff;
`;

const AnimatedDiskCenter = Animated.createAnimatedComponent(DiskCenter);

const Row = styled.View`
  flex-direction: row;
  align-items: center;
  height: 80px;
  width: 150px;
  justify-content: space-between;
  position: absolute;
  right: 30px;
`;

const Icon = styled.Image``;

const Playing = styled.View`
  background: rgba(255, 255, 255, 0.6);
  width: 300px;
  height: 85px;
  border-radius: 14px;
  z-index: -1;
  align-items: center;
  padding-top: 10px;
`;

const AnimatedPlaying = Animated.createAnimatedComponent(Playing);

const Column = styled.View`
  flex-direction: column;
  height: 100%;
  padding-left: 60px;
`;

const AnimatedColumn = Animated.createAnimatedComponent(Column);

const Artist = styled.Text`
  font-size: 15px;
  font-family: "roboto-bold";
  color: rgba(0, 0, 0, 0.7);
`;

const Title = styled.Text`
  font-size: 12px;
  font-family: "roboto-light";
  color: rgba(0, 0, 0, 0.7);
`;
Enter fullscreen mode Exit fullscreen mode

Following the hierarchy, the connections are pretty simple.
Here you have the complete code for the MusicPlayer.js:

import React, { useState } from "react";

import styled from "styled-components";
import { ProgressBar } from "react-native-paper";
import { TouchableOpacity, Animated, Easing } from "react-native";

const translateY = new Animated.Value(0);
const scale = new Animated.Value(1);
const rotation = new Animated.Value(0);
const opacity = new Animated.Value(0);

const MusicPlayer = () => {
  const [toggled, setToggled] = useState(true);

  const spin = rotation.interpolate({
    inputRange: [0, 1],
    outputRange: ["0deg", "360deg"]
  });

  const opacityInterpolate = opacity.interpolate({
    inputRange: [0, 0.85, 1],
    outputRange: [0, 0, 1]
  });

  const rotationLoop = () => {
    return Animated.loop(
      Animated.timing(rotation, {
        toValue: 1,
        duration: 2500,
        easing: Easing.linear
      })
    ).start();
  };

  const onPress = () => {
    setToggled(!toggled);
    if (toggled) {
      Animated.parallel([
        Animated.timing(translateY, { toValue: -70 }),
        Animated.timing(scale, { toValue: 1.2 }),
        rotationLoop(),
        Animated.timing(opacity, { toValue: 1 })
      ]).start();
    } else {
      Animated.parallel([
        Animated.timing(translateY, { toValue: 0 }),
        Animated.timing(scale, { toValue: 1 }),
        Animated.timing(rotation, { toValue: 0 }),
        Animated.timing(opacity, { toValue: 0 })
      ]).start();
    }
  };

  return (
    <Container>
      <AnimatedImage
        source={require("./cots.jpg")}
        style={{ transform: [{ scale }, { rotate: spin }] }}
      />
      <AnimatedDiskCenter style={{ transform: [{ scale }] }} />
      <Row>
        <Icon
          source={require("./back.png")}
          style={{ width: 23.46, height: 16.93 }}
        />
        <TouchableOpacity onPress={onPress}>
          {toggled ? (
            <Icon
              source={require("./play.png")}
              style={{ width: 23.46, height: 16.93 }}
            />
          ) : (
            <Icon
              source={require("./stop.png")}
              style={{ width: 20, height: 16.93 }}
            />
          )}
        </TouchableOpacity>
        <Icon
          source={require("./next.png")}
          style={{ width: 23.46, height: 16.93 }}
        />
      </Row>
      <AnimatedPlaying style={{ transform: [{ translateY }] }}>
        <AnimatedColumn style={{ opacity: opacityInterpolate }}>
          <Artist>Quinn XCII</Artist>
          <Title>Another day in paradise</Title>
          <ProgressBar
            progress={0.5}
            color="#FF8EAB"
            style={{ width: 150, position: "absolute", bottom: 25, left: 60 }}
          />
        </AnimatedColumn>
      </AnimatedPlaying>
    </Container>
  );
};

export default MusicPlayer;

const Container = styled.View`
  width: 326px;
  height: 99.5px;
  background: #ffffff;
  border-radius: 14px;
  box-shadow: 0 50px 57px #6f535b;
  justify-content: center;
  align-items: center;
`;

const Image = styled.Image`
  width: 100px;
  height: 100px;
  position: absolute;
  left: 20px;
  top: -30px;
  border-radius: 50px;
`;

const AnimatedImage = Animated.createAnimatedComponent(Image);

const DiskCenter = styled.View`
  width: 20px;
  height: 20px;
  border-radius: 10px;
  position: absolute;
  left: 60px;
  top: 10px;
  z-index: 10;
  background: #ffffff;
`;

const AnimatedDiskCenter = Animated.createAnimatedComponent(DiskCenter);

const Row = styled.View`
  flex-direction: row;
  align-items: center;
  height: 80px;
  width: 150px;
  justify-content: space-between;
  position: absolute;
  right: 30px;
`;

const Icon = styled.Image``;

const Playing = styled.View`
  background: rgba(255, 255, 255, 0.6);
  width: 300px;
  height: 85px;
  border-radius: 14px;
  z-index: -1;
  align-items: center;
  padding-top: 10px;
`;

const AnimatedPlaying = Animated.createAnimatedComponent(Playing);

const Column = styled.View`
  flex-direction: column;
  height: 100%;
  padding-left: 60px;
`;

const AnimatedColumn = Animated.createAnimatedComponent(Column);

const Artist = styled.Text`
  font-size: 15px;
  font-family: "roboto-bold";
  color: rgba(0, 0, 0, 0.7);
`;

const Title = styled.Text`
  font-size: 12px;
  font-family: "roboto-light";
  color: rgba(0, 0, 0, 0.7);
`;

Enter fullscreen mode Exit fullscreen mode

If you found this useful and/or fun, share this, leave a like or a comment, and If you want me to change something or make more animations send me them and I will!

As always, thanks!

GitHub logo AlvaroJSnish / react-native-animation-series

A series of react native animations!

Animation Series

Every branch contains an animation, and we are creating them here! and here

If you want to learn animations with React Native, clone the repo and follow the tutorials!

alvarojsnish image

Top comments (0)