Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
React Projects

You're reading from   React Projects Build advanced cross-platform projects with React and React Native to become a professional developer

Arrow left icon
Product type Paperback
Published in Apr 2022
Publisher Packt
ISBN-13 9781801070638
Length 384 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Roy Derks Roy Derks
Author Profile Icon Roy Derks
Roy Derks
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Chapter 1: Creating a Single-Page Application in React FREE CHAPTER 2. Chapter 2: Creating a Portfolio in React with Reusable Components and Routing 3. Chapter 3: Building a Dynamic Project Management Board 4. Chapter 4: Building a Server-Side-Rendered Community Feed Using Next.js 5. Chapter 5: Building a Personal Shopping List Application Using Context and Hooks 6. Chapter 6: Building an Application Exploring TDD Using the React Testing Library and Cypress 7. Chapter 7: Building a Full-Stack E-Commerce Application with Next.js and GraphQL 8. Chapter 8: Building an Animated Game Using React Native and Expo 9. Chapter 9: Building a Full-Stack Social Media Application with React Native and Expo 10. Chapter 10: Creating a Virtual Reality Application with React and Three.js 11. Other Books You May Enjoy

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:

Figure 8.1 – Expo DevTools when running Expo

Figure 8.1 – Expo DevTools when running Expo

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:

  1. 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
  2. From this library and the core library from react-navigation, we need to import the following to create a stack navigator in App.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() {
        // ...
  3. 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 a Home component in a new directory called screens. This component can be created in a file called Home.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',
      },
    });
  4. In App.js, we need to import this Home component and set up the stack navigator by returning a NavigationContainer component from the App component. Inside this component, the stack navigator is created by the Navigator component from the Stack component, and the home screen is described in a Stack.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:

Figure 8.2 – The application with a stack navigator

Figure 8.2 – The application with a stack navigator

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:

  1. This screen can be added by creating a new component in a file called Game.js in the screens 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',
      },
    });
  2. 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 the initialRouteName 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>
        );
      }
      // ...
  3. From the Home component in screens/Home.js, we can get the navigation object from the useNavigation Hook and create a button that will navigate to the Game screen when pressed. This is done by using the navigate method from the navigation object and passing it to the onPress prop of the Button 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:

Figure 8.3 – Our application with basic routing

Figure 8.3 – Our application with basic routing

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:

  1. Import these Hooks from React in the Game component, next to the Button and Alert 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 the useNavigation Hook from react-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.

  1. 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({
        // ...
  2. From an useEffect Hook, we can compare the values for baseNumber and score and, based on the value choice, show an alert. Depending on the choice, the user sees an Alert component displayed with a message saying whether they've won or not, and the score. Next to displaying the alert, the values for baseNumber, score, and choice the navigation object will be used to navigate back to the previous page. This will reset the Game 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:

  1. In screens/Home.js, we need to replace the Button component with a TouchableHighlight component, as Button components in React Native are hard to style. This TouchableHighlight component is an element that can be pressed, and it gives the user feedback by getting highlighted when pressed. Inside this component, a Text 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>
        );
      }
      // ...
  2. The TouchableHighlight and Text components use the button and buttonText styles from the styles object, which we need to add to the create method of StyleSheet 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.

  1. We also need to make styling additions to the buttons on the Game screen by opening the screens/Game.js file. In this file, we again need to replace the Button components from React Native with a TouchableHighlight component with an inner Text:
      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>
        );
      }
      // ...
  2. The styles object must have the new baseNumber, button, and buttonText 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,
    +   },
      });
  3. 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>
      );
      // ...
  4. These buttonGreen and buttonRed 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:

Figure 8.4 – The styled React Native application

Figure 8.4 – The styled React Native application

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:

  1. Start by creating a new directory called components, which will hold all our reusable components. In this directory, create a file called AnimatedButton.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>
      );
    }
  2. 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',
      },
    });
  3. As you can see, this component is comparable to the buttons we have in screens/Game.js. Therefore, we can remove the TouchableHighlight buttons in that file and replace them with the AnimatedButton component. Make sure to pass the correct values for action and onPress 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>
        );
      }
      // ...
  4. 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 a TouchableWithoutFeedback element first. That way, the default transition with the highlight will be gone, and we can replace this with our own effect. The TouchableWithoutFeedback element can be imported from React Native in components/AnimatedButton.js and should be placed around a View 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>
        );
      }
      // ...
  5. 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 a useRef Hook, since you want this value to be changeable later on. Also, we need to import Animated 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 (
          //  ...
  6. 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, and timing, where you'll be using the timing method from the Animated API to change the animated value within a specified time frame. The Animated API can be triggered from the onPress event on TouchableWithoutFeedback and calls the onPress 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.

  1. The View component can be replaced by an Animated.View component. This component uses the opacity variable created by the useRef 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:

  1. Install using Expo:
    expo install react-native-gesture-handler
  2. Import TapGestureHandler and State from react-native-gesture-handler, next to View and Alert from React Native. The TouchableHighlight 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() {
        // ...
  3. We can replace the TouchableHighlight component with TapGestureHandler, and we need to put a View component inside it, to which we apply the styling. TapGestureHandler doesn't take an onPress prop but an onHandlerStateChange prop instead, to which we pass the new on 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, and END. 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>
        );
      }
      // ...
  4. 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 a LongPressGestureHandler component inside the TapGestureHandler component. Also, we need to create a function that can be called by the LongPressGestureHandler component, which navigates us to the Game 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');
    +     }
    +   }
        // ...
  5. Inside the TapGestureHandler the newly imported LongPressGestureHandler 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:

  1. To get started with Lottie, run the following command, which will install Lottie to our project:
    yarn add lottie-react-native
  2. 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',
      },
    });
  3. 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';
      // ...
  4. We also imported the HeaderBackButton component from React Navigation when adding the Result screen, as we also want to change the go back button in the header for this screen. This way, it will navigate back to the Home screen instead of the Game 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>
      );
      // ...
  5. From the Game screen in screens/Game.js, we can navigate the user to the Result 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 (
      // ...
  6. From the Result screen in the screens/Result.js file, we can import LottieView from lottie-react-native and get the param from the route object using the useRoute 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>
          // ...
  7. The imported Lottie component can render any Lottie file that you either create yourself or that is downloaded from the LottieFiles library. In the GitHub repository for this chapter, you will find a Lottie file that can be used in this project called winner.json. This file must be placed in the assets directory and can be rendered by the LottieView component when you add it to the source, and the width and height values of the animation can be set by passing a style object. Also, you should add the autoPlay 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>
        );
      }
      // ...
  8. 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:

Figure 8.5 – The Lottie animation after winning a game

Figure 8.5 – The Lottie animation after winning a game

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.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image