Where we want to get to is to have the data pulled in passed onto the Deck
component that handles the card list
constructing. The Card list will be a sequence that is rendered on top of each other. Another thing to note is that the
only card that has the ability to rotate is the one on top, the others are static until the choice is made for the one
on top…essentially the deck behaves as a stack and you swipe the top off.
We need to refactor the panResponder
to get to allow us to move things around so let’s do that.
Our Deck
component will look like this now:
// Deck.js
import React, { useRef } from 'react'
import { Animated, PanResponder } from 'react-native'
import CoffeeShop from './CoffeeShop'
export default ({ data }) => {
const position = useRef(new Animated.ValueXY()).current
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
position.setOffset({
x: position.x._value,
y: position.y._value,
})
},
onPanResponderMove: (_, gesture) => {
position.setValue({ x: gesture.dx, y: gesture.dy })
},
onPanResponderRelease: () => {
position.flattenOffset()
},
})
).current
return (
<Animated.View
style={position.getLayout()}
{...panResponder.panHandlers}
>
{data.map((item) => (
<CoffeeShop item={item} />
))}
</Animated.View>
)
}
Previously we have used getTranslateTransform()
to build the animation styles directly in the style prop. There is
another method that allows us to retrieve transform styles that is getLayout()
. This method will not work with
useNativeDriver: true
set.
What we can do now is drag the entire deck around the screen along the x and y axis.
The basic UX we are aiming for is also to rotate the animated element. In order to do this we want to give our list the
ability to rotate. We can do this via transform: { rotate: '<x>deg' }
similar to what we would do in regular css.
There is one catch though, as we drag the list we also want to rotate it more. What this means is that the angle of
rotation needs to be linearly correlated with the distance the card has moved on the x-axis
. This is done via
interpolation. The interpolation system allows us to associate two scales and create a linear relationship between them
so that they change in the same way(in our case when we swipe in a specific direction the rotation angle changes in the
way the scale defines it).
Given that the style for the Animated.View
has become quite complicated we want to extract it into its own function
and we can use it in the style prop of the animated element.
// Deck.js
...
const getCardStyle = (position) => {
const rotate = position.x.interpolate({
inputRange: [-500, 0, 500],
outputRange: ['-120deg', '0deg', '120deg'],
})
return {
transform: [...position.getTranslateTransform(),{ rotate }],
}
}
...
return (
<Animated.View style={getCardStyle(position)} {...panResponder.panHandlers}>
...
We can now rotate the card on the screen, next stop we want to enable some kind of threshold where we understand that the swipe to the left or right has actually been intentional and the user wanted to express liking a specific coffee shop, otherwise we want the card to spring back to its original state.
// Deck.js
const resetPosition = () => {
Animated.spring(position, {
toValue: { x: 0, y: 0 },
useNativeDriver: true
}).start()
}
We want to hook this into the onPanResponderRelease
callback like so:
// Deck.js
...
onPanResponderRelease: (event, gesture) => {
if (gesture.dx > SWIPE_THRESHOLD) {
forceSwipe('right')
} else if (gesture.dx < -SWIPE_THRESHOLD) {
forceSwipe('left')
} else {
resetPosition()
}
},
...
The SWIPE_THRESHOLD
is a constant that is 1/4 of the maximum of the interpolation interval(500) = 125.
SWIPE_THRESHOLD
Let’s do this in the next section which is also an opportunity for few short sprints of individual work improve our understanding the concepts we learned so far.