A card flip animation in React Native with Hooks
Before I begin, I should mention it’s possible I won’t be blogging as frequently in the near future thanks to an unbelievable new opportunity! It’s not final yet but I’m so excited to share more soon!
I love long-form writing, but it’s a little exhausting and soon I’ll have even less free time for it. So, when I’m back to blogging, this Medium account will look a little different. It’ll be more a notepad of ideas, which fits the way I think anyway, and not as many essays (though I still have to finish my Express/Sequelize/PostreSQL tutorial).
Now that’s out of the way, let’s get back to familiar territory for this blog: React, Poker and front-end design…
Background
I’ve blogged about React before but never about React Native. React Native is to mobile app development what React is to web app development: the two are sister frameworks, both created by Facebook engineers, with similar purpose, concepts and principles. (React is not the same as React Native and copy-pasting code from one into the other often doesn’t work!)
Forging ahead with a mobile Poker game in React Native, I wanted to implement a card-flip animation for playing card images:
The tutorials I saw for doing this are great, but I’ve never seen an updated walkthrough for React Native’s hooks. Big shout-out to Jason Brown whose approach in this awesome lecture video I updated for this brief walkthrough. I love hooks in React — let’s see if they’re just as awesome in React Native!
Components in React Native
A quick look at Card.js
will show you a bit of the latest and greatest from React Native’s component library:
import { Image, View, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) { ... return (
<View
style={ style.cardWrapper }
>
<Image
style={ style.cardFront }
source={ require( ... ) }
/>
<Image
style={ style.cardBack } }
source={ require( ... ) }
/>
</View>
);}const style = StyleSheet.create( {
cardWrapper: { ... },
cardFront: {
...
position: "absolute",
},
cardBack: {
...
backfaceVisibility: "hidden"
},
};
Some important notes:
- The basic building block of React Native is the
View
, which functions as an all-purposediv
orsection
tag. Native also gives us other familiar DOM elements likeText
,TextInput
s,Switch
es,Button
s and several kinds ofList
s in addition to theImage
s we see above. Soon we’ll respond to finger touches by replacingView
with a component calledPressable
— an update to an existing component calledTouchable
. - You can probably tell from the
dispatch
prop that yes, a dialect of Redux is available in React Native — again, different from Redux in React. A full tutorial on Redux in React Native is beyond this article’s scope so I’m not covering it. Consult the Redux docs or find one of the many great tutorials floating around if you’d like to learn more. - We don’t style React Native with CSS: it gives us a
StyleSheet
class instead, initialized with a JS object where eachstyle
is a key; values are themselves objects nearly identical to Reactstyle
objects, with options likemargin
andposition
anddisplay
familiar from CSS. We then apply those styles to a component with thestyle={}
attribute, which is broadly similar toclassName
in React. - Notice in our component, we have two images ( for the card front and back); see that in our
StyleSheet
we place thecardFront
on top withposition: "absolute"
and usebackfaceVisibility
to hide thecardBack
when the card is flipped. - Assets like local images must be
require()
d! Also note that you must hard-write sources for everyrequire()
: you can’t userequire()
with an interpolated string in React Native, as in:require( `./assets/images/logos/${ colorName }.png` )
.
The Animated
component
Let’s get a big round of applause now for the main event: the Animated
class & components. It’s a solid and snazzy little CSS animation library with decent range, and we’ll get started using it with some definitions:
import { Animated, Image, View, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {const flipAnimation = useRef( new Animated.Value( 0 ) ).current;let flipRotation = 0;
flipAnimation.addListener( ( { value } ) => flipRotation = value );const flipToFrontStyle = {
transform: [
{ rotateY: flipAnimation.interpolate( {
inputRange: [ 0, 180 ],
outputRange: [ "0deg", "180deg" ]
} ) }
]
};const flipToBackStyle = {
transform: [
{ rotateY: flipAnimation.interpolate( {
inputRange: [ 0, 180 ],
outputRange: [ "180deg", "360deg" ]
} ) }
]
};...return ( ... );}
I think this brain bomb will make more sense after a breakdown:
- First let’s import
Animated
and create anew Animated.Value()
that starts at0
.Animated.Value()s
help us keep track of where we are in an animation. Before hooks, we would have done this by creating something likethis.animation
— but here we’ll use the hook equivalent,useRef().current
. I’ve said before that useRef() is a godsend in desktop React and it’s just as useful here. - We’re also keeping track of where we are in an animation using
Animated.Value().addListener()
, which does… exactly what it says on the box, giving us use of aflipRotation
variable so we can tell if a card has been flipped yet. Note that inaddListener
, the{ value }
must be deconstructed from the callback’s arguments! - We need some additional
style
s, one each for the front and back, and unfortunately the nesting here is an affront to decency. (You can define more variables but I don’t think it helps much.) Each style is an object with a single key,transform
, corresponding to an array oftransform
objects. - Keep in mind,
transform
s likerotateY
need text strings as values — so theserotateY
objects use a function calledAnimated.Value().interpolate()
, which translates anAnimated.Value()
to a string of anywhere from"0deg"
to"180deg"
to"360deg"
(depending on where we happen to be in the animation). Pretty cool, huh?
Now let’s write some functions to trigger the animations…
import { Animated, Image, View, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {const flipAnimation = useRef( new Animated.Value( 0 ) ).current;let flipRotation = 0;
flipAnimation.addListener( ( { value } ) => flipRotation = value );const flipToFrontStyle = { ... };const flipToBackStyle = { ... };const flipToFront = () => {
Animated.timing( flipAnimation, {
toValue: 180,
duration: 300,
useNativeDriver: true,
} ).start();
};const flipToBack = () => {
Animated.timing( flipAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true,
} ).start();
};...return ( ... );}
…using Animated.timing().start()
. It takes two arguments: our Animated.Value
, and a config object with the value we’re animating to and duration
in milliseconds. You also need useNativeDriver: true
or else you’ll get an angry yellow Animated: `useNativeDriver` was not specified
error!
Finally we’re ready to change our Image
s to Animated.Image
s, add our animation styles, and change View
to Pressable
to make each card respond to a finger press by flipping back and forth:
import { Animated, Pressable, StyleSheet } from "react-native";function Card( { dispatch, card, game } ) {const flipAnimation = useRef( new Animated.Value( 0 ) ).current;let flipRotation = 0;
flipAnimation.addListener( ( { value } ) => flipRotation = value );const flipToFrontStyle = { ... };const flipToBackStyle = { ... };const flipToFront = () => { ... };const flipToBack = () => { ... };...return (
<Pressable
style={ style.cardWrapper }
onPress={ () => !!flipRotation ? flipToBack() : flipToFront() }
>
<Animated.Image
style={ { ...style.cardFront, ...flipToBackStyle } }
source={ require( <card front>) }
/>
<Animated.Image
style={ { ...style.cardBack, ...flipToFrontStyle } }
source={ require( <card back> ) }
/>
</Pressable>
);}
Since we keep track of the card’s flipRotation
with Animated.Value().addListener()
, we can use it to track if a card’s been flipped or not. Replace the ternary in the callback with an if () {} else {}
for multiple lines (to add a dispatch
to a Redux state, for example).
Conclusion
The impression I got at first from React Native was that of a scaled-down React, but I realize now there’s plenty of room for creativity with React Native’s toolkit. And it’s easy to take advantage of the power and flexibility of Animated
!
It’ll be a little while until next time, whenever that is… until then, take care of yourself, take time for yourself, and always keep learning a little something new whenever you can!