Creating an animated game application with React Native and Expo
In this section, you'll build an animated game with React Native and Expo that runs directly on a mobile device. React Native allows you to use the same syntax and patterns you already know from React, as it's using the core React library. Also, Expo makes it possible to prevent having to install and configure Xcode (for iOS) or Android Studio to start creating native applications on your machine. Therefore, you can write applications for both the iOS and Android platforms from any machine.
Expo combines both React APIs and JavaScript APIs to the React Native development process, such as JSX components, Hooks, and native features such as camera access. Briefly, the Expo toolchain consists of multiple tools that help you with React Native, such as the Expo CLI, which allows you to create React Native projects from your terminal, with all the dependencies that you need to run React Native. With the Expo client, you can open these projects from iOS and Android mobile devices that are connected to your local network, and Expo SDK is a package that contains all the libraries that make it possible to run your application on multiple devices and platforms.
Setting up React Native with Expo
Applications that we previously created in this book used Create React App or Next.js to set up a starter application. For React Native, a similar boilerplate is available, which is part of the Expo CLI and can be set up just as easily.
You need to globally install the Expo CLI with the following command, using Yarn
:
yarn global add expo-cli
Alternatively, you can use npm
:
npm install -g expo-cli
Note
Expo is using Yarn as its default package manager, but you can still use it with npm instead as weve done in the previous React chapters.
This will start the installation process, which can take some time, as it will install the Expo CLI with all its dependencies to help you develop mobile applications. After that, you will be able to create a new project using the init
command from the Expo CLI:
expo init chapter-8
Expo will now create the project for you, but before that, it will ask you whether you want to create just a blank template, a blank template with TypeScript configuration, or a sample template with some example screens set up. For this chapter, you'll need to choose the first option. Expo automatically detects whether you have Yarn installed on your machine; if so, it will use Yarn to install the other dependencies that are needed to set up your computer.
Your application will now be created, using the setting you've previously selected. This application can now be started by moving into the directory that was just created by Expo, using the following commands:
cd chapter-8 yarn start
This will start Expo and give you the ability to start your project from both the terminal and your browser. In the terminal, you will now see a QR code, which you can scan with the Expo application from your mobile device, or you can start either the iOS or Android emulator if you have Xcode or Android studio installed. Also, Expo DevTools will be opened in your browser after running the start
command:
On this page, you will see a sidebar on the left and the logs from your React Native application on the right. If you're using an Android device, you can scan the QR code directly from the Expo Go application. On iOS, you need to use your camera to scan the code, which will ask you to open the Expo client. Alternatively, the sidebar in Expo DevTools has buttons to start the iOS or Android emulator, for which you need to have either Xcode installed or Android Studio installed. Otherwise, you can also find a button to send a link to the application by email.
It doesn't matter whether you've opened the application using the emulator for iOS or Android, or from an iOS or Android device; the application at this point should be a white screen displaying Open up App.js to start working on your app!.
Note
If you don't see the application, but a red screen displaying an error, you should make sure that you're running the correct version of React Native and Expo on your local machine and mobile device. These versions should be React Native version 0.64.3 and Expo version 44. Using any other version can lead to errors, as the versions for React Native and Expo should be in sync.
The project structure from this React Native application created with Expo is quite similar to the React projects you've created in the previous chapters:
chapter-8 |- node_modules |- assets |- package.json |- App.js |- app.json |- babel.config.js
In the assets
directory, you can find the images that are used as the application icon on the home screen once you've installed this application on your mobile device and the image that will serve as the splash screen, which is displayed when you start the application. The App.js
file is the actual entry point of your application, where you'll put code that will be rendered when the application mounts. Configurations for your application – for example, the App Store – are placed in app.json
, while babel.config.js
holds specific Babel configurations.
Adding basic routing
For web applications created with React, we've used React Router for navigation, while with Next.js, the routing was already built in using the filesystem. For React Native, we'll need a different routing library that supports both iOS and Android. The most popular library for this is react-navigation
, which we can install from Yarn
:
yarn add @react-navigation/native
This will install the core library, but we need to extend our current Expo installation with dependencies that are needed for react-navigation
by running the following:
expo install react-native-screens react-native-safe-area-context
To add routing to your React Native application, you will need to understand the difference between routing in a browser and a mobile application. History in React Native doesn't behave the same way as in a browser, where users can navigate to different pages by changing the URL in the browser and previously visited URLs are added to the browser history. Instead, you will need to keep track of transitions between pages yourself and store local history in your application.
With React Navigation, you can use multiple different navigators to help you do this, including a stack navigator and a tab navigator. The stack navigator behaves in a way that is very similar to a browser, as it stacks pages after transition on top of each other and lets you navigate using native gestures and animations for iOS and Android. Let's get started:
- First, we need to install the library to use stack navigation and an additional library with navigation elements from
react-navigation
:yarn add @react-navigation/native-stack@react-navigation/elements
- From this library and the core library from
react-navigation
, we need to import the following to create a stack navigator inApp.js
:import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; + import { NavigationContainer } from '@react-navigation/native'; + import { createNativeStackNavigator } from '@react-navigation/native-stack'; + const Stack = createNativeStackNavigator(); export default function App() { // ...
- From the
App
component, we need to return this stack navigator, which also needs a component to return to the home screen. Therefore, we need to create aHome
component in a new directory calledscreens
. This component can be created in a file calledHome.js
with the following content:import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; export default function Home() { return ( <View style={styles.container}> <Text>Home screen</Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });
- In
App.js
, we need to import thisHome
component and set up the stack navigator by returning aNavigationContainer
component from theApp
component. Inside this component, the stack navigator is created by theNavigator
component from theStack
component, and the home screen is described in aStack.Screen
component. Also, the status bar for the mobile device is defined here:import { StatusBar } from 'expo-status-bar'; import React from 'react'; - import { StyleSheet, Text, View } from 'react-native'; + import { StyleSheet } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; + import Home from './screens/Home'; const Stack = createNativeStackNavigator(); export default function App() { export default function App() { return ( - <View style={styles.container}> - <Text>Open up App.js to start working on your app!</Text> + <NavigationContainer> <StatusBar style='auto' /> + <Stack.Navigator> + <Stack.Screen name='Home' component={Home} /> + </Stack.Navigator> + </NavigationContainer> - </View> ); } // ...
Make sure that you still have Expo running from your terminal; otherwise, start it again with the yarn start command. The application on your mobile device or emulator should now look like this:
Note
To reload the application in Expo Go, you can shake the device when you're using an iOS or Android phone. By shaking the device, a menu with an option to reload the application will appear. In this menu, you must also select to enable a fast refresh to refresh the application automatically when you make changes to the code.
We've got our stack navigator with the first page set up, so let's add more pages and create buttons to navigate between them in the next part of this section.
Navigate between screens
Navigating between screens in React Native also works a bit differently than in the browser, as again there are no URLs. Instead, you need to use the navigation object that is available as a prop from components that are rendered by the stack navigator, or by calling the useNavigation
Hook from react-navigation
.
Before learning how to navigate between screens, we need to add another screen to navigate to:
- This screen can be added by creating a new component in a file called
Game.js
in thescreens
directory with the following code:import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; export default function Game() { return ( <View style={styles.container}> <Text>Game screen</Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });
- This component must be imported in
App.js
and added as a new screen to the stack navigator. Also, on the navigator, we need to set the default screen that must be displayed by setting theinitialRouteName
prop:import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { StyleSheet } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import Home from './screens/Home'; + import Game from './screens/Game'; const Stack = createNativeStackNavigator(); export default function App() { return ( <NavigationContainer> <StatusBar style='auto' /> - <Stack.Navigator> + <Stack.Navigator initialRouteName='Home'> <Stack.Screen name='Home' component={Home} /> + <Stack.Screen name='Game' component={Game} /> </Stack.Navigator> </NavigationContainer> ); } // ...
- From the
Home
component inscreens/Home.js
, we can get the navigation object from theuseNavigation
Hook and create a button that will navigate to theGame
screen when pressed. This is done by using thenavigate
method from thenavigation
object and passing it to theonPress
prop of theButton
component from React Native:import React from 'react'; - import { StyleSheet, Text, View } from 'react-native'; + import { StyleSheet, View, Button } from 'react-native'; + import { useNavigation } from '@react-navigation/native'; export default function Home() { + const navigation = useNavigation(); return ( <View style={styles.container}> - <Text>Home screen</Text> + <Button onPress={() => navigation.navigate( 'Game')} title='Start game!' /> </View> ); } // ...
From the application, you can now move between the Home and Game screen by using the button that we just created or by using the button in the header. This header is automatically generated by react-navigation, but you can also customize this, which we'll do in Chapter 9, Building a Full-Stack Social Media Application with React Native and Expo:
At this point, we've added basic routing to our application, but we don't have a game yet. In the screens/Game.js
file, the logic for the Higher/Lower game can be added by using local state management, using the useState
and useEffect
Hooks. These Hooks work the same in React Native as they do in a React web application. Let's add the game logic:
- Import these Hooks from React in the Game component, next to the
Button
andAlert
components from React Native. After importing them, we need to create a local state variable to store the user's choice and create the randomized number and score for the game. Also, import theuseNavigation
Hook fromreact-navigation
:- import React from 'react'; - import { StyleSheet, Text, View } from 'react-native'; + import React, { useEffect, useState } from 'react'; + import { Button, StyleSheet, Text, View, Alert } from 'react-native'; + import { useNavigation } from '@react-navigation/native'; export default function Game() { + const baseNumber = Math.floor(Math.random() * 100); + const score = Math.floor(Math.random() * 100); + const [choice, setChoice] = useState(''); return ( <View style={styles.container}> // ...
The baseNumber
value is the number that starts the game with an initial random value between 1 and 100, created with a Math
method from JavaScript. The score value also has a random number as a value, and this value is used to compare with baseNumber
. The choice
local state variable is used to store the choice of the user if a score is either higher or lower than baseNumber
.
- To be able to make a choice, we need to add two
Button
components that set the value for a choice to be higher or lower, depending on which button you've pressed:// ... return ( <View style={styles.container}> - <Text>Game screen</Text> + <Text>Starting: {baseNumber}</Text> + <Button onPress={() => setChoice('higher')} title='Higher' /> + <Button onPress={() => setChoice('lower')} title='Lower' /> </View> ); } const styles = StyleSheet.create({ // ...
- From an
useEffect
Hook, we can compare the values forbaseNumber
andscore
and, based on the value choice, show an alert. Depending on the choice, the user sees anAlert
component displayed with a message saying whether they've won or not, and the score. Next to displaying the alert, the values forbaseNumber
,score
, andchoice
the navigation object will be used to navigate back to the previous page. This will reset theGame
component as well:// ... + const navigation = useNavigation(); + useEffect(() => { + if (choice) { + const winner = + (choice === 'higher' && score > baseNumber) || + (choice === 'lower' && baseNumber > score); + Alert.alert(`You've ${winner ? 'won' : 'lost'}`, `You scored: ${score}`); + navigation.goBack(); + } + }, [baseNumber, score, choice]); return ( <View style={styles.container}> // ...
You're now able to play the game and choose whether you think the score will be higher or lower than the displayed baseNumber
. But we haven't added any styling yet, which we'll do in the next part of this section.
Styling in React Native
You might have seen in the previous components that we changed or added to the project that we used a variable called StyleSheet
. Using this variable from React Native, we can create an object of styles, which we can attach to React Native components by passing it as a prop called style
. We've already used this to style the components with a style called container
, but let's make some changes to also add styling to the other components:
- In
screens/Home.js
, we need to replace theButton
component with aTouchableHighlight
component, asButton
components in React Native are hard to style. ThisTouchableHighlight
component is an element that can be pressed, and it gives the user feedback by getting highlighted when pressed. Inside this component, aText
component must be added to display the label for the button:import React from 'react'; - import { StyleSheet, View, Button } from 'react-native'; + import { StyleSheet, Text, View, TouchableHighlight } from 'react-native'; import { useNavigation } from '@react-navigation/native'; export default function Home() { const navigation = useNavigation(); return ( <View style={styles.container}> - <Button onPress={() => navigation.navigate( 'Game')} title='Start game!' /> + <TouchableHighlight + onPress={() => navigation.navigate('Game')} + style={styles.button} + > + <Text style={styles.buttonText}> Start game!</Text> + </TouchableHighlight> </View> ); } // ...
- The
TouchableHighlight
andText
components use thebutton
andbuttonText
styles from thestyles
object, which we need to add to thecreate
method ofStyleSheet
at the bottom of the file:// ... const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, + button: { + width: 300, + height: 300, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-around', + borderRadius: 150, + backgroundColor: 'purple', + }, + buttonText: { + color: 'white', + fontSize: 48, + }, });
Creating styles with React Native means you need to use camelCase notation instead of kebab-case as we're used to with CSS – for example, background-color
becomes backgroundColor
.
- We also need to make styling additions to the buttons on the
Game
screen by opening thescreens/Game.js
file. In this file, we again need to replace theButton
components from React Native with aTouchableHighlight
component with an innerText
:import React, { useEffect, useState } from 'react'; import { - Button, StyleSheet, Text, View, Alert, + TouchableHighlight, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; export default function Game() { // ... return ( <View style={styles.container}> - <Text>Starting: {baseNumber}</Text> - <Button onPress={() => setChoice('higher')} title='Higher' /> - <Button onPress={() => setChoice('lower')} title='Lower' /> + <Text style={styles.baseNumber}> Starting: {baseNumber}</Text> + <TouchableHighlight onPress={() => setChoice('higher')} style={styles.button}> + <Text style={styles.buttonText}>Higher </Text> + </TouchableHighlight> + <TouchableHighlight onPress={() => setChoice('lower')} style={styles.button}> + <Text style={styles.buttonText}>Lower</Text> + </TouchableHighlight> </View> ); } // ...
- The
styles
object must have the newbaseNumber
,button
, andbuttonText
styles, which we can add at the bottom of the file:// ... const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, + baseNumber: { + fontSize: 48, + marginBottom: 30, + }, + button: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-around', + borderRadius: 15, + padding: 30, + marginVertical: 15, + }, + buttonText: { + color: 'white', + fontSize: 24, + }, });
- However, both buttons will now have the same white background. We can change this by adding additional styling to them. The
style
prop on React Native components can also take an array of styling objects instead of just a single object:// ... return ( <View style={styles.container}> <Text style={styles.baseNumber}> Starting: {baseNumber}</Text> <TouchableHighlight onPress={() => setChoice('higher')} - style={styles.button} + style={[styles.button, styles.buttonGreen]} > <Text style={styles.buttonText}>Higher</Text> </TouchableHighlight> <TouchableHighlight onPress={() => setChoice('lower')} - style={styles.button} + style={[styles.button, styles.buttonRed]} > <Text style={styles.buttonText}>Lower</Text> </TouchableHighlight> </View> ); // ...
- These
buttonGreen
andbuttonRed
objects must also be added to the styling object:// ... const styles = StyleSheet.create({ // ... + buttonRed: { + backgroundColor: 'red', + }, + buttonGreen: { + backgroundColor: 'green', + }, buttonText: { color: 'white', fontSize: 24, }, });
With these additions, the application is now styled, which makes it more appealing to play. We've used the StyleSheet
object from React Native to apply this styling, making your application look like this:
Mobile games often have flashy animations that make the user want to keep playing and make the game more interactive. The Higher/Lower game that is already functioning uses no animations so far and just has some transitions that have been built in with React Navigation. In the next section, you'll be adding animations and gestures to the application, which will improve the game interface and make the user feel more comfortable while playing the game.
Adding gestures and animations in React Native
There are multiple ways to use animations in React Native, and one of those is to use the Animated API, which can be found at the core of React Native. With the Animated API, you can create animations for the View
, Text
, Image
, and ScrollView
components from React Native by default. Alternatively, you can use the createAnimatedComponent
method to create your own.
Creating a basic animation
One of the simplest animations you can add is fading an element in or out by changing the value for the opacity of that element. In the Higher/Lower game you created previously, the buttons were styled. These colors already show a small transition, since you're using the TouchableHighlight
element to create the button. However, it's possible to add a custom transition to this by using the Animated API. To add an animation, the following code blocks must be changed:
- Start by creating a new directory called
components
, which will hold all our reusablecomponents
. In this directory, create a file calledAnimatedButton.js
, which will contain the following code to construct the new component:import React from 'react'; import { StyleSheet, Text, TouchableHighlight } from 'react-native'; export default function AnimatedButton({ action, onPress }) { return ( <TouchableHighlight onPress={onPress} style={[ styles.button, action === 'higher' ? styles.buttonGreen : styles.buttonRed, ]} > <Text style={styles.buttonText}>{action}</Text> </TouchableHighlight> ); }
- Add the following styling to the bottom of this file:
// ... const styles = StyleSheet.create({ button: { display: 'flex', alignItems: 'center', justifyContent: 'space-around', borderRadius: 15, padding: 30, marginVertical: 15, }, buttonRed: { backgroundColor: 'red', }, buttonGreen: { backgroundColor: 'green', }, buttonText: { color: 'white', fontSize: 24, textTransform: 'capitalize', }, });
- As you can see, this component is comparable to the buttons we have in
screens/Game.js
. Therefore, we can remove theTouchableHighlight
buttons in that file and replace them with theAnimatedButton
component. Make sure to pass the correct values foraction
andonPress
as a prop to this component:import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, View, Alert, - TouchableHighlight, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; + import AnimatedButton from '../components/AnimatedButton'; export default function Game() { // ... return ( <View style={styles.container}> <Text style={styles.baseNumber}> Starting: {baseNumber}</Text> - <TouchableHighlight onPress={() => setChoice('higher')} style={[styles.button, styles.buttonGreen]}> - <Text style={styles.buttonText}>Higher </Text> - </TouchableHighlight> - <TouchableHighlight onPress={() => setChoice('lower')} style={[styles.button, styles.buttonRed]}> - <Text style={styles.buttonText}>Lower</Text> - </TouchableHighlight> + <AnimatedButton action='higher' onPress={() => setChoice('higher')} /> + <AnimatedButton action='lower' onPress={() => setChoice('lower')} /> </View> ); } // ...
- No visible changes are present if you look at the application on your mobile device or the emulator on your computer, since we need to change the clickable element from a
TouchableHighlight
element to aTouchableWithoutFeedback
element first. That way, the default transition with the highlight will be gone, and we can replace this with our own effect. TheTouchableWithoutFeedback
element can be imported from React Native incomponents/AnimatedButton.js
and should be placed around aView
element, which will hold the default styling for the button:import React from 'react'; import { StyleSheet, Text, - TouchableHighlight, + TouchableWithoutFeedback, + View } from 'react-native'; export default function AnimatedButton({ action, onPress }) { return ( - <TouchableHighlight onPress={onPress} style={[ styles.button, action === 'higher' ? styles.buttonGreen : styles.buttonRed ]}> + <TouchableWithoutFeedback onPress={onPress}> + <View style={[ styles.button, action === 'higher' ? styles.buttonGreen : styles.buttonRed ]}> <Text style={styles.buttonText}>{action}</Text> - </TouchableHighlight> + </View> + </TouchableWithoutFeedback> ); } // ...
- To create a transition when we click on the button, we can use the Animated API. We'll use this to change the opacity of the
AnimatedButton
component from the moment it's pressed. A new instance of the Animated API starts by specifying a value that should be changed during the animation that we created with the Animated API. This value should be changeable by the Animated API in your entire component, so you can add this value to the top of the component. This value should be created with auseRef
Hook, since you want this value to be changeable later on. Also, we need to importAnimated
from React Native:- import React from 'react'; + import React, { useRef } from 'react'; import { StyleSheet, Text, TouchableWithoutFeedback, - View, + Animated, } from 'react-native'; export default function AnimatedButton({ action, onPress }) { + const opacity = useRef(new Animated.Value(1)); return ( // ...
- This value can now be changed by the Animated API using any of the three animations types that are built in. These are
decay
,spring
, andtiming
, where you'll be using thetiming
method from the Animated API to change the animated value within a specified time frame. The Animated API can be triggered from theonPress
event onTouchableWithoutFeedback
and calls theonPress
prop after finishing the animation:// ... export default function AnimatedButton({ action, onPress }) { const opacity = useRef(new Animated.Value(1)); return ( <TouchableWithoutFeedback - onPress={onPress} + onPress={() => { + Animated.timing(opacity.current, { + toValue: 0.2, + duration: 800, + useNativeDriver: true, + }).start(() => onPress()); + }} > // ...
The timing
method takes the opacity
that you've specified at the top of your component and an object with the configuration for the Animated API. We need to take the current value of the opacity, as this is a ref
value. One of the fields is toValue
, which will become the value for opacity
when the animation has ended. The other field is for the field's duration, which specifies how long the animation should last.
Note
The other built-in animation types next to timing
are decay
and spring
. Whereas the timing
method changes gradually over time, the decay
type has animations that change fast in the beginning and gradually slow down until the end of the animation. With spring
, you can create animations that move a little outside of their edges at the end of the animation.
- The
View
component can be replaced by anAnimated.View
component. This component uses theopacity
variable created by theuseRef
Hook to set its opacity:// ... - <View + <Animated.View style={[ styles.button, action === 'higher' ? styles.buttonGreen : styles.buttonRed, + { opacity: opacity.current }, ]} > <Text style={styles.buttonText}>{action} </Text> - </View> + </Animated.View> </TouchableWithoutFeedback> ); } // ...
Now, when you press any of the buttons on the Game
screen, they will fade out, since the opacity transitions from 1
to 0.2
in 400 milliseconds.
Something else you can do to make the animation appear smoother is to add an easing
field to the Animated
object. The value for this field comes from the Easing
module, which can be imported from React Native. The Easing
module has three standard functions: linear
, quad
, and cubic
. Here, the linear
function can be used for smoother timing animations:
import React, { useRef } from 'react'; import { StyleSheet, Text, TouchableWithoutFeedback, Animated, + Easing, } from 'react-native'; export default function AnimatedButton({ action, onPress }) { const opacity = useRef(new Animated.Value(1)); return ( <TouchableWithoutFeedback onPress={() => { Animated.timing(opacity.current, { toValue: 0.2, duration: 400, useNativeDriver: true, + easing: Easing.linear(), }).start(() => onPress()); }} > // ...
With this last change, the animation is complete, and the game interface already feels smoother, since the buttons are being highlighted using our own custom animation. In the next part of this section, we will combine some of these animations to make the user experience for this game even more advanced.
Note
You can also combine animations – for example, with the parallel
method – from the Animated API. This method will start the animations that are specified within the same moment and take an array of animations as its value. Next to the parallel
function, three other functions help you with animation composition. These functions are delay
, sequence
, and stagger
, which can also be used in combination with each other. The delay
function starts any animation after a predefined delay, the sequence
function starts animations in the order you've specified and waits until an animation is resolved before starting another one, and the stagger
function can start animations both in order and parallel with specified delays in between.
Handling gestures with Expo
Gestures are an important feature of mobile applications, as they make the difference between a mediocre and a good mobile application. In the Higher/Lower game you've created, several gestures can be added to make the game more appealing.
Previously, you used the TouchableHighlight
element, which gives the user feedback after they press it by changing it. Another element that you could have used for this was the TouchableOpacity
element. These gestures give the user an impression of what happens when they make decisions within your application, leading to improved user experience. These gestures can be customized and added to other elements as well, making it possible to have custom touchable elements as well.
For this, you can use a package called react-native-gesture-handler
, which helps you access native gestures on every platform. All of these gestures will be run in the native thread, which means you can add complex gesture logic without having to deal with the performance limitations of React Native's gesture responder system. Some of the gestures it supports include tap, rotate, drag, and pan, and a long press. In the previous section, we installed this package, as it's a requirement for react-navigation
.
Note
You can also use gestures directly from React Native, without having to use an additional package. However, the gesture responder system that React Native currently uses doesn't run in the native thread. Not only does this limit the possibilities of creating and customizing gestures, but you can also run into cross-platform or performance problems. Therefore, it's advised that you use the react-native-gesture-handler
package, but this isn't necessary for using gestures in React Native.
The gesture we will implement is a long press gesture, which will be added to the start button in our Home
screen, located at screens/Home.js
. Here, we'll use the TapGestureHandler
element from react-native-gesture-handler
, which runs in the native thread, instead of the TouchableWithoutFeedback
element from React Native, which uses the gesture responder system. To implement this, we need to do the following this becomes number 2 please make sure the rest of the numbers are updated accordingly:
- Install using Expo:
expo install react-native-gesture-handler
- Import
TapGestureHandler
andState
fromreact-native-gesture-handler
, next toView
andAlert
from React Native. TheTouchableHighlight
import can be removed, as this will be replaced:import React from 'react'; import { StyleSheet, Text, View, + Alert, - TouchableHighlight, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; + import { TapGestureHandler, State } from 'react-native-gesture-handler'; export default function Home() { // ...
- We can replace the
TouchableHighlight
component withTapGestureHandler
, and we need to put aView
component inside it, to which we apply the styling.TapGestureHandler
doesn't take anonPress
prop but anonHandlerStateChange
prop instead, to which we pass the newon
Tap
function. In this function, we need to check whether the state of the tap event is active. For this, you need to know that the tap event goes through different states:UNDETERMINED
,FAILED
,BEGAN
,CANCELLED
,ACTIVE
, andEND
. The naming of these states is pretty straightforward, and usually, the handler will have the following flow:UNDETERMINED
>BEGAN
>ACTIVE
>END
>UNDETERMINED
:// ... export default function Home() { const navigation = useNavigation(); + function onTap(e) { + if (e.nativeEvent.state === State.ACTIVE) { + Alert.alert('Long press to start the game'); + } + } return ( <View style={styles.container}> - <TouchableHighlight - onPress={() => navigation.navigate('Game')} - style={styles.button} - > + <TapGestureHandler onHandlerStateChange={onTap}> + <View style={styles.button}> <Text style={styles.buttonText}>Start game!</Text> + </View> - </TouchableHighlight> + </TapGestureHandler> </View> ); } // ...
- If you now press the start button on the
Home
screen, you will receive the message that you need to long press the button to start the game. To add this long press gesture, we need to add aLongPressGestureHandler
component inside theTapGestureHandler
component. Also, we need to create a function that can be called by theLongPressGestureHandler
component, which navigates us to theGame
screen:import React from 'react'; import { StyleSheet, Text, View, Alert } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { + LongPressGestureHandler, TapGestureHandler, State, } from 'react-native-gesture-handler'; export default function Home() { const navigation = useNavigation(); + function onLongPress(e) { + if (e.nativeEvent.state === State.ACTIVE) { + navigation.navigate('Game'); + } + } // ...
- Inside the
TapGestureHandler
the newly importedLongPressGestureHandler
component should be placed. This component takes the function to navigate to the game, and a prop to set the minimal duration of the long press. If you dont set this prop, the minimal duration will be 500ms by default::// ... export default function Home() { // ... return ( <View style={styles.container}> <TapGestureHandler onHandlerStateChange={onSingleTap} > + <LongPressGestureHandler+ onHandlerStateChange={onLongPress} + minDurationMs={600} + > <View style={styles.button}> <Text style={styles.buttonText}> Start game!</Text> </View> + </LongPressGestureHandler> </TapGestureHandler> </View> ); } // ...
With this latest change, you can only start the game by long pressing the start button on the Home screen. These gestures can be customized even more, since you can use composition to have multiple tap events that respond to each other. By creating so-called cross-handler interactions, you can create a touchable element that supports a double-tap gesture and a long-press gesture.
The next section will show you how to handle even more advanced animations, such as displaying animated graphics when any of two players win. For this, we'll use the Lottie package, since it supports more functionalities than the built-in Animated API.
Advanced animations with Lottie
The React Native Animated API is great for building simple animations, but building more advanced animations can be harder. Luckily, Lottie offers a solution for creating advanced animations in React Native by making it possible for us to render After Effects animations in real time for iOS, Android, and React Native.
Note
When using Lottie, you don't have to create these After Effects animations yourself; there's a whole library full of resources that you can customize and use in your project. This library is called LottieFiles
and is available at https://lottiefiles.com/.
Since we've already added animations to the buttons of our game, a nice place to add more advanced animations would be the message that is displayed when you win or lose the game. This message can be displayed on a screen instead of an alert, where a trophy can be displayed if the user won. Let's do this now:
- To get started with Lottie, run the following command, which will install Lottie to our project:
yarn add lottie-react-native
- After the installation is completed, we can create a new screen component called
screens/Result.js
with the following content:import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; export default function Result() { return ( <View style={styles.container}> <Text></Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });
- Add this screen to the stack navigator so that it can be used in the navigation for this mobile application by importing it in App.js. Also, the navigation element
HeaderBackButton
should be imported:import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { StyleSheet } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; + import { HeaderBackButton } from '@react-navigation/elements'; import Home from './screens/Home'; import Game from './screens/Game'; + import Result from './screens/Result'; // ...
- We also imported the
HeaderBackButton
component from React Navigation when adding theResult
screen, as we also want to change the go back button in the header for this screen. This way, it will navigate back to theHome
screen instead of theGame
screen so that the user can start a new game after finishing it:// ... export default function App() { return ( <NavigationContainer> <StatusBar style='auto' /> <Stack.Navigator initialRouteName='Home'> <Stack.Screen name='Home' component={Home} /> <Stack.Screen name='Game' component={Game} /> + <Stack.Screen + name='Result' + component={Result} + options={({ navigation }) => ({ + headerLeft: (props) => ( + <HeaderBackButton + {...props} + label='Home' + onPress={() => navigation.navigate('Home')} + /> + ), + })} + /> </Stack.Navigator> </NavigationContainer> ); // ...
- From the
Game
screen inscreens/Game.js
, we can navigate the user to theResult
screen after playing the game and also pass a param to this screen. Using this param, a message can be displayed with the result of the game:// ... export default function Game() { // ... useEffect(() => { if (choice.length) { const winner = (choice === 'higher' && score > baseNumber) || (choice === 'lower' && baseNumber > score); - Alert.alert(`You've ${winner ? 'won' : 'lost'}`, `You scored: ${score}`); - navigation.goBack(); + navigation.navigate('Result', { winner }) } }, [baseNumber, score, choice]); return ( // ...
- From the
Result
screen in thescreens/Result.js
file, we can importLottieView
fromlottie-react-native
and get the param from theroute
object using theuseRoute
Hook from React Navigation. Using this param, we can return a message if the user has won or lost:import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; + import LottieView from 'lottie-react-native'; + import { useRoute } from '@react-navigation/native'; export default function Result() { + const route = useRoute(); + const { winner } = route.params; return ( <View style={styles.container}> + <Text>You've {winner ? 'won' : 'lost'}</Text> // ...
- The imported
Lottie
component can render any Lottie file that you either create yourself or that is downloaded from theLottieFiles
library. In the GitHub repository for this chapter, you will find a Lottie file that can be used in this project calledwinner.json
. This file must be placed in theassets
directory and can be rendered by theLottieView
component when you add it to the source, and thewidth
andheight
values of the animation can be set by passing astyle
object. Also, you should add theautoPlay
prop to start the animation once the component renders:// ... export default function Result() { const route = useRoute(); const { winner } = route.params; return ( <View style={styles.container}> <Text>You've {winner ? 'won' : 'lost'}</Text> + {winner && ( + <LottieView + autoPlay + style={{ + width: 300, + height: 300, + }} + source={require('../assets/winner.json')} + /> + )} </View> ); } // ...
- As a finishing touch, we can add some styling to the message that is displayed on this screen and make it bigger:
// ... return ( <View style={styles.container}> - <Text>You've {winner ? 'won' : 'lost'}</Text> + <Text style={styles.message}> You've {winner ? 'won' : 'lost'}</Text> // ... const styles = StyleSheet.create({ // ... + message: { + fontSize: 48, + }, });
When the Result
screen component receives the winner
param with the true
value, instead of the board, the user will see the trophy animation being rendered. An example of how this will look when you're running the application with the iOS simulator or on an iOS device can be seen here:
Note
If you find the speed of this animation too fast, you can reduce it by combining the Animated API with Lottie. The LottieView
component can take a progress
prop that determines the speed of the animation. When passing a value that is created by the Animated API, you can tweak the speed of the animation as per your preference.
By adding this animation using Lottie, we've created a mobile application with an animated game that you can play for hours.