Created
December 2, 2024 17:37
-
-
Save 1mehdifaraji/e291d0def96a880c29b8aa233dfeb500 to your computer and use it in GitHub Desktop.
React native coffee carousel animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useRef, useState } from "react"; | |
import { | |
StatusBar, | |
Image, | |
FlatList, | |
Dimensions, | |
Animated, | |
Text, | |
View, | |
SafeAreaView, | |
} from "react-native"; | |
import { | |
FlingGestureHandler, | |
Directions, | |
State, | |
GestureHandlerRootView, | |
} from "react-native-gesture-handler"; | |
const { width } = Dimensions.get("screen"); | |
const headerHeight = 70; | |
const eachItemWidth = width * 0.8; | |
const itemsToShow = 3; | |
const App = () => { | |
const [index, setIndex] = useState(0); | |
const [data, setData] = useState([ | |
{ | |
title: "Salted Caramel", | |
category: "Milkshake", | |
price: "12.49", | |
img: require("@/assets/images/coffee/salted-caramel-milkshake.png"), | |
}, | |
{ | |
title: "Peanut Butter", | |
category: "Milkshake", | |
price: "9.49", | |
img: require("@/assets/images/coffee/peanut-butter-milkshake.png"), | |
}, | |
{ | |
title: "Caramel", | |
category: "Machiato", | |
price: "5.78", | |
img: require("@/assets/images/coffee/caramel-macchiato.png"), | |
}, | |
{ | |
title: "Banana", | |
category: "Milkshake", | |
price: "5.78", | |
img: require("@/assets/images/coffee/banana-milkshake.png"), | |
}, | |
{ | |
title: "Chocolate", | |
category: "Milkshake", | |
price: "11.49", | |
img: require("@/assets/images/coffee/chocolate-milkshake.png"), | |
}, | |
{ | |
title: "Strawberry", | |
category: "Milkshake", | |
price: "7.20", | |
img: require("@/assets/images/coffee/strawberry-milkshake.png"), | |
}, | |
{ | |
title: "Brownie Island", | |
category: "Milkshake", | |
price: "13.49", | |
img: require("@/assets/images/coffee/brownie-island-milkshake.png"), | |
}, | |
]); | |
const xIndex = useRef(new Animated.Value(0)).current; | |
const animatedX = useRef(new Animated.Value(0)).current; | |
const setActiveItem = useCallback((activeIndex: number) => { | |
xIndex.setValue(activeIndex); | |
setIndex(activeIndex); | |
}, []); | |
useEffect(() => { | |
Animated.spring(animatedX, { | |
toValue: xIndex, | |
useNativeDriver: true, | |
speed: 5, | |
bounciness: 0, | |
overshootClamping: false, | |
}).start(); | |
}); | |
useEffect(() => { | |
// Loop carousel | |
if (index === data.length - itemsToShow - 1) { | |
const newData = [...data, ...data]; | |
setData(newData); | |
} | |
}); | |
return ( | |
<GestureHandlerRootView> | |
<FlingGestureHandler | |
key="left" | |
direction={Directions.LEFT | Directions.DOWN} | |
onHandlerStateChange={({ nativeEvent }) => { | |
if (nativeEvent.state === State.END) { | |
if (index === data.length - 1) return; | |
setActiveItem(index + 1); | |
} | |
}} | |
> | |
<FlingGestureHandler | |
key="right" | |
direction={Directions.RIGHT | Directions.UP} | |
onHandlerStateChange={({ nativeEvent }) => { | |
if (nativeEvent.state === State.END) { | |
if (index === 0) return; | |
setActiveItem(index - 1); | |
} | |
}} | |
> | |
<SafeAreaView | |
style={{ | |
flex: 1, | |
justifyContent: "center", | |
backgroundColor: "#fff", | |
}} | |
> | |
<StatusBar hidden /> | |
<Header data={data} animatedX={animatedX} /> | |
<FlatList | |
data={data} | |
keyExtractor={(_, index: number) => String(index)} | |
horizontal | |
inverted | |
contentContainerStyle={{ | |
flex: 1, | |
justifyContent: "center", | |
alignItems: "center", | |
}} | |
scrollEnabled={false} | |
removeClippedSubviews={false} | |
CellRendererComponent={({ | |
item, | |
index, | |
children, | |
style, | |
...props | |
}) => { | |
const newStyle = [style, { zIndex: data.length - index }]; | |
return ( | |
<View style={newStyle} key={index} {...props}> | |
{children} | |
</View> | |
); | |
}} | |
renderItem={({ item, index }) => { | |
const inputRange = [index - 1, index, index + 1]; | |
const translateX = animatedX.interpolate({ | |
inputRange, | |
outputRange: [80, 0, -400], | |
}); | |
const translateY = animatedX.interpolate({ | |
inputRange, | |
outputRange: [-60, 0, 400], | |
}); | |
const scale = animatedX.interpolate({ | |
inputRange, | |
outputRange: [0.9, 1.3, 0.2], | |
}); | |
return ( | |
<Animated.View | |
style={{ | |
position: "absolute", | |
left: -eachItemWidth / 1.6, | |
bottom: -200, | |
transform: [{ translateX }, { translateY }, { scale }], | |
width, | |
}} | |
> | |
<Image | |
source={item.img} | |
style={{ | |
width: eachItemWidth, | |
}} | |
/> | |
</Animated.View> | |
); | |
}} | |
/> | |
</SafeAreaView> | |
</FlingGestureHandler> | |
</FlingGestureHandler> | |
</GestureHandlerRootView> | |
); | |
}; | |
const Header = ({ data, animatedX }: any) => { | |
const translateY = animatedX.interpolate({ | |
inputRange: [-1, 0, 1], | |
outputRange: [headerHeight, 0, -headerHeight], | |
}); | |
return ( | |
<View | |
style={{ | |
height: headerHeight, | |
overflow: "hidden", | |
}} | |
> | |
<View> | |
{data.map((item: any, index: number) => ( | |
<Animated.View | |
key={index} | |
style={{ | |
height: headerHeight, | |
paddingHorizontal: 20, | |
paddingTop: 10, | |
flexDirection: "row", | |
justifyContent: "space-between", | |
alignItems: "flex-start", | |
transform: [ | |
{ | |
translateY, | |
}, | |
], | |
}} | |
> | |
<View> | |
<Text | |
style={{ | |
fontSize: 28, | |
fontWeight: 400, | |
letterSpacing: -1, | |
}} | |
> | |
{item.title} | |
</Text> | |
<Text style={{ fontSize: 12, color: "#666", fontWeight: 400 }}> | |
{item.category} | |
</Text> | |
</View> | |
{item.price ? ( | |
<View | |
style={{ | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "flex-end", | |
flexDirection: "row", | |
}} | |
> | |
<Text | |
style={{ | |
fontWeight: 400, | |
fontSize: 26, | |
letterSpacing: -1, | |
}} | |
> | |
£{item.price.split(".")[0]}. | |
</Text> | |
<Text | |
style={{ | |
marginBottom: 4, | |
letterSpacing: 0.5, | |
fontWeight: 500, | |
fontSize: 12, | |
}} | |
> | |
{item.price.split(".")[1]} | |
</Text> | |
</View> | |
) : null} | |
</Animated.View> | |
))} | |
</View> | |
</View> | |
); | |
}; | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment