React Native — Animated Quiz App Template with progress Bar | Navigation + Animation | Beginner Friendly

Katrinashui
9 min readJun 13, 2023

--

This article provides an easy-to-follow guide to creating a quiz application using Navigation, and Animation. The template includes three main parts: Routes, Pages, and Models. Wish you can gain the idea about how to create a React Native App with navigation and animation.

If you are looking for the same animated quiz app but using Flutter instead of React Native, please refer to this.

Overview of the Quiz App

The quiz app includes three main parts:

  • Routes : Identifying the navigation between pages (Wrap in NavigationContrainer in App.js)
  • Screen Pages:
    - Welcome Page (WelcomePage function in App.js)
    - Question Page (QuizPage function in App.js)
    - Result Page (ResultPage function in App.js)
  • Models: storing the question banks (in QuizData.js)

1. Navigate between pages — Navigation

1.1 Installation of dependences

To implement the navigation, first, you need to install the corresponding dependence:-

//(Use the Terminal in Visual Studio to install it)

npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context

npm install @react-navigation/native-stack

1.2 Import NavigationContainer, createNativeStackNavigation

Then, you need to import the NavigationContainer, createNativeStackNavigation from ‘@react-navigation/native’ and ‘@react-navigation/native-stack’ respectively.

//in App.js

import 'package:finalquiz/routes/app_route.dart';
import 'package:flutter/material.dart';

export default function App() {
...
const Stack = createNativeStackNavigator();
...
}

1.3 Defining the pages & path in Navigation Container

To add the page to navigation Container, it included:-

1. First page of app to be shown is specify in:
“initialRouteName=”Welcome”. (Can change “Welcome”. to “Question” or “Result”)

//in App.js
export default function App() {
...
return (
<NavigationContainer>
// Location of first page of app
<Stack.Navigator initialRouteName="Welcome">
<Stack.Screen name="Welcome" component={WelcomePage}
options={{ headerShown: false, }} />
<Stack.Screen name="Home" component={QuizPage}
options={{
title: 'Question',
headerStyle: {
backgroundColor: '#EDA276',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
}, }}/>
<Stack.Screen name="Result" component={ResultPage}
options={{headerShown: false}}/>
</Stack.Navigator>
</NavigationContainer>
);
}

2. Every Page is listed as below
— Wrapped in <Stack.Screen>, which Stack is defined earlier in code const Stack = createNativeStackNavigator();
— name:
referring to the name of the path
component: referring to the function/component of the Page
options: referring to the setting of the Page, including title, colors and even whether headerShown.

<Stack.Screen name="Home" component={QuizPage} 
options={{
title: 'Question',
headerStyle: {
backgroundColor: '#EDA276',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
}, }}/>

1.4 Navigate between pages

To navigate between pages, after declaring the above-mentioned information in App() inside the <NavigationController />, in each page component, we can call to navigate by:-
1. Defining navigation
2. Calling navigation.navigate

const WelcomePage = ({navigation}) => { //1. Defining navigation
return(
<View>
....
<TouchableOpacity
onPress={() =>{
navigation.navigate('Home'); //2. calling navigation.navigate
startQuiz();}
}>
....
</TouchableOpacity>
</View>)}

2. Screens

2.1 Quiz Screen (QuizPage function in App.js)

2.1.1 Question Navigating — use of CurrentQuestionIndex

To handle the navigating amongst questions, a CurrentQuestionIndex is introduced and the steps are:

  1. Define the Variable (import the questionbank + setQuestionIndex
  2. Define the Function to handle the State change of currentQuestionIndex
  3. Define the page set up(UI) to show the question based on this index
    a. Showing the question
    b. Showing the options, with “.map” to show each option
//In App.js

import data from './QuizData';

export default function App() {
//1. Define the Varible (import the questionbank + setQuestionIndex
const allQuestions = data;
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
...

//2. Define the Function to handle the State change of currentQuestionIndex
const handleNext = (navigation) => {
if(currentQuestionIndex== allQuestions.length-1){
navigation.navigate('Result');
}else{
setCurrentQuestionIndex(currentQuestionIndex+1);
...
}
....
}

//3. Define the page set up(UI) to show the question based on this index
const QuizPage = ({navigation}) => {

return (...
//3a. Showing the question
<Text style={{...}}>{allQuestions[currentQuestionIndex]?.question}
</Text>
...
//3b. Showing the options, with ".map" to show each option
<View>
{allQuestions[currentQuestionIndex]?.options.map((option,index) => ((option,index) => (
...
<Text style={{...} } >{option}</Text>)
...
</View>
})
}

2.1.2 Animation

There are three major animations in the application:

  1. The “Left to Right Swipe Page” animation, which is handled by the navigation (mentioned in 1).
  2. The “Question Progress Bar” animation, which is handled by the Animated in React Native discuss soon.
  3. The “Button Swipe Up” animation, which is also handled by the Animated in React Native discuss soon.

To allow the animations to run on each new question page, animated.view is wrapping the progress bar and answer button, as shown.

For the Question Progress Bar animation, there are three main codes in App():

  1. Define Animated.Value in useState
  2. Define the functions to trigger Animation
    a. Define the sequence of animation
    b. Define the duration of animation
  3. Define the Page set up to show the animation
    a. Wrap the animated component inside Animated.View
    b. Locating the animatedValue in width of bar
//In App.js

import { ..., Animated } from 'react-native';

...
//Inside App() function

//1. Define Animated.Value in useState
const [progress, setProgress] = useState(new Animated.Value(0));
const progressAnim = progress.interpolate({
inputRange: [0, allQuestions.length],
outputRange: ['0%','100%']
})

//2. Define the functions to trigger Animation
const handleNext = (navigation) => {
...
//2a. Define the sequence of animation
Animated.parallel([
//2b. Define the duration of animation
Animated.timing(progress, {
toValue: currentQuestionIndex+2,
duration: 2000,
useNativeDriver: false
}),
...
]).start();
}
//3. Define the Page set up to show the animation
const QuizPage = ({navigation}) => {
return(
...
//For the Progress Bar
//3a. Wrap the animated component inside Animated.View
<Animated.View style={[{
height: 5,
borderRadius: 5,
backgroundColor: COLORS.accent+'90'
},{
//3b. Locating the animatedValue in the target location (here is width of the progress bar)
width: progressAnim
}]}>
</Animated.View>
...)
}

Similarly, to create “Button Swipe Up” animation, same set up as question progress bar is defined. The only difference is the animatedValue used for FadeTransition(opacity) and Transform.

//In App.js, Inside App() function

//1. Define Animated.Value in useState
const [fadeAnim, setFadeAnim] = useState(new Animated.Value(0));

//2. Define the functions toc trigger Animation
const handleNext = (navigation) => {
...
Animated.parallel([
...
Animated.sequence([
Animated.timing(fadeAnim,{
toValue: 0,
duration: 100,
useNativeDriver: false
}),
Animated.timing(fadeAnim,{
toValue: 1,
duration: 1900,
useNativeDriver: false})
])
]).start();
}
...
//3. Define the Page set up to show the animation
//For the Swipe up of button
{allQuestions[currentQuestionIndex]?.options.map((option,index) => (
//3a. Wrap the animated component inside Animated.View
<Animated.View
key={index}
//3b. Locating the animatedValue in the target location (here is width of the opacity and translateY)
style={{opacity:fadeAnim,
transform: [{
translateY: fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150 / 4 *(index+10), 0]
}),
}],
}} >
...
<Text style={{...} } >{option}</Text>
</Animated.View> ))
}

Overall View on the Quiz Page

In summary, to create the Quiz Page with the above-mentioned function, it must contain three parts:

  1. Define Variable Parts
//In App.js, Inside App() function 

//1. Define Variable Parts
const allQuestions = data;
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
const [isOptionsDisabled, setIsOptionsDisabled] = useState(false);
const [currentOptionSelected, setCurrentOptionSelected] = useState(null);
const [correctOption, setCorrectOption] = useState(null);
const [score, setScore] = useState(0)

const [progress, setProgress] = useState(new Animated.Value(0));
const [fadeAnim, setFadeAnim] = useState(new Animated.Value(0));

const progressAnim = progress.interpolate({
inputRange: [0, allQuestions.length],
outputRange: ['0%','100%']
})

const Stack = createNativeStackNavigator();

2. Define Function Parts

//In App.js, Inside App() function 

//2. Define Function Parts
const validateAnswer = (selectedOption,navigation) => {
if (isOptionsDisabled == false){
let correct_option = allQuestions[currentQuestionIndex]['correct_option'];
setCurrentOptionSelected(selectedOption);
setCorrectOption(correct_option);
setIsOptionsDisabled(true);
if(selectedOption==correct_option){
setScore(score+1)
}
}else{
handleNext(navigation)
}
}

const handleNext = (navigation) => {
if(currentQuestionIndex== allQuestions.length-1){
navigation.navigate('Result');
}else{
setCurrentQuestionIndex(currentQuestionIndex+1);
setCurrentOptionSelected(null);
setCorrectOption(null);
setIsOptionsDisabled(false);
}
Animated.parallel([
Animated.timing(progress, {

toValue: currentQuestionIndex+2,
duration: 2000,
useNativeDriver: false
}),
Animated.sequence([
Animated.timing(fadeAnim,{
toValue: 0,
duration: 100,
useNativeDriver: false
}),
Animated.timing(fadeAnim,{
toValue: 1,
duration: 1900,
useNativeDriver: false})
])
]).start();

}

3. Define Page Components

//In App.js, Inside App() function 

//3.Define Page Components
const QuizPage = ({navigation}) => {

return (
<ScrollView style={styles.scrollView}>
<View style={{
flex: 1,
paddingVertical: 20,
paddingHorizontal: 30,
backgroundColor: COLORS.background,
position:'relative',
}}>
<View style={{
marginTop: 50,
marginVertical: 10,
padding: 40,
borderTopRightRadius: 40,
borderRadius: 10,
backgroundColor: 'white',
alignItems: 'center',
shadowColor: '#171717',
shadowOffset: {width: -6, height: 6},
shadowOpacity: 0.2,
shadowRadius: 3,
}}>
{/* Progress Bar */}
<View style={{
width: '80%',
height: 5,
borderRadius: 5,
backgroundColor: '#00000020',
marginBottom: 10

}}>
<Animated.View style={[{
height: 5,
borderRadius: 5,
backgroundColor: COLORS.accent+'90'
},{
width: progressAnim
}]}>
</Animated.View>
</View>
{/* Question */}
<View>
{/* Question Counter */}
<View style={{
flexDirection: 'row',
alignItems: 'flex-end'

}}>
<Text style={{color: COLORS.black, fontSize: 15, opacity: 0.6, marginRight: 2}}>{currentQuestionIndex+1}</Text>
<Text style={{color: COLORS.black, fontSize: 13, opacity: 0.6}}>/ {allQuestions.length}</Text>
</View>

{/* Question */}
<Text style={{
color: COLORS.black,
fontSize: 18,
textAlign: 'center',

}}>{allQuestions[currentQuestionIndex]?.question}</Text>
</View>
</View>
{renderOptions(navigation)}
</View>
</ScrollView>
)
}

const renderOptions = (navigation) => {

return (
<View style={{marginTop:100}}>

{
allQuestions[currentQuestionIndex]?.options.map((option,index) => (
<Animated.View
key={index}
style={{opacity:fadeAnim,
transform: [{
translateY: fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150 / 4 *(index+10), 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}} >
<TouchableOpacity
onPress={()=> validateAnswer(option,navigation)}
key={option}
style={{backgroundColor:
isOptionsDisabled ?
option==correctOption
? COLORS.success
: option==currentOptionSelected
? COLORS.error
: COLORS.grey
: COLORS.accent,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center',
padding: 10,
paddingHorizontal: 30,
marginVertical: 10,
shadowColor: '#171717',
shadowOffset: {width: -3, height: 3},
shadowOpacity: 0.2,
shadowRadius: 3, }}>
<Text style={{fontSize: 15, color: COLORS.white, textAlign: 'center',} } >{option}</Text>
</TouchableOpacity>
</Animated.View>
))
}

</View>
)
}

2.2 & 2.3 Welcome Page & Result Page (WelcomePage & ResultPage function in App.js))

These two pages are relatively straightforward. It mainly used the View, TouchableOpacity and Text.

2.2 Welcome Page:

  1. Define Function Part
//In App.js, Inside App() function 

//1. Define the Function used in Welcome Page
const startQuiz = () => {
Animated.sequence([
Animated.timing(fadeAnim,{
toValue: 0,
duration: 100,
useNativeDriver: false
}),
Animated.timing(fadeAnim,{
toValue: 1,
duration: 1900,
useNativeDriver: false})
]).start();


Animated.timing(progress, {
toValue: currentQuestionIndex+1,
duration: 2000,
useNativeDriver: false,
}).start();

}

2. Define Page Component

//In App.js, Inside App() function 

//2.Define the UIView and styling of Result Page
const WelcomePage = ({navigation}) => {

return (

<View style={{
flex: 1,
backgroundColor: COLORS.background,
alignItems: 'center',
justifyContent: 'center',
}}>

<Image style={{
width: '100%',
height: 400,
resizeMode: 'contain',
}} source={require('./assets/driving_img1.jpg')} />
<View style={{
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
marginVertical: 20,
marginHorizontal: 20
}}>
<Text style={{
fontSize: 25,
fontWeight: 'bold',
color: COLORS.black
}}>Ready For your Written Test?</Text>

</View>
{/* Retry Quiz button */}
<TouchableOpacity
onPress={() =>{

navigation.navigate('Home');
startQuiz();
}
}
style={{
backgroundColor: COLORS.black,
paddingHorizontal: 5,
paddingVertical: 20,
width: '50%', borderRadius: 15,
}}>
<Text style={{
textAlign: 'center', color: COLORS.white, fontSize: 20
}}>Let's Begin</Text>
</TouchableOpacity>


</View>)

}

2.3 Result Page:

  1. Define Function Part
//In App.js, Inside App() function 

//1.Define the Function used in Result Page
const restartQuiz = () => {
setCurrentQuestionIndex(0);
setScore(0);
setCurrentOptionSelected(null);
setCorrectOption(null);
setIsOptionsDisabled(false);
}

2. Define Page Component

//In App.js, Inside App() function 

//2.Define the UIView and styling of Result Page
const ResultPage = ({navigation}) => {

return (

<View style={{
flex: 1,
backgroundColor: COLORS.background,
alignItems: 'center',
justifyContent: 'center'
}}>
<View style={{
backgroundColor: COLORS.background,
width: '90%',
borderRadius: 20,
padding: 20,
alignItems: 'center'
}}>
<Text style={{fontSize: 30}}>Your Score</Text>

<View style={{
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
marginVertical: 30
}}>
<Text style={{
fontSize: 100,
color: COLORS.black,
fontWeight: 'bold'
}}>{score}</Text>
<Text style={{
fontSize: 100, color: COLORS.black,
fontWeight: 'bold'
}}> / { allQuestions.length }</Text>
</View>
{/* Retry Quiz button */}
<TouchableOpacity
onPress={()=>{
restartQuiz();
navigation.navigate('Welcome');}}
style={{
backgroundColor: COLORS.black,
paddingHorizontal: 5,
paddingVertical: 15,
width: '50%', borderRadius: 15,
}}>
<Text style={{
textAlign: 'center', color: COLORS.white, fontSize: 20
}}>Retry</Text>
</TouchableOpacity>

</View>

</View>

)
}

  • * Be Aware that all the three Page Components are inside the App() components in App.js

3. Models — the Question Bank (QuizData.js)

The main data used in this quiz app are the questions, which include corresponding options and the correct answer.

//In QuizData.js

export default data = [
{
question: "What should you do when approaching a yellow traffic light?",
options: ["Speed up and cross the intersection quickly","Come to a complete stop","Slow down and prepare to stop","Ignore the light and continue driving"],
correct_option: "Slow down and prepare to stop"
},
{
question: "What does a red octagonal sign indicate?",
options: ["Yield right of way","Stop and proceed when safe","Merge with traffic","No left turn allowed"],
correct_option: "Stop and proceed when safe"
},
{
question: "What is the purpose of a crosswalk?",
options: ["A designated area for parking","A place to stop and rest","A path for pedestrians to cross the road","A location for U-turns"],
correct_option: "A path for pedestrians to cross the road"
}
]

Here is the outline of this code. Hope this is useful to you. To quickly use this code, you can replace the questions with your own. The page will update automatically.

If you need the full code, you can check out this GitHub link. And if you have any questions or just want to say hi, feel free to drop me a message. Now go forth and build the awesome quiz apps! If you can interested to create in Flutter, you can also check on this code and article.

Animated Quiz App UI using React Native

Source Code could be found:

https://github.com/Katrinasms/React-quiz-app-template

--

--