Floating Action Button with Menu Explanation
Intro
We’ll be rebuilding the starbucks app pay button. Not only does this have a floating button, it has 2 other floating buttons, and a circular background cover that shoots out to allow you to focus on the options. These other 2 floating buttons will appear when user will press the floating pay button with animation.Attached a gif file from where you can visualise how the animation will look like when we press the floating pay button
Setup
A standard setup, react-native-vector-icons library for buttons icons and an animated value. This animated value will only go from 0
to 1
so we can keep our animation reversible.
import React, {useState} from 'react';
import {
StyleSheet,
Text,
View,
Animated,
TouchableWithoutFeedback,
Image,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
function App(): JSX.Element {
const [animation] = useState(new Animated.Value(0));
return (
<View style={styles.container}/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
export default App;
Add Bottom Button
So first we need to add our main floating action button. We won’t be animating this button, but we will be animating the text.
<View style={styles.container}>
<TouchableWithoutFeedback onPress={toggleOpen}>
<View style={[styles.button, styles.pay]}>
<Animated.Text style={[styles.label]}>Pay</Animated.Text>
<Text style={styles.payText}>$5.00</Text>
</View>
</TouchableWithoutFeedback>
</View>
We’ll position our button in the corner and create a reusable style so all of our buttons will be the same shape and size. This will allow us to hide them behind our button then animate them visible. Then to make our button green we just add our pay
style to add a background color.
Additionally we position our text absolutely and render it inside of our button. Without adding any top/left/bottom/right
values it’ll float freely but still stay centered.
label: {
color: "#FFF",
position: "absolute",
fontSize: 18,
backgroundColor: "transparent",
},
button: {
width: 60,
height: 60,
alignItems: "center",
justifyContent: "center",
shadowColor: "#333",
shadowOpacity: 0.1,
shadowOffset: { x: 2, y: 0 },
shadowRadius: 2,
borderRadius: 30,
position: "absolute",
bottom: 20,
right: 20,
},
payText: {
color: "#FFF",
},
pay: {
backgroundColor: "#00B15E",
},
Add More Buttons
Now let’s add our other buttons. These will need to be animated, so we use an Animated.View
and choose the appropriate icons. Because our button
class is positioning everything in the same spot, and we have placed these buttons above our pay
button in the render, they will be rendered behind our pay
button.
<View style={styles.container}>
<TouchableWithoutFeedback>
<Animated.View style={[styles.button, styles.other]}>
<Animated.Text style={[styles.label]}>Order</Animated.Text>
<Icon name="food-fork-drink" size={20} color="#555" />
</Animated.View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback>
<Animated.View style={[styles.button, styles.other]}>
<Animated.Text style={[styles.label]}>Reload</Animated.Text>
<Icon name="reload" size={20} color="#555" />
</Animated.View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={toggleOpen}>
<View style={[styles.button, styles.pay]}>
<Animated.Text style={[styles.label]}>Pay</Animated.Text>
<Text style={styles.payText}>$5.00</Text>
</View>
</TouchableWithoutFeedback>
</View>
The only thing we need to do is specify their background color.
other: {
backgroundColor: "#FFF",
}
Add Hidden Background
We want a circular animated black opaque background, however, rather than making it hidden via opacity, we’ll just treat it like another button and tuck it behind the rest of the buttons.
<View style={styles.container}>
<Animated.View style={[styles.background, bgStyle]} />
<TouchableWithoutFeedback>
<Animated.View style={[styles.button, styles.other, orderStyle]}>
<Animated.Text style={[styles.label, labelStyle]}>Order</Animated.Text>
<Icon name="food-fork-drink" size={20} color="#555" />
</Animated.View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback>
<Animated.View style={[styles.button, styles.other, reloadStyle]}>
<Animated.Text style={[styles.label, labelStyle]}>Reload</Animated.Text>
<Icon name="reload" size={20} color="#555" />
</Animated.View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={toggleOpen}>
<View style={[styles.button, styles.pay]}>
<Animated.Text style={[styles.label, labelStyle]}>Pay</Animated.Text>
<Text style={styles.payText}>$5.00</Text>
</View>
</TouchableWithoutFeedback>
</View>
- Basically the same as the button styling.
background: {
backgroundColor: "rgba(0,0,0,.2)",
position: "absolute",
width: 60,
height: 60,
bottom: 20,
right: 20,
borderRadius: 30,
},
Setup Animation on Press
Because we don’t need to toggle pointer events on this animation we just need to save off on the instance whether or not our menu is opened or closed. Then decide to animate to 0
or 1
. This will produce a reversible animation that also can be interrupted at any point.
let _open: boolean;
const toggleOpen = () => {
if (_open) {
Animated.timing(animation, {
toValue: 0,
duration: 300,
useNativeDriver: false,
}).start();
} else {
Animated.timing(animation, {
toValue: 1,
duration: 300,
useNativeDriver: false,
}).start();
}
_open = !_open;
};
- Our reload button will be closest so we’ll offset it by
-70
giving us some padding from the pay button. Our order button will be the last button so we just need to offset it by-140
so it will bypass the reload button and also have some padding.
Additionally we’ll pass in our 0<=>1
animated value into scale so it will be moving and growing at the same time.
const reloadInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: [0, -70],
});
const orderInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: [0, -140],
});
const reloadStyle = {
transform: [
{
scale: animation
},
{
translateY: reloadInterpolate,
},
],
};
const orderStyle = {
transform: [
{
scale: animation
},
{
translateY: orderInterpolate,
},
],
};
Animate Labels
The label animations are the more difficult of the animations. They start hidden in the center of each individual button. However, we don’t want the text to appear over our icons and transition out. This would look bad.
However, what we can do is keep it hidden and keep animating its location. Then once we know it’s cleared the buttons of any overlap, we’ll fade it in. The text will always be offset by -30
and animate to an offset of -90
but to accomplish our fade in we’ll have it happen after our animation is 80%
complete. So we’ll make a cliff at that point and then quickly fade it into 1
over the last 20%
of the animation.
We also want all of our labels to do the same thing so we can pass the same label style into all of our labels.
const labelPositionInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: [-30, -90],
});
const opacityInterpolate = animation.interpolate({
inputRange: [0, 0.8, 1],
outputRange: [0, 0, 1],
});
const labelStyle = {
opacity: opacityInterpolate,
transform: [
{
translateX: labelPositionInterpolate,
},
],
};
Animate Background
Finally, our animated background is simply a scale of our small box. This is an arbitrary number selected, however, you could use math to calculate how many times the background needs to scale before it covers the entire view. I picked a large enough number to cover the screen and then some.
const scaleInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: [0, 30],
});
const bgStyle = {
transform: [
{
scale: scaleInterpolate,
},
],
};
You can download the code from this GitHub link https://github.com/amanmanhas/FloatingActionButton/blob/main/App.tsx
Thanks for reading this blog