ClassSelection
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.
Adding Custom Fonts
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, }, ...
Animations
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.