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;
`;
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";
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]
});
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);
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();
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();
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);
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();
}
};
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();
};
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);
`;
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);
`;
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!
Top comments (0)