Skip to content

Commit 3ab7703

Browse files
committed
React Native release 🎉
1 parent a1a3194 commit 3ab7703

File tree

3 files changed

+197
-4
lines changed

3 files changed

+197
-4
lines changed

Diff for: index.native.js

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
const React = require('react')
2+
const { View, PanResponder, Dimensions } = require('react-native')
3+
const { useSpring, animated } = require('react-spring/native')
4+
const { height, width } = Dimensions.get('window')
5+
6+
const settings = {
7+
maxTilt: 25, // in deg
8+
rotationPower: 50,
9+
swipeThreshold: 1 // need to update this threshold for RN (1.5 seems reasonable...?)
10+
}
11+
12+
// physical properties of the spring
13+
const physics = {
14+
touchResponsive: {
15+
friction: 50,
16+
tension: 2000
17+
},
18+
animateOut: {
19+
friction: 30,
20+
tension: 400
21+
},
22+
animateBack: {
23+
friction: 10,
24+
tension: 200
25+
}
26+
}
27+
28+
const pythagoras = (x, y) => {
29+
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
30+
}
31+
32+
const animateOut = async (gesture, setSpringTarget) => {
33+
const diagonal = pythagoras(height, width)
34+
const velocity = pythagoras(gesture.vx, gesture.vy)
35+
const finalX = diagonal * gesture.vx
36+
const finalY = diagonal * gesture.vy
37+
const finalRotation = gesture.vx * 45
38+
const duration = diagonal / velocity
39+
40+
setSpringTarget({
41+
x: finalX,
42+
y: finalY,
43+
rot: finalRotation, // set final rotation value based on gesture.vx
44+
config: { duration: duration }
45+
})
46+
47+
// for now animate back
48+
return await new Promise((resolve) =>
49+
setTimeout(() => {
50+
resolve()
51+
}, duration)
52+
)
53+
}
54+
55+
const animateBack = (setSpringTarget) => {
56+
// translate back to the initial position
57+
setSpringTarget({ x: 0, y: 0, rot: 0, config: physics.animateBack })
58+
}
59+
60+
const getSwipeDirection = (speed) => {
61+
if (Math.abs(speed.x) > Math.abs(speed.y)) {
62+
return speed.x > 0 ? 'right' : 'left'
63+
} else {
64+
return speed.y > 0 ? 'down' : 'up'
65+
}
66+
}
67+
68+
// must be created outside of the TinderCard forwardRef
69+
const AnimatedView = animated(View)
70+
71+
const TinderCard = React.forwardRef(
72+
(
73+
{ flickOnSwipe = true, children, onSwipe, onCardLeftScreen, className, preventSwipe = [] },
74+
ref
75+
) => {
76+
const [{ x, y, rot }, setSpringTarget] = useSpring(() => ({
77+
x: 0,
78+
y: 0,
79+
rot: 0,
80+
config: physics.touchResponsive
81+
}))
82+
83+
React.useImperativeHandle(ref, () => ({
84+
async swipe (dir = 'right') {
85+
if (onSwipe) onSwipe(dir)
86+
const power = 1.3
87+
const disturbance = (Math.random() - 0.5) / 2
88+
if (dir === 'right') {
89+
await animateOut({ vx: power, vy: disturbance }, setSpringTarget)
90+
} else if (dir === 'left') {
91+
await animateOut({ vx: -power, vy: disturbance }, setSpringTarget)
92+
} else if (dir === 'up') {
93+
await animateOut({ vx: disturbance, vy: power }, setSpringTarget)
94+
} else if (dir === 'down') {
95+
await animateOut({ vx: disturbance, vy: -power }, setSpringTarget)
96+
}
97+
if (onCardLeftScreen) onCardLeftScreen(dir)
98+
}
99+
}))
100+
101+
const handleSwipeReleased = React.useCallback(
102+
async (setSpringTarget, gesture) => {
103+
// Check if this is a swipe
104+
if (
105+
Math.abs(gesture.vx) > settings.swipeThreshold ||
106+
Math.abs(gesture.vy) > settings.swipeThreshold
107+
) {
108+
const dir = getSwipeDirection({ x: gesture.vx, y: gesture.vy })
109+
110+
if (flickOnSwipe) {
111+
if (!preventSwipe.includes(dir)) {
112+
if (onSwipe) onSwipe(dir)
113+
114+
await animateOut(gesture, setSpringTarget)
115+
if (onCardLeftScreen) onCardLeftScreen(dir)
116+
return
117+
}
118+
}
119+
}
120+
121+
// Card was not flicked away, animate back to start
122+
animateBack(setSpringTarget)
123+
},
124+
[flickOnSwipe, onSwipe, onCardLeftScreen, preventSwipe]
125+
)
126+
127+
const panResponder = React.useMemo(
128+
() =>
129+
PanResponder.create({
130+
// Ask to be the responder:
131+
onStartShouldSetPanResponder: (evt, gestureState) => true,
132+
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
133+
onMoveShouldSetPanResponder: (evt, gestureState) => true,
134+
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
135+
136+
onPanResponderGrant: (evt, gestureState) => {
137+
// The gesture has started.
138+
// Probably wont need this anymore as postion i relative to swipe!
139+
setSpringTarget({ x: gestureState.dx, y: gestureState.dy, rot: 0, config: physics.touchResponsive })
140+
},
141+
onPanResponderMove: (evt, gestureState) => {
142+
// use guestureState.vx / guestureState.vy for velocity calculations
143+
// translate element
144+
let rot = ((300 * gestureState.vx) / width) * 15// Magic number 300 different on different devices? Run on physical device!
145+
rot = Math.max(Math.min(rot, settings.maxTilt), -settings.maxTilt)
146+
setSpringTarget({ x: gestureState.dx, y: gestureState.dy, rot, config: physics.touchResponsive })
147+
},
148+
onPanResponderTerminationRequest: (evt, gestureState) => {
149+
return true
150+
},
151+
onPanResponderRelease: (evt, gestureState) => {
152+
// The user has released all touches while this view is the
153+
// responder. This typically means a gesture has succeeded
154+
// enable
155+
handleSwipeReleased(setSpringTarget, gestureState)
156+
}
157+
}),
158+
[]
159+
)
160+
161+
return (
162+
<AnimatedView
163+
{...panResponder.panHandlers}
164+
style={{
165+
transform: [
166+
{ translateX: x },
167+
{ translateY: y },
168+
{ rotate: rot.interpolate((rot) => `${rot}deg`) }
169+
]
170+
}}
171+
>
172+
{children}
173+
</AnimatedView>
174+
)
175+
}
176+
)
177+
178+
module.exports = TinderCard

Diff for: package.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"license": "MIT",
77
"files": [
88
"index.d.ts",
9-
"index.js"
9+
"index.js",
10+
"index.native.js"
1011
],
1112
"scripts": {
1213
"test": "standard && ts-readme-generator --check"
@@ -26,6 +27,12 @@
2627
"keywords": [
2728
"tinder",
2829
"card",
30+
"react-native",
31+
"native",
32+
"ios",
33+
"android",
34+
"web",
35+
"react",
2936
"swipeable",
3037
"swipe",
3138
"physics",

Diff for: readme.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
A react component to make swipeable elements like in the app tinder.
44

5+
## Compatibility
6+
- React
7+
- React Native
8+
9+
The import and api is identical for both Web and Native.
10+
511
## Demo
612
![](tinder.gif)
713

814
Try out the interactive demo <a href="https://3djakob.github.io/react-tinder-card-demo/">here.</a>
915

10-
Check out the demo repo <a href="https://github.com/3DJakob/react-tinder-card-demo">here.</a>
16+
Check out the Web demo repo <a href="https://github.com/3DJakob/react-tinder-card-demo">here.</a>
17+
18+
Check out the Native demo repo <a href="https://github.com/3DJakob/react-native-tinder-card-demo">here.</a>
1119

1220
## Installation
1321

@@ -37,13 +45,13 @@ return (
3745
)
3846
```
3947

40-
If you want more usage help check out the demo repository code [here.](https://github.com/3DJakob/react-tinder-card-demo/tree/master/src/examples)
48+
If you want more usage help check out the demo repository code: [Web](https://github.com/3DJakob/react-tinder-card-demo/tree/master/src/examples) / [Native](https://github.com/3DJakob/react-native-tinder-card-demo/tree/master/src/examples)
4149

4250
The simple example is the minimum code needed to get you started.
4351

4452
The advanced example implements a state to dynamically remove swiped elements as well as using buttons to trigger swipes.
4553

46-
Both code examples can be tested on the [demo page.](https://3djakob.github.io/react-tinder-card-demo/)
54+
Both Web code examples can be tested on the [demo page.](https://3djakob.github.io/react-tinder-card-demo/) The Native code examples can be cloned and runned using `expo start`.
4755

4856
## Props
4957

0 commit comments

Comments
 (0)