Skip to content

Instantly share code, notes, and snippets.

@1mehdifaraji
Created December 2, 2024 17:37
Show Gist options
  • Save 1mehdifaraji/e291d0def96a880c29b8aa233dfeb500 to your computer and use it in GitHub Desktop.
Save 1mehdifaraji/e291d0def96a880c29b8aa233dfeb500 to your computer and use it in GitHub Desktop.
React native coffee carousel animation
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