Considering the success of the React framework, Facebook recently introduced a new mobile development framework called React Native. With React Native's game-changing approach to hybrid mobile development, you can build native mobile applications that are much more powerful, interactive, and faster by using JavaScript.
In this lesson, we will set the focus on feature development rather than in building a user interface by delegating the styling of our applications to UI libraries such as native-base as well as spend more time in building custom UI components and screens.
The app we will build is a car booking app in which the user can select the location in which he/she wants to be picked up and the type of car she wants to book for the ride. Since we want to focus on the user interface, our app will only have two screens and a little state management is needed. Instead, we will dive deeper into aspects such as animations, component's layout, using custom fonts, or displaying external images.
The app will be available for iOS and Android devices, and since all the user interface will be custom made, 100% of the code will be reused between both platforms. We will only use two external libraries:
React-native-geocoder
: This will translate coordinates into human-readable locationsReact-native-maps
: This will easily display the maps and the markers showing the locations for the bookable cars
Due to its nature, most of the car booking apps put their complexity in the backend code to connect drivers with riders effectively. We will skip this complexity and mock all that functionality in the app itself to focus on building beautiful and usable interfaces.
When building mobile apps, we need to make sure we reduce the interface complexity to the minimum, as it's often punishing to present the user intrusive manuals or tooltips once the app is open. It is a good practice to make our app self-explanatory, so the user can understand the usage just by going through the app screens. That's why using standard components such as drawer menus or standard lists is always a good idea, but is not always possible (as it happens in our current app) due to the kind of data we want to present to the user.
In our case, we put all the functionality in the main screen plus in a modal box. Let's take a look at what the app will look like on iOS devices:

The background on our main screen is the maps component itself where we will show all the available cars as markers in the map. On the maps, we will display three components:
The pickup location box displaying the selected pickup location
The location pin, which can be dragged around the maps to select a new location
The selector for the kind of car the user wants to book. We will display three options:
ECONOMY
,SPECIAL
, andSUPERIOR
Since most of the components are custom built, this screen will look very similar in any Android device:

The main difference between the iOS and the Android version will be the map component. While iOS will use Apple maps by default, Android uses Google Maps. We will leave this setup as each platform has its own map component optimized, but it's good to know that we can switch the iOS version to use Google Maps just by configuring our component.
Once the user has selected a pickup location, we will display a modal box to confirm the booking and contact the nearest driver for pickup:

As it happened with the main screen, this screen uses custom components: we even decided to create our own animated activity indicator. Because of this, the Android version will look very similar:

Since our app won't be connected to any external API, it should be seen as a mere display of the visual capabilities of React Native, although it could be easily extended by adding a state management library and a matching API.
Let's take a look at the topics of this lesson:
Using maps in our app
Style sheets in React Native
Flexbox in React Native
Using external images in a React Native app
Adding custom fonts
Animations in React Native
Using modals
Working with shadows and opacity
Let's initialize a React Native project using React Native's CLI. The project will be named carBooking
and will be available for iOS and Android devices:
react-native init --version="0.49.3" carBooking
In this app, there is only one screen so that the folder structure for the code should be very straightforward. Since we will be using external images and fonts, we will organize these resources in two separate folders: img
and fonts
, both under the root folder.
The images and fonts used to build this app can be downloaded freely from some image and font sock websites. The name of the font we will use is Blair ITC.
We also stored the following images inside the img
folder:
car.png
: A simple drawing of a car to represent the bookable cars on the map.class.png
: The silhouette of a car to show inside the class selection buttonclassBar.png
: The bar in which the class selection button will be slid to change the class.loading.png
: Our custom spinner. It will be stored as a static image and animated through the code.
Finally, let's take a look at our package.json
file:
{ "name": "carBooking", "version": "0.0.1", "private": true, "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", "test": "jest" }, "dependencies": { "react": "16.0.0-beta.5", "react-native": "0.49.3", "react-native-geocoder": " 0.4.8", "react-native-maps": " 0.15.2" }, "devDependencies": { "babel-jest": "20.0.3", "babel-preset-react-native": "1.9.2", "jest": "20.0.4", "react-test-renderer": "16.0.0-alpha.6" }, "jest": { "preset": "react-native" }, "rnpm": { "assets": ["./fonts"] } }
We only use two npm modules:
react-native-geocoder
: This translates coordinates into human-readable locationsreact-native-maps
: This easily displays the maps and the markers showing the locations for the bookable cars
In order to allow the app to use custom fonts, we need to make sure they are accessible from the native side. For that, we need to add a new key to package.json
named rnpm
. This key will store an array of assets
in which we will define our fonts
folder. During build time, React Native will copy the fonts to a location from where they will be available natively and therefore usable within our code. This is only required by fonts and some special resources, but not by images.
Let's take the chance of having a simple folder structure in this app to show what other files and folders are created by React Native's CLI when initializing a project through react-native init <projectName>
.
React Native's CLI includes Jest as a developer dependency and, to get testing started, it includes a folder named __tests__
, in which all tests can be stored. By default, React Native's CLI adds one test file: index.js
, representing the initial set of tests. Developers can add later tests for any components in the app. React Native also adds a test
script in our package.json
, so we can run npm run test
from the very first moment.
Jest is ready to be used with every project initialized through the CLI and it's definitely the easiest option when it comes to testing React components, although it is also possible to use other libraries such as Jasmine or Mocha.
These two folders hold the built app for both platforms natively. This means that we can find our .xcodeproj
and java
files in here. Every time we need to make changes to the native code of our app, we will need to modify some files in these two directories.
The most common reasons to find and modify files in these folders are:
Modify permissions (push notifications, access to location services, access to compass, and many more) by changing
Info.plist
(iOS) orAndroidManifest.xml
(Android)Change the build settings for any platform
Add API keys for native libraries
Add or modify native libraries to be used from our React Native code
This folder should be familiar to most of the JavaScript developers who worked with npm
as it is where npm
stores all the modules marked as a dependency in our project. It is not common to have the necessity to modify anything inside this folder, as everything should be handled through npm's CLI and our package.json
file.
React Native's CLI creates a number of files in the root directory of our project; let's take a look at the most important ones:
.babelrc
: Babel is the default library in React Native to compile our JavaScript files containing JSX and ES6 (for example, syntax into plain JavaScript capable to be understood by most of the JavaScript engines). Here, we can modify the configuration for this compiler so we can, for example, use the@
syntax for decorators as it was done in the first versions of React..buckconfig
: Buck is the build system used by Facebook. This file is used to configure the building process when using Buck..watchmanconfig
: Watchman is a service that watches the files in our project to trigger a rebuild anytime something changes in them. In this file, we can add some configuration options such as directories, which should be ignored.app.json
: This file is used by thereact-native eject
command to configure the native apps. It stores the name that identifies the app in each platform and also the name that will be displayed on the home screen of the device when the app is installed.yarn.lock
: Thepackage.json
file describes the intended versions desired by the original author, whileyarn.lock
describes the last-known-good configuration for a given application.
Some apps depend on libraries with native capabilities which, before React Native CLI, required developers to copy native library files into the native projects. This was a cumbersome and repetitive project until react-native link
came to the rescue. In this lesson we will use it to copy library files from react-native-maps
and to link custom fonts from our /fonts
folder to the compiled app.
By running react-native link
in our project's root folder we will trigger the linking steps which will result in those native capabilities and resources to be accessible from our React Native code.
Having the dependencies in the package.json
file and all the initial files in place, we can run the following command (in the root folder of our project) to finish the installation:
npm install
Then, all the dependencies should be installed in our project. Once npm finishes installing all dependencies, we can start our app in the iOS simulator:
react-native run-ios
Or in the Android emulator using the following command:
react-native run-android
When React Native detects the app is running in a simulator, it enables a developer toolset available through a hidden menu, which can be accessed through the shortcuts command + D on iOS or command + M on Android (on Windows Ctrl should be used instead of command). This is how the developer menu looks like in iOS:

And this is how it looks like in the Android simulator:

In the process of building an app in React Native, the developer will have debugging needs. React Native fulfills these needs with the ability to remotely debug our apps in Chrome developer's tools or external applications such as React Native Debugger. Errors, logs, and even React components can be debugged easily as in a normal web environment.
On top of that, React Native provides a way to automatically reload our app each time a change is done saving the developers the task of manually reloading the app (which can be achieved by pressing command + R or Ctrl + R). There are two options when we set our app for automatic reloading:
Live reload detects any changes we make in the app's code and resets the app to its initial state after reloading.
Hot reload also detects changes and reloads the app, but keeps the current state of the app. This is really useful when we are implementing user flows to save the developer to repeat each step in the flow (for example, logging in or registering test users)
Finally, we can start the performance monitor to detect possible performance issues when performing complex operations such as animations or mathematical calculations.
Let's start our app's code by creating the entry point for our app: index.js
. We import src/main.js
in this file to use a common root component for our code base. Moreover, we will register the app with the name carBooking
:
/*** index.js ***/ import { AppRegistry } from 'react-native'; import App from './src/main'; AppRegistry.registerComponent('carBooking', () => App);
Let's start building our src/main.js
by adding a map component:
/*** src/main.js ** */ import React from 'react'; import { View, StyleSheet } from 'react-native'; import MapView from 'react-native-maps'; export default class Main extends React.Component { constructor(props) { super(props); this.initialRegion = { latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.00922, longitudeDelta: 0.00421, }; } render() { return ( <View style={{ flex: 1 }}> <MapView style={styles.fullScreenMap} initialRegion={this.initialRegion} /> </View> ); } } const styles = StyleSheet.create({ fullScreenMap: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, }, });
Instead of using libraries for styling, we will create our own styles using StyleSheet
, a React Native API, which serves as an abstraction similar to CSS style sheets. With StyleSheet
, we can create a style sheet from an object (through the create
method), which can be used in our components by referring to each style by its ID.
This way, we can reuse the style code and make the code more readable as we will be using meaningful names to refer to each style (for example, <Text style={styles.title}>Title 1</Text>
).
At this point, we will only create a style referred by the key fullScreenMap
and make it as an absolute position by covering the fullscreen size by adding top
, bottom
, left
, and right
coordinates to zero. On top of this, we need to add some styling to our container view to ensure it fills the whole screen: {flex: 1}
. Setting flex
to 1
, we want our view to fill all the space its parent occupies. Since this is the main view, {flex: 1}
will take over the whole screen.
For our map component, we will use react-native-maps
, an open module created by Airbnb using native maps capabilities for Google and Apple maps. react-native-maps
is a very flexible module, really well maintained, and fully featured so that it has become the de facto maps module for React Native. As we will see later in this lesson, react-native-maps
requires the developer to run react-native link
in order for it to work.
Apart from the style, the <MapView/>
component will take initialRegion
as a property to centre the map in a specific set of coordinates, which should be the current location of the user. For consistency reasons, we will locate the center of the map in San Francisco where we will also place some bookable cars:
/** * src/main.js ** */ import React from 'react'; import { View, Animated, Image, StyleSheet } from 'react-native'; import MapView from 'react-native-maps'; export default class Main extends React.Component { constructor(props) { super(props); this.state = { carLocations: [ { rotation: 78, latitude: 37.78725, longitude: -122.4318, }, { rotation: -10, latitude: 37.79015, longitude: -122.4318, }, { rotation: 262, latitude: 37.78525, longitude: -122.4348, }, ], }; this.initialRegion = { latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.00922, longitudeDelta: 0.00421, }; } render() { return ( <View style={{ flex: 1 }}> <MapView style={styles.fullScreenMap} initialRegion={this.initialRegion} > {this.state.carLocations.map((carLocation, i) => ( <MapView.Marker key={i} coordinate={carLocation}> <Animated.Image style={{ transform: [{ rotate: `${carLocation.rotation}deg` }], }} source={require('../img/car.png')} /> </MapView.Marker> ))} </MapView> </View> ); } } ...
We have added an array of carLocations
to be shown on the map as markers. Inside our render
function, we will iterate over this array and place the corresponding <MapView.Marker/>
in the provided coordinates. Inside each marker, we will add the image of the car rotating it by a specific number of degrees, so they match the streets directions. Rotating images must be done with the Animated
API, which will be better explained later in this lesson.
Let's add a new property in our state to store a human-readable position for the location in which the map is centered:
/** * src/main.js ** */ import GeoCoder from 'react-native-geocoder'; export default class Main extends React.Component { constructor(props) { super(props); this.state = { position: null, ... }; ... } _onRegionChange(region) { this.setState({ position: null }); const self = this; if (this.timeoutId) clearTimeout(this.timeoutId); this.timeoutId = setTimeout(async () => { try { const res = await GeoCoder.geocodePosition({ lat: region.latitude, lng: region.longitude, }); self.setState({ position: res[0] }); } catch (err) { console.log(err); } }, 2000); } componentDidMount() { this._onRegionChange.call(this, this.initialRegion); } render() { <View style={{ flex: 1 }}> <MapView style={styles.fullScreenMap} initialRegion={this.initialRegion} onRegionChange={this._onRegionChange.bind(this)} > ... </MapView> </View>; } } ...
To fill this state variable, we also created a function _onRegionChange
, which uses the react-native-geocoder
module. This module uses Google Maps reverse geocoding services to translate some coordinates into a human-readable location. Because it's a Google Service, we might need to add an API key in order to authenticate our app with the service. All the instructions to get this module fully installed can be found at its repository URL https://github.com/airbnb/react-native maps/blob/master/docs/installation.md.
We want this state variable to be available from the first mount of the main component, so we will call _onRegionChange
in componentDidMount
so that the name of the initial location is also stored in the state. Moreover, we will add the onRegionChange
property on our <MapView/>
to ensure the name of the location is recalculated every time the map is moved to show a different region, so we always have the name of the location in the center of the map in our position
state variable.
As a final step on this screen, we will add all the subviews and another function to confirm the booking request:
/** * src/main.js ** */ ... import LocationPin from './components/LocationPin'; import LocationSearch from './components/LocationSearch'; import ClassSelection from './components/ClassSelection'; import ConfirmationModal from './components/ConfirmationModal'; export default class Main extends React.Component { ... _onBookingRequest() { this.setState({ confirmationModalVisible: true, }); } render() { return ( <View style={{ flex: 1 }}> ... <LocationSearch value={ this.state.position && (this.state.position.feature || this.state.position.formattedAddress) } /> <LocationPin onPress={this._onBookingRequest.bind(this)} /> <ClassSelection /> <ConfirmationModal visible={this.state.confirmationModalVisible} onClose={() => { this.setState({ confirmationModalVisible: false }); }} /> </View> ); } } ...
We added four subviews:
LocationSearch
: The component in which we will show the user the location that is centered on the map so she can know the name of the location she is exactly requesting the pickup.LocationPin
: A pinpointing to the center of the map, so the user can see on the map where she will request the pickup. It will also display a button to confirm the pickup.ClassSelection
: A bar where the user can select the type of car for the pickup (economy, special, or superior).ConfirmationModal
: The modal displaying the confirmation of the request.
The _onBookingRequest
method will be responsible for bringing the confirmation modal up when a booking is requested.
React Native deals with images in a similar way as websites do: images should be placed in a folder inside the projects folder structure, and then they can be referenced from the <Image />
(or <Animated.Image />
) by the source
property. Let's see an example from our app:
car.png
: This is placed inside theimg/
folder in the root of our projectThen the image will be displayed by creating an
<Image/>
component using thesource
property:<Image source={require('../img/car.png')} />
Notice how the
source
property doesn't accept a string, but arequire('../img/car.png')
. This is a special case in React Native and may change in future versions.
This should be a simple textbox displaying the human-readable name of the location in which the map is centered. Let's take a look at the code:
/*** src/components/LocationSearch.js ** */ import React from 'react'; import { View, Text, TextInput, ActivityIndicator, StyleSheet, } from 'react-native'; export default class LocationSearch extends React.Component { render() { return ( <View style={styles.container}> <Text style={styles.title}>PICKUP LOCATION</Text> {this.props.value && ( <TextInput style={styles.location} value={this.props.value} /> )} {!this.props.value && <ActivityIndicator style={styles.spinner} />} </View> ); } } const styles = StyleSheet.create({ container: { backgroundColor: 'white', margin: 20, marginTop: 40, height: 60, padding: 10, borderColor: '#ccc', borderWidth: 1, }, title: { alignSelf: 'center', fontSize: 12, color: 'green', fontWeight: 'bold', }, location: { height: 40, textAlign: 'center', fontSize: 13, }, spinner: { margin: 10, }, });
It receives only one property: value
(the name of the location to be displayed). If it's not set, it will display a spinner to show activity.
Because there are many different styles to be applied in this component, it's beneficial to use the StyleSheet
API to organize the styles in a key/value object and refer it from our render
method. This separation between logic and style helps in readability of the code and also enables code reuse as the styles can be cascaded down to child components.
React Native uses Flexbox for setting up the layout of the elements in an app. This is mostly straightforward, but sometimes it can be confusing when it comes to aligning elements as there are four properties that can be used for this purpose:
justifyContent:
It defines the alignment of the child elements through the main axisalignItems
: It defines the alignment of the child elements through the cross-axisalignContent
: It aligns a flex container's lines within when there is extra space in the cross-axisalignSelf
: It allows the default alignment (or the one specified byalignItems
) to be overridden for individual flex items
The first three properties should be assigned to the container element, while the fourth one will be applied to a child element in case we want to override the default alignment.
In our case, we only want one element (the title) to be center aligned so we can use alignSelf: 'center'
. Later in this lesson, we will see other uses for the different align
properties.
In this section, we will focus on building the pinpointing to the center of the map to visually confirm the pickup location. This pin also contains a button, which can be used to trigger a pickup request:
/** * src/components/LocationPin.js ** */ import React from 'react'; import { View, Text, Dimensions, TouchableOpacity, StyleSheet, } from 'react-native'; const { height, width } = Dimensions.get('window'); export default class LocationPin extends React.Component { render() { return ( <View style={styles.container}> <View style={styles.banner}> <Text style={styles.bannerText}>SET PICKUP LOCATION</Text> <TouchableOpacity style={styles.bannerButton} onPress={this.props.onPress} > <Text style={styles.bannerButtonText}>{'>'}</Text> </TouchableOpacity> </View> <View style={styles.bannerPole} /> </View> ); } } const styles = StyleSheet.create({ container: { position: 'absolute', top: height / 2 - 60, left: width / 2 - 120, }, banner: { flexDirection: 'row', alignSelf: 'center', justifyContent: 'center', borderRadius: 20, backgroundColor: '#333', padding: 10, paddingBottom: 10, shadowColor: '#000000', shadowOffset: { width: 0, height: 3, }, shadowRadius: 5, shadowOpacity: 1.0, }, bannerText: { alignSelf: 'center', color: 'white', marginRight: 10, marginLeft: 10, fontSize: 18, }, bannerButton: { borderWidth: 1, borderColor: '#ccc', width: 26, height: 26, borderRadius: 13, }, bannerButtonText: { color: 'white', textAlign: 'center', backgroundColor: 'transparent', fontSize: 18, }, bannerPole: { backgroundColor: '#333', width: 3, height: 30, alignSelf: 'center', }, });
This component is again very light in terms of functionality, but has a lot of custom style. Let's dive into some of the style details.
By default, React Native and Flexbox stack elements vertically:

For the banner in our pin, we want to stack every element horizontally after each other as follows:

This can be achieved by adding the following styles to the containing element flexDirection: 'row'
. The other valid options for flexDirection
are:
row-reverse
column
(default)column-reverse
One of the first lines of code in this component extracts the height and the width from the device into two variables:
const {height, width} = Dimensions.get('window');
Obtaining the height and width of the device enables us developers to absolute position some elements being confident they will show properly aligned. For example, we want the banner of our pin to be aligned in the center of the screen, so it points to the center of the map. We can do this by adding {top: (height/2), left: (width/2)}
to the banner
style in our style sheet. Of book, that would align the upper-left corner, so we need to subtract half the size of the banner to each property to ensure it gets centered in the middle of the element. This trick can be used whenever we need to align an element that is not relative to any other in the components tree although it is recommended to use relative positioning when possible.
Let's set focus on our banner's style, specifically on the shadows
properties:
banner: { ... shadowColor: '#000000', shadowOffset: { width: 0, height: 3 }, shadowRadius: 5, shadowOpacity: 1.0 }
In order to add a shadow to a component, we need to add four properties:
shadowColor
: This adds the hexadecimal or RGBA value of the color we want for our componentshadowOffset
: This shows how far we want our shadow to be castedshadowRadius
: This shows the value of the radius in the corner of our shadowshadowOpacity
: This shows how dark we want our shadow to be
That's it for our LocationPin
component.
In this component, we will explore the Animated
API in React Native to get started with animations. Moreover, we will use custom fonts to improve the user experience and increase the feeling of customization in our app:
/*** src/components/ClassSelection.js ** */ import React from 'react'; import { View, Image, Dimensions, Text, TouchableOpacity, Animated, StyleSheet, } from 'react-native'; const { height, width } = Dimensions.get('window'); export default class ClassSelection extends React.Component { constructor(props) { super(props); this.state = { classButtonPosition: new Animated.Value(15 + width * 0.1), }; } _onClassChange(className) { if (className === 'superior') { Animated.timing(this.state.classButtonPosition, { toValue: width * 0.77, duration: 500, }).start(); } if (className === 'special') { Animated.timing(this.state.classButtonPosition, { toValue: width * 0.5 - 20, duration: 500, }).start(); } if (className === 'economy') { Animated.timing(this.state.classButtonPosition, { toValue: 15 + width * 0.1, duration: 500, }).start(); } } render() { return ( <View style={styles.container}> <Image style={styles.classBar} source={require('../../img/classBar.png')} /> <Animated.View style={[styles.classButton, { left: this.state.classButtonPosition }]} > <Image style={styles.classButtonImage} source={require('../../img/class.png')} /> </Animated.View> <TouchableOpacity style={[ styles.classButtonContainer, { width: width / 3 - 10, left: width * 0.11, }, ]} onPress={this._onClassChange.bind(this, 'economy')} > <Text style={styles.classLabel}>economy</Text> </TouchableOpacity> <TouchableOpacity style={[ styles.classButtonContainer, { width: width / 3, left: width / 3 }, ]} onPress={this._onClassChange.bind(this, 'special')} > <Text style={[styles.classLabel, { textAlign: 'center' }]}> Special </Text> </TouchableOpacity> <TouchableOpacity style={[ styles.classButtonContainer, { width: width / 3, right: width * 0.11 }, ]} onPress={this._onClassChange.bind(this, 'superior')} > <Text style={[styles.classLabel, { textAlign: 'right' }]}> Superior </Text> </TouchableOpacity> </View> ); } } const styles = StyleSheet.create({ container: { height: 80, backgroundColor: 'white', position: 'absolute', bottom: 0, left: 0, right: 0, paddingBottom: 10, }, classBar: { width: width * 0.7, left: width * 0.15, resizeMode: 'contain', height: 30, top: 35, }, classButton: { top: 30, justifyContent: 'center', borderRadius: 20, borderColor: '#ccc', borderWidth: 1, position: 'absolute', backgroundColor: 'white', height: 40, width: 40, }, classButtonImage: { alignSelf: 'center', resizeMode: 'contain', width: 30, }, classButtonContainer: { backgroundColor: 'transparent', position: 'absolute', height: 70, top: 10, }, classLabel: { paddingTop: 5, fontSize: 12, }, });
This simple component is made out of five sub components:
classBar
: This is an image showing the bar and the stop points for each classclassButton
: This is the round button, which will be moved to the selected class once the user presses a specific classclassButtonContainer
: This is the touchable component detecting what class the user wants to selectclassLabel
: These are titles for each class to be displayed on top of the bar
Let's start by taking a look at the styles as we can find a new property for image components: resizeMode
, which determines how to resize the image when the frame doesn't match the raw image dimensions. From the five possible values (cover
, contain
, stretch
, repeat
, and center
), we chose contain as we want to scale the image uniformly (maintain the image's aspect ratio) so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. We are using these properties both in classBar
and classButtonImage
being the two images we will need to resize in this view.
React Native includes a long list of cross-platform fonts available by default. The list of fonts can be checked on https://github.com/react-native-training/react-native-fonts.
Nevertheless, adding custom fonts is a common need when developing apps, especially when designers are involved, so we will use our car booking app as a playground to test this functionality.
Adding custom fonts to our app is a three steps task:
Add the font file (
.ttf
) into a folder inside our project. We usedfonts/
for this app.Add the following lines to our
package.json
:"rnpm": { "assets": ["./fonts"] }
Run the following command in a terminal:
react-native link
That's it, React Native's CLI will handle the insertion of the fonts
folder and its files inside the iOS and Android project at once. Our fonts will be available by their font name (which may not be the same as the filename). In our case, we have fontFamily: 'Blair ITC'
in our style sheet.
We can now modify our classLabel
style in the ClassSelection
component to include the new font:
... classLabel: { fontFamily: 'Blair ITC', paddingTop: 5, fontSize: 12, }, ...
React Native's Animated
API is designed to make it very easy to concisely express a wide variety of interesting animation and interaction patterns in a very performant way. Animated focuses on declarative relationships between inputs and outputs, with configurable transforms in between, and simple start
/stop
methods to control time-based animation execution.
What we want to do in our app is to move the classButton
to a specific location whenever the user presses the class she wants to book. Let's take a closer look at how we are using this API in our app:
/** * src/components/ClassSelection ***/ ... export default class ClassSelection extends React.Component { constructor(props) { super(props); this.state = { classButtonPosition: new Animated.Value(15 + width * 0.1), }; } _onClassChange(className) { if (className === 'superior') { Animated.timing(this.state.classButtonPosition, { toValue: width * 0.77, duration: 500, }).start(); } ... } render() { return ( ... <Animated.View style={{ left: this.state.classButtonPosition }}> <Image style={styles.classButtonImage} source={require('../../img/class.png')} /> </Animated.View> ... <TouchableOpacity onPress={this._onClassChange.bind(this, 'superior')} > <Text>Superior</Text> </TouchableOpacity> ... ); } } ...
For this movement to happen correctly, we need to wrap the classButtonImage
in Animated.View
and provide an initial Animated.Value
to it as a left coordinate. We will use this.state.classButtonPosition
for this matter so that we can change it when the user selects a specific class.
We are ready to start our animation. It will be triggered by the _onClassChange
method, as it is the one invoked when the user presses classButtonContainer
(<TouchableOpacity/>
). This method is calling the Animated.timing
function passing two parameters:
The animated value to drive (
this.state.classButtonPosition
)An object containing the end value and the duration of the animation
Invoking Animated.timing
will result in an object containing the start()
method, which we call right away to start the animation. React Native will then know that the left
coordinate of the Animated.View
needs to be slowly changed according to the provided parameters.
As this may feel a bit overcomplicated for a simple move animation, it allows a wide range of customization as chaining animations or modifying the easing functions. We will see a rotation animation later in this lesson.
Our last component is a modal view, which will be opened once the user has pressed on the SET PICKUP LOCATION
button on the location pin. We will display the modal and a custom activity indicator, which will use a complex animation setup to continuously rotate in its position:
/** * src/components/ConfirmationModal.js ***/ import React from 'react'; import { Modal, View, Text, Animated, Easing, TouchableOpacity, StyleSheet, } from 'react-native'; export default class ConfirmationModal extends React.Component { componentWillMount() { this._animatedValue = new Animated.Value(0); } cycleAnimation() { Animated.sequence([ Animated.timing(this._animatedValue, { toValue: 100, duration: 1000, easing: Easing.linear, }), Animated.timing(this._animatedValue, { toValue: 0, duration: 0, }), ]).start(() => { this.cycleAnimation(); }); } componentDidMount() { this.cycleAnimation(); } render() { const interpolatedRotateAnimation = this._animatedValue.interpolate({ inputRange: [0, 100], outputRange: ['0deg', '360deg'], }); return ( <Modal animationType={'fade'} visible={this.props.visible} transparent={true} > <View style={styles.overlay}> <View style={styles.container}> <Text style={styles.title}>Contacting nearest car...</Text> <Animated.Image style={[ styles.spinner, { transform: [{ rotate: interpolatedRotateAnimation }] }, ]} source={require('../../img/loading.png')} /> <TouchableOpacity style={styles.closeButton} onPress={this.props.onClose} > <Text style={styles.closeButtonText}>X</Text> </TouchableOpacity> </View> </View> </Modal> ); } } const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: '#0006', justifyContent: 'center', }, container: { backgroundColor: 'white', alignSelf: 'center', padding: 20, borderColor: '#ccc', borderWidth: 1, }, title: { textAlign: 'right', fontFamily: 'Blair ITC', paddingTop: 5, fontSize: 12, }, spinner: { resizeMode: 'contain', height: 50, width: 50, margin: 50, alignSelf: 'center', }, closeButton: { backgroundColor: '#333', width: 40, height: 40, borderRadius: 20, justifyContent: 'center', alignSelf: 'center', }, closeButtonText: { color: 'white', alignSelf: 'center', fontSize: 20, }, });
For this component, we are using the <Modal />
component available in React Native to take advantage of its fade animation and visibility capabilities. The property this.props.visible
will drive the visibility of this component as it is the parent who is aware of the pickup request from the user.
Let's focus again on animations as we want to do a more complex setup for the spinner showing activity. We want to display an endless rotating animation, so we need to systematically call our start()
animation method. In order to achieve this, we created a cycleAnimation()
method, which is called on the component mount (to get the animation started) and from the Animated.timing
returned object as it is passed as a callback to be invoked every time the animation ends.
We are also using Animated.sequence
to concatenate two animations:
Moving from 0 degrees to 360 (in one second using a linear easing)
Moving from 360 degrees to 0 (in 0 seconds)
This is required to repeat the first animation over at the end of each cycle.
Finally, we defined a variable named interpolatedRotateAnimation
to store the interpolation from 0 degrees to 360, so it can be passed to the transform
/rotate
style defining what are going to be the available rotation values when animating our Animated.Image
.
As an experiment, we can try and change loading.png with an alternative image and see how it gets animated. This can be easily achieved by replacing the source
property in our <Animated.Image />
component:
... <Animated.Image style={[ styles.spinner, { transform: [{ rotate: interpolatedRotateAnimation }] }, ]} source={require('../../img/spinner.png')} /> ...
Using UI libraries such as native-base
or react-native-elements
saves a lot of time and maintenance hassle when we need to build apps, but the results end up having a standard flavor, which is not always desirable in terms of user experience. That's why learning how to manipulate the style of our apps is always a good idea, especially on teams where the design is provided by UX specialists or app designers.
In this lesson, we took a deep look into the folders and files created by React Native's CLI when initializing a project. Moreover, we familiarized ourselves with the developer menu and its debugging functionalities. When building our app we set the focus on the layouts and component styling, but also on how to add and manipulate animations to make our interface more appealing to the user. We took a look at Flexbox layout system and how to stack and center elements in our components. API's such as dimensions were used to retrieve the device width and height to perform positioning tricks on some components. You learned how to add fonts and images into our app and how to show them to improve the user experience.
Now that we know how to build more custom interfaces, let's build in the next lesson an image sharing app in which design plays a key role.
Why does the
react-native-geocoder
module uses Google Maps reverse geocoding services?To store a human-readable position for the location in which the map is centred
To translate some coordinates into a human-readable location
To add an API key in order to authenticate our app with the service
To ensure the name of the location is recalculated every time the map is moved to show a different region
Which of the following properties is used for aligning elements?
justifyContent
alignLeft
alignRight
alignJustify
By default, React Native and Flexbox stack elements ________.
Diagonally
Reverse
Vertically
Horizontally
Which of the following lines of code extracts the height and the width from a device into two variables?
const {height, width} = Dimensions.get('height, width');
constant {height, width} = Dimensions.get('window');
const {height, width} = get('window');
const {height, width} = Dimensions.get('window');
Which are the four properties in order to add a shadow to a component?