Skip to content

Instantly share code, notes, and snippets.

@efstathiosntonas
Last active December 19, 2022 19:57
Show Gist options
  • Save efstathiosntonas/ae446fca58844ec1717dc78d2abb2bdd to your computer and use it in GitHub Desktop.
Save efstathiosntonas/ae446fca58844ec1717dc78d2abb2bdd to your computer and use it in GitHub Desktop.
React Native card swiper like iOS iMessage multiple images swiper
import React, {FC} from 'react';
import {Dimensions, StyleSheet, View} from 'react-native';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
Easing,
interpolate,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import Card from './Card';
import {Colors} from './Colors';
const {width, height} = Dimensions.get('window');
interface CardContainerProps {
color: string;
id: number;
index: number;
priority: Animated.SharedValue<number[]>;
}
const CardContainer: FC<CardContainerProps> = ({
color,
id,
index,
priority,
}) => {
const xTranslation = useSharedValue(0);
const rotation = useSharedValue(0);
const isRightFlick = useSharedValue(true);
const gesture = Gesture.Pan()
.onBegin(({absoluteX, translationX}) => {
if (absoluteX < width / 2) {
isRightFlick.value = false;
}
xTranslation.value = translationX + 30;
rotation.value = translationX + 5;
})
.onUpdate(({translationX}) => {
rotation.value = translationX + 5;
xTranslation.value = translationX + 30;
})
.onEnd(() => {
const priorities = [...priority.value];
const lastItem = priorities[priorities.length - 1];
for (let i = priorities.length - 1; i > 0; i--) {
priorities[i] = priorities[i - 1];
}
priorities[0] = lastItem;
priority.value = priorities;
xTranslation.value = withTiming(
30,
{
duration: 400,
easing: Easing.quad,
},
() => {
isRightFlick.value = true;
},
);
rotation.value = withTiming(
10,
{
duration: 400,
easing: Easing.linear,
},
() => {
rotation.value = 30;
},
);
});
const style = useAnimatedStyle(() => {
const getPosition = (idx: number) => {
switch (idx) {
case 1:
return 50;
case 0.9:
return 75;
case 0.8:
return 100;
case 0.7:
return 125;
case 0.6:
return 150;
default:
return 0;
}
};
return {
position: 'absolute',
height: 400,
width: 200,
backgroundColor: color,
bottom: withTiming(getPosition(index), {duration: 500}),
borderRadius: 8,
zIndex: priority.value[index] * 100,
transform: [
{
translateX: xTranslation.value,
},
{
rotate: `${interpolate(
rotation.value,
isRightFlick.value ? [30, height] : [30, -height],
[0, 4],
)}rad`,
},
{
scale: withTiming(priority.value[index], {
duration: 250,
easing: Easing.linear,
}),
},
],
};
});
return (
<GestureDetector gesture={gesture}>
<Card id={id} style={style} />
</GestureDetector>
);
};
const App = () => {
const arrayLength = 5;
const priority = useSharedValue([1, 0.9, 0.8, 0.7, 0.6]);
const colors = [
Colors.LIGHT_GOLD,
Colors.DARK_RED,
Colors.LIGHT_BLUE,
Colors.GREEN,
Colors.YELLOW,
];
return (
<GestureHandlerRootView style={styles.rootView}>
<View style={styles.container}>
{Array.from({length: arrayLength}).map((_, index) => {
return (
<CardContainer
color={colors[index]}
id={index}
index={index}
key={index}
priority={priority}
/>
);
})}
</View>
</GestureHandlerRootView>
);
};
const styles = StyleSheet.create({
rootView: {
flex: 1,
},
container: {
flex: 1,
alignItems: 'center',
backgroundColor: 'black',
},
});
export default App;
import React, {FC} from 'react';
import {StyleSheet, View} from 'react-native';
import Animated from 'react-native-reanimated';
import {Colors} from './Colors';
interface CardProps {
id: number;
style: object;
}
const Card: FC<CardProps> = ({id, style}) => {
const getColor = () => {
switch (id) {
case 0:
return Colors.DARK_BLUE;
case 1:
return Colors.DARK_RED;
case 2:
return Colors.DARK_GOLD;
}
};
return (
<Animated.View style={style}>
<View style={cardStyle.spacer} />
<View style={cardStyle.container}>
<View style={[cardStyle.circle, {backgroundColor: getColor()}]} />
</View>
</Animated.View>
);
};
const cardStyle = StyleSheet.create({
spacer: {
flex: 1,
},
container: {
flexDirection: 'row',
},
circle: {
height: 80,
width: 80,
borderRadius: 40,
marginBottom: 20,
marginLeft: 15,
},
topLine: {
height: 20,
width: 120,
borderRadius: 40,
marginBottom: 20,
marginLeft: 15,
},
bottomLine: {
height: 20,
width: 60,
borderRadius: 40,
marginBottom: 20,
marginLeft: 15,
},
});
export default Card;
export const Colors = {
LIGHT_BLUE: '#afd0ff',
LIGHT_GOLD: '#e8d38f',
LIGHT_RED: '#ff7e85',
DARK_BLUE: '#4a64a8',
DARK_GOLD: '#85692a',
DARK_RED: '#992e1e',
GREEN: 'green',
YELLOW: 'yellow',
PINK: 'pink',
};
{
"name": "CardShiftAnimationScratch",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"react": "18.0.0",
"react-native": "0.69.5",
"react-native-gesture-handler": "^2.8.0",
"react-native-reanimated": "^2.13.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/runtime": "^7.12.5",
"@react-native-community/eslint-config": "^2.0.0",
"@tsconfig/react-native": "^2.0.2",
"@types/jest": "^26.0.23",
"@types/react-native": "^0.69.5",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"babel-jest": "^26.6.3",
"eslint": "^7.32.0",
"jest": "^26.6.3",
"metro-react-native-babel-preset": "^0.70.3",
"react-test-renderer": "18.0.0",
"typescript": "^4.4.4"
},
"jest": {
"preset": "react-native",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}
@efstathiosntonas
Copy link
Author

demo:

Simulator.Screen.Recording.-.iPhone.13.-.2022-12-19.at.11.40.08.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment