I am trying to create this animation in react native
https://raw.githubusercontent.com/Ramotion/react-native-circle-menu/HEAD/preview.gif
Since the library is no longer maintained it doesnt work anymore in modern expo
I was able to create the radial menu and almost the circular animation but facing some problems
My radial menu Code
//ActionButtonItem.js
import React, { Component } from "react";
import { StyleSheet, View, Animated, TouchableOpacity, Platform } from "react-native";
import PropTypes from "prop-types";
export const iosElevation = (forIos, forAndroid) => {
return (
Platform.OS === "ios" && {
shadowOffset: { width: -2, height: 4 },
shadowColor: "#171717",
shadowOpacity: 0.2,
shadowRadius: 3,
}
);
};
export default class ActionButtonItem extends Component {
handlePress = (e) => {
e.stopPropagation(); // Prevents the parent ActionButton from closing
this.props.onPress && this.props.onPress(e);
};
render() {
const offsetX = this.props.radius * Math.cos(this.props.angle);
const offsetY = this.props.radius * Math.sin(this.props.angle);
return (
<Animated.View
style={[
{
opacity: this.props.anim,
width: this.props.size,
height: this.props.size,
transform: [
{
translateY: this.props.anim.interpolate({
inputRange: [0, 1],
outputRange: [0, offsetY],
}),
},
{
translateX: this.props.anim.interpolate({
inputRange: [0, 1],
outputRange: [0, offsetX],
}),
},
{
rotate: this.props.anim.interpolate({
inputRange: [0, 1],
outputRange: [
`${this.props.startDegree}deg`,
`${this.props.endDegree}deg`,
],
}),
},
{
scale: this.props.anim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
},
],
},
]}
>
<TouchableOpacity
style={{ flex: 1 }}
activeOpacity={this.props.activeOpacity || 0.85}
onPress={this.handlePress}
>
<View
style={[
styles.actionButton,
{
width: this.props.size,
height: this.props.size,
borderRadius: this.props.size / 2,
backgroundColor: this.props.buttonColor,
elevation: 15,
},
iosElevation(),
]}
>
{this.props.children}
</View>
</TouchableOpacity>
</Animated.View>
);
}
}
ActionButtonItem.propTypes = {
angle: PropTypes.number,
radius: PropTypes.number,
buttonColor: PropTypes.string,
onPress: PropTypes.func,
children: PropTypes.node.isRequired,
startDegree: PropTypes.number,
endDegree: PropTypes.number,
};
ActionButtonItem.defaultProps = {
onPress: () => {},
startDegree: 0,
endDegree: 720,
};
const styles = StyleSheet.create({
actionButton: {
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
paddingTop: 2,
shadowOpacity: 0.3,
shadowOffset: {
width: 0,
height: 1,
},
shadowColor: "#444",
shadowRadius: 1,
backgroundColor: "red",
position: "absolute",
},
});
//ActionButton.js
import React, { Component } from "react";
import {
StyleSheet,
Text,
View,
Animated,
TouchableOpacity,
TouchableWithoutFeedback,
} from "react-native";
import PropTypes from "prop-types";
import ActionButtonItem, { iosElevation } from "./ActionButtonItem";
import { RFPercentage } from "react-native-responsive-fontsize";
import * as Progress from "react-native-progress";
const alignMap = {
center: {
alignItems: "center",
justifyContent: "flex-end",
startDegree: 180,
endDegree: 360,
},
left: {
alignItems: "flex-start",
justifyContent: "flex-end",
startDegree: 270,
endDegree: 360,
},
right: {
alignItems: "flex-end",
justifyContent: "flex-end",
startDegree: 180,
endDegree: 270,
},
radial: {
alignItems: "center",
justifyContent: "center",
startDegree: 0,
endDegree: 360,
},
};
export default class ActionButton extends Component {
constructor(props) {
super(props);
this.state = {
active: props.active,
anim: new Animated.Value(props.active ? 1 : 0),
};
this.timeout = null;
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
getActionButtonStyle() {
return [styles.actionBarItem, this.getButtonSize()];
}
getActionContainerStyle() {
const { alignItems, justifyContent } = alignMap[this.props.position];
return [
styles.overlay,
styles.actionContainer,
{
alignItems,
justifyContent,
},
];
}
getActionsStyle() {
return [this.getButtonSize()];
}
getButtonSize() {
return {
width: this.props.size,
height: this.props.size,
};
}
animateButton() {
if (this.state.active) {
this.reset();
return;
}
Animated.spring(this.state.anim, {
toValue: 1,
duration: 250,
friction: 5,
tension: 100,
useNativeDriver: true,
}).start();
this.setState({ active: true });
}
reset() {
Animated.spring(this.state.anim, {
toValue: 0,
duration: 250,
friction: 5,
tension: 100,
useNativeDriver: true,
}).start();
setTimeout(() => {
this.setState({ active: false });
}, 250);
}
renderButton() {
return (
<View style={this.getActionButtonStyle()}>
<TouchableOpacity
activeOpacity={0.85}
onLongPress={this.props.onLongPress}
onPress={() => {
this.props.onPress();
if (this.props.children) {
this.animateButton();
}
}}
>
<Animated.View
style={[
styles.btn,
iosElevation(),
{
width: this.props.size - 5,
height: this.props.size - 5,
elevation: RFPercentage(5),
borderRadius: this.props.size / 2,
backgroundColor: this.state.anim.interpolate({
inputRange: [0, 1],
outputRange: [this.props.buttonColor, this.props.btnOutRange],
}),
},
{
top: 5,
transform: [
{
scale: this.state.anim.interpolate({
inputRange: [0, 1],
outputRange: [1, this.props.outRangeScale],
}),
},
{
rotate: this.state.anim.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", this.props.degrees + "deg"],
}),
},
],
},
// {backgroundColor:'red'}
]}
>
{this.renderButtonIcon()}
</Animated.View>
</TouchableOpacity>
</View>
);
}
renderButtonIcon() {
if (this.props.icon) {
return this.props.icon;
}
return (
<Animated.Text
style={[
styles.btnText,
{
color: this.state.anim.interpolate({
inputRange: [0, 1],
outputRange: [
this.props.buttonTextColor,
this.props.btnOutRangeTxt,
],
}),
},
]}
>
+
</Animated.Text>
);
}
renderActions() {
if (!this.state.active) return null;
const startDegree =
this.props.startDegree || alignMap[this.props.position].startDegree;
const endDegree =
this.props.endDegree || alignMap[this.props.position].endDegree;
const startRadian = (startDegree * Math.PI) / 180;
const endRadian = (endDegree * Math.PI) / 180;
const childrenCount = React.Children.count(this.props.children);
let offset = 0;
if (childrenCount !== 1) {
offset = (endRadian - startRadian) / (childrenCount || 1);
}
return React.Children.map(this.props.children, (button, index) => {
return (
<View pointerEvents="box-none" style={this.getActionContainerStyle()}>
<ActionButtonItem
key={index}
position={this.props.position}
anim={this.state.anim}
size={this.props.itemSize}
radius={this.props.radius}
angle={startRadian + index * offset}
btnColor={this.props.btnOutRange}
{...button.props}
onPress={() => {
if (this.props.autoInactive) {
this.timeout = setTimeout(() => {
this.reset();
}, 200);
}
button.props.onPress();
}}
/>
</View>
);
});
}
render() {
let backdrop;
if (this.state.active) {
backdrop = (
<TouchableWithoutFeedback
style={styles.overlay}
onPress={() => {
this.reset();
this.props.onOverlayPress();
}}
>
<Animated.View
style={{
backgroundColor: this.props.bgColor,
opacity: this.state.anim,
flex: 1,
}}
>
{this.props.backdrop}
</Animated.View>
</TouchableWithoutFeedback>
);
}
return (
<View pointerEvents="box-none" style={styles.overlay}>
{backdrop}
{this.props.children && this.renderActions()}
<View pointerEvents="box-none" style={this.getActionContainerStyle()}>
{this.renderButton()}
</View>
</View>
);
}
}
ActionButton.Item = ActionButtonItem;
ActionButton.propTypes = {
active: PropTypes.bool,
bgColor: PropTypes.string,
buttonColor: PropTypes.string,
buttonTextColor: PropTypes.string,
size: PropTypes.number,
itemSize: PropTypes.number,
autoInactive: PropTypes.bool,
onPress: PropTypes.func,
onOverlayPress: PropTypes.func,
backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
startDegree: PropTypes.number,
endDegree: PropTypes.number,
radius: PropTypes.number,
children: PropTypes.node,
position: PropTypes.oneOf(["left", "center", "right","radial"]),
};
ActionButton.defaultProps = {
active: false,
bgColor: "transparent",
buttonColor: "rgba(0,0,0,1)",
buttonTextColor: "rgba(255,255,255,1)",
position: "center",
outRangeScale: 1,
autoInactive: true,
onPress: () => {},
onOverlayPress: () => {},
backdrop: false,
degrees: 135,
size: 63,
itemSize: 36,
radius: 100,
btnOutRange: "rgba(0,0,0,1)",
btnOutRangeTxt: "rgba(255,255,255,1)",
};
const styles = StyleSheet.create({
overlay: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
top: 0,
backgroundColor: "transparent",
},
actionContainer: {
flexDirection: "column",
// padding: 10,
},
actionBarItem: {
alignItems: "center",
justifyContent: "center",
backgroundColor: "transparent",
},
btn: {
justifyContent: "center",
alignItems: "center",
// shadowOpacity: 0.3,
// shadowOffset: {
// width: 0,
// height: 1,
// },
// shadowColor: '#444',
// shadowRadius: 1,
},
btnText: {
marginTop: -4,
fontSize: 24,
backgroundColor: "transparent",
position: "relative",
},
});
Giving output as
Now I want the radial animation and here is my attempt
//RadialAnimation.js
import React, { useEffect } from "react";
import { View, StyleSheet } from "react-native";
import Svg, { Circle } from "react-native-svg";
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
useAnimatedStyle,
Easing,
} from "react-native-reanimated";
import Icon from "react-native-vector-icons/Ionicons";
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const RadialProgressAnimation = ({
color = "blue",
radius = 150,
icon = "ios-add",
height,
width,
anglez,
}) => {
const progress = useSharedValue(0);
const circumference = 2 * Math.PI * radius;
useEffect(() => {
progress.value = withTiming(1.7, {
duration: 1400,
easing: Easing.linear,
});
}, []);
const animatedProps = useAnimatedProps(() => {
return {
strokeDashoffset: circumference * (1 - progress.value),
};
});
const iconStyle = useAnimatedStyle(() => {
const angle = progress.value * 360;
return {
transform: [
{ translateX: radius },
{ rotate: `${angle}deg` },
{ translateY: progress.value },
],
};
});
return (
<View style={styles.container}>
<Svg
height={radius * 2.5} // Adding some extra space to avoid clipping
width={radius * 2.5}
style={{
transform: [{ rotate: `${anglez}deg` }],
flex: 1,
}}
>
<AnimatedCircle
cx={radius} //15 for right
cy={radius}
r={radius}
fill="transparent"
stroke={color}
strokeWidth={100}
strokeLinecap="round"
strokeDasharray={`${circumference}, ${circumference}`}
animatedProps={animatedProps}
style={{ flex: 1 }}
/>
</Svg>
<Animated.View
style={[
iconStyle,
{
position: "absolute",
backgroundColor: "green",
flex: 1,
},
]}
>
<Icon
name={icon}
size={32}
color={"white"}
style={{ backgroundColor: color }}
/>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "auto",
width: "auto",
margin: 30,
zIndex: 150,
},
});
export default RadialProgressAnimation;
//Index.js
import { useState } from "react";
import React, { StyleSheet, View, useWindowDimensions } from "react-native";
import ActionButton from "@kashyap_trivedi/react-native-circular-action-menu";
import Icon from "react-native-vector-icons/Ionicons";
import RadialProgressAnimation from "./components/RadialAnimation";
const CircleMenu = () => {
const [activeButton, setActiveButton] = useState(null);
const handleButtonClick = (color, icon, anglez) => {
setActiveButton({ color, icon, anglez });
// Reset after animation (duration + a little extra)
setTimeout(() => setActiveButton(null), 1100);
};
const { height, width } = useWindowDimensions();
return (
<View
style={{
flex: 1,
backgroundColor: "#f3f3f3",
position: "absolute",
top: height / 2,
left: width / 2,
}}
>
{activeButton && (
<View
style={{
flex: 1,
position: "absolute",
zIndex: 50,
height: "100%",
width: "100%",
}}
>
<RadialProgressAnimation
color={activeButton.color}
icon={activeButton.icon}
anglez={activeButton.anglez}
height={height}
width={width}
radius={150}
/>
</View>
)}
<ActionButton
buttonColor="rgba(231,76,60,1)"
radius={150}
btnOutRange={"rgba(231,76,60,1)"}
degrees={360}
size={90}
itemSize={90}
position="radial"
autoInactive={false}
>
<ActionButton.Item
buttonColor="#9b59b6"
title="New Task"
onPress={() => handleButtonClick("#9b59b6", "navigate", 360)}
>
<Icon name="navigate" style={styles.actionButtonIcon} />
</ActionButton.Item>
<ActionButton.Item
buttonColor="#3498db"
title="Notifications"
onPress={() => handleButtonClick("#3498db", "accessibility", 90)}
>
<Icon name="accessibility" style={styles.actionButtonIcon} />
</ActionButton.Item>
<ActionButton.Item
buttonColor="#1abc9c"
title="All Tasks"
onPress={() => handleButtonClick("#1abc9c", "menu", 180)}
>
<Icon name="menu" style={styles.actionButtonIcon} />
</ActionButton.Item>
<ActionButton.Item
buttonColor="#1abc9c"
title="All ok"
onPress={() => handleButtonClick("#1abc9c", "home", 270)}
>
<Icon name="home" style={styles.actionButtonIcon} />
</ActionButton.Item>
</ActionButton>
</View>
);
};
export default CircleMenu;
const styles = StyleSheet.create({
actionButtonIcon: {
fontSize: 20,
height: 22,
color: "white",
},
});
Leading to this
https://imgur.com/a/aRPip7T
Now how to solve these problems in the animation
1)RadialAnimation gets clipped even on setting a higher height and width
2)Animation should start from bottom of the button
3)Button Icon should translate in the radial progress animation