Skip to content

How to create custom Pull To Refresh animations

tutorial, react-native5 min read

What is Pull to Refresh?

I’ve always been fascinated by different UI interactions, and how something so unique and different, can be made so familiar and easy to use. A great example that is widely used today is pull-to-refresh.

alt text

Pull to refresh on iOS

This interaction can be found in almost every mobile app, but I guarantee you that 99.99% of you have never heard Loren Brichter even though we’ve been using his work every day. You can read more about how pull-to-refresh is born here.

Since then, many have come up with different kinds of custom pull to refresh animations.

alt text

Credits: https://dribbble.com/shots/3026988-Pull-To-Refresh-Airbnb

alt text

Credits: https://dribbble.com/shots/2242263--1-Pull-to-refresh-Freebie-Weather-Concept

alt text

This one’s my favorite. Stacking burgers! Credits: https://dribbble.com/shots/10733383-Pull-to-refresh

After digging around, I’ve realized that there isn’t any easy way to create these cool animations with React Native, so I’ve decided to come up with a solution of my own.

Let’s get started

I wanted to create something a bit more developer-friendly, as a lot of the solutions out there only applies for a specific animation. Introducing Lottie.

A Lottie is a JSON-based animation file format that enables designers to ship animations on any platform as easily as shipping static assets. They are small files that work on any device and can scale up or down without pixelation.

Using Lottie allows us to quickly drop an animation into our pull to refresh component, and works automagically.

Here’s what we’re going to be building today:

alt text

Custom pull to refresh animation using Lottie


Step 1: Adding Lottie Animation

For simplicity’s sake, I’ll have to assume you guys already know how to use a FlatList . For those who want to follow along, I’ve created a starter template for this tutorial. You can download it here. Don’t forget to run yarn install or npm install in the project after downloading it to make sure you get all the dependencies installed.

The first thing you’ll want to do is to get a Lottie animation. Head over Lottie Files, pick an animation that you like, and then download it to Lottie JSON format. For this tutorial I will be using this one:

alt text

Credits to Alex Martov: https://lottiefiles.com/9258-bouncing-fruits

After downloading your desired Lottie animation, open up the project folder, and move the animation file into the assets/ folder. Again, make sure the animation is a .json file! If not, go back to Lottiefiles and download the JSON version.

alt text

I’ve renamed my animation to “bouncing-fruits.json”

Now that you got the animation file, the next thing to do is to load it and display the animation. In order to do that, we’ll need to use lottie-react-native. Go ahead and install it by running yarn add lottie-react-native (or use npm install lottie-react-native)

Navigate to FruitList.jsx, right below the import statements, add the following:

1import React from 'react';
2import {
3 View,
4 FlatList,
5 Text,
6 StyleSheet,
7} from 'react-native';
8
9// 1. Import LottieView
10import LottieView from 'lottie-react-native';
11
12// 2. Change the require path, to the path of the animation downloaded
13// from LottieFiles. Mine is in ../assets/bouncing-fruits.json.
14const fruitsAnimation = require('../assets/bouncing-fruits.json');
15
16//...

Now inside styles let’s create another style called lottieView

1const styles = StyleSheet.create({
2 // ...
3 // 3. Create a new style
4 lottieView: {
5 height: 100,
6 alignSelf: 'center',
7 },
8});

And finally, for the exciting part, let’s wrap the existing FlatList and LottieView into a new View. Below is the full code for FruitList.jsx.

1import React from 'react';
2import {
3 View,
4 FlatList,
5 Text,
6 StyleSheet,
7} from 'react-native';
8
9// 1. Import LottieView
10import LottieView from 'lottie-react-native';
11
12// 2. Change the require path, to the path of the animation downloaded
13// from LottieFiles. Mine is in ./assets/bouncing-fruits.json.
14const fruitsAnimation = require('../assets/bouncing-fruits.json');
15
16const fruits = [
17 'Apple',
18 'Orange',
19 'Watermelon',
20 'Avocado',
21 'Blueberry',
22 'Coconut',
23 'Durian',
24 'Mango',
25];
26
27const styles = StyleSheet.create({
28 flatlist: {
29
30 },
31 row: {
32 height: 100,
33 justifyContent: 'center',
34 padding: 20,
35 borderBottomWidth: 3,
36 borderBottomColor: 'black',
37 backgroundColor: 'white',
38 },
39 rowTitle: {
40 fontSize: 30,
41 fontWeight: 'bold',
42 },
43
44 // 3. Create a new style
45 lottieView: {
46 height: 100,
47 alignSelf: 'center',
48 },
49});
50
51function FruitList() {
52 function renderItem({ item }) {
53 return (
54 <View key={item} style={styles.row}>
55 <Text style={styles.rowTitle}>{item}</Text>
56 </View>
57 );
58 }
59
60 return (
61 // 4. Create a View to include both LottieView and FlatList
62 <View>
63 {/* 5. Add LottieView */}
64 <LottieView
65 autoPlay
66 style={styles.lottieView}
67 source={fruitsAnimation}
68 />
69 <FlatList
70 data={fruits}
71 renderItem={renderItem}
72 style={[
73 styles.flatlist,
74 {
75 paddingTop: 20,
76 },
77 ]}
78 />
79 </View>
80 );
81}
82
83export default FruitList;

Now let’s run the app, and you’d probably see something like this

alt text

Bouncing fruits animation

Awesome! Except, the animation runs immediately when we open the app. It’s not exactly what we’re looking for… Let’s figure out how to actually turn this into a pull to refresh.

Step 2: “Hiding” animation behind the FlatList

The idea to achieve the pull to refresh effect is simple

  1. Hide the animation behind the FlatList.
  2. As the FlatList scrolls down, track the changes in y-offset (how much did they scroll down), and animate the animation appropriately.
  3. When the user released the scroll view (onResponderRelease) check if the offset is enough to trigger a refresh.
  4. The animation remains visible during the entire duration of the refreshing.
  5. When refreshing ends, stops the animation and FlatList is then scrolled back up to hide the animation.

To hide the animation behind the Flatlist, all we have to do is go back to FruitList.jsx and update the styles for LottieView.

1const styles = StyleSheet.create({
2 //..
3 // Update lottieView style.
4 // This will enable the LottieView to appear BEHIND the FlatList.
5 lottieView: {
6 height: 100,
7 position: 'absolute',
8 top: 5,
9 left: 0,
10 right: 0,
11 },
12});

To those not following using the starter project, for this to work the FlatList must have a transparent background color (or no background color), and each row should have a background color. Only by doing so we can allow the animation to be visible when the FlatList is scrolled down.

Step 3: Tracking scroll offset

To track the scrolling offset, we’re going to make use of the onScroll prop, and then store that into a state called offsetY.

At the very top of FruitList.jsx, modify the import statement to include useState, and then create a new state called offsetY.

1// 1. Import useState
2import React, { useState } from 'react';
3//...
4
5function FruitList() {
6 // 2. Create a new state called offsetY
7 const [offsetY, setOffsetY] = useState(0);
8
9 //...
10}
11
12export default FruitList;

Create a function onScroll that receives a scroll event and then sets the y offset to state.

1// 3. Create onScroll function
2// to capture scroll events
3function onScroll(event) {
4 const { nativeEvent } = event;
5 const { contentOffset } = nativeEvent;
6 const { y } = contentOffset;
7 setOffsetY(y);
8}

And then remember to assign the onScroll function to the onScroll FlatList prop.

1//...
2
3function FruitList() {
4 //...
5 return (
6 <View>
7 <LottieView
8 autoPlay
9 style={styles.lottieView}
10 source={fruitsAnimation}
11 />
12 <FlatList
13 data={fruits}
14 renderItem={renderItem}
15 style={[
16 styles.flatlist,
17 {
18 paddingTop: 20,
19 },
20 ]}
21 // Assign onScroll function to onScroll prop
22 onScroll={onScroll}
23 />
24 </View>
25 );
26}
27
28//...

And Voilà! We are now able to track the y offset of the Flatlist. Now let’s make use of the offset y, and then animate the animation according to the offset. Fortunately for us, LottieView provided us with the prop progress. Here’s what the documentation has to say:

progress: A number between 0 and 1, or an Animated number between 0 and 1. This number represents the normalized progress of the animation. If you update this prop, the animation will correspondingly update to the frame at that progress value. This prop is not required if you are using the imperative API.

Hmm… interesting. So progress is a percentage that determines the frame of the animation. Currently, we have the yOffset, and if you do console.log(yOffset) and scroll down, you’ll see something like this:

-0.5
-15.5
-75.5
-101.5

To convert that into a percentage (value between 0 to 1), we’ll also need the height where the refreshing happens.

Conveniently, we already have that. Take a look lottieView styles, and you’ll notice the height is set to 100, which will be our height. Let’s refactor that by creating a new variable called refreshingHeight above the styles.

1// Create new variable called refreshingHeight
2const refreshingHeight = 100;
3
4const styles = StyleSheet.create({
5 //...
6 lottieView: {
7 // Use refreshingHeight instead of hardcoded value
8 height: refreshingHeight,
9 position: 'absolute',
10 top: 5,
11 left: 0,
12 right: 0,
13 },
14});

Now to get the progress, all we have to do it take the -offsetY / refreshingHeight

Right after the onScroll function, we’ll create the progress variable and assign it to LottieView prop.

1// Only set progress when offset is negative
2let progress = 0;
3if (offsetY <= 0) {
4 progress = -offsetY / refreshingHeight;
5}
6
7return (
8 <View>
9 <LottieView
10 // Removed autoplay
11 style={styles.lottieView}
12 source={fruitsAnimation}
13 // Set progress
14 progress={progress}
15 />
16 <FlatList
17 data={fruits}
18 renderItem={renderItem}
19 style={[
20 styles.flatlist,
21 {
22 paddingTop: 20,
23 },
24 ]}
25 onScroll={onScroll}
26 />
27 </View>
28);

If you run the app now, you should be able to scroll down and see that the animation follows your scrolling!

alt text

Step 4: On Refresh

Remember the plan that is detailed in Step 2? Here’s what we’ve done so far.

  1. H̶i̶d̶e̶ ̶t̶h̶e̶ ̶a̶n̶i̶m̶a̶t̶i̶o̶n̶ ̶b̶e̶h̶i̶n̶d̶ ̶t̶h̶e̶ ̶F̶l̶a̶t̶L̶i̶s̶t̶
  2. A̶s̶ ̶t̶h̶e̶ ̶F̶l̶a̶t̶L̶i̶s̶t̶ ̶s̶c̶r̶o̶l̶l̶s̶ ̶d̶o̶w̶n̶,̶ ̶t̶r̶a̶c̶k̶ ̶t̶h̶e̶ ̶c̶h̶a̶n̶g̶e̶s̶ ̶i̶n̶ ̶y̶-̶o̶f̶f̶s̶e̶t̶ ̶(̶h̶o̶w̶ ̶m̶u̶c̶h̶ ̶d̶i̶d̶ ̶t̶h̶e̶y̶ ̶s̶c̶r̶o̶l̶l̶ ̶d̶o̶w̶n̶)̶,̶ ̶a̶n̶d̶ ̶a̶n̶i̶m̶a̶t̶e̶ ̶t̶h̶e̶ ̶a̶n̶i̶m̶a̶t̶i̶o̶n̶ ̶a̶p̶p̶r̶o̶p̶r̶i̶a̶t̶e̶l̶y̶.̶
  3. When the user released the scroll view (onResponderRelease) check if the offset is enough to trigger a refresh.
  4. The animation remains visible during the entire duration of the refreshing.
  5. When refreshing ends, stops the animation and FlatList is then scrolled back up to hide the animation.

Let’s start by creating another new state, isRefreshing. This will be used to track if the list is refreshing or not. Add the following below the offsetY state.

1const [offsetY, setOffsetY] = useState(0);
2// 1. Create a new state isRefreshing
3const [isRefreshing, setIsRefreshing] = useState(false);

And then create the function onRelease right after the onScroll function. And then assign it to the FlatList prop onResponderRelease.

1//...
2
3function FruitList() {
4 const [offsetY, setOffsetY] = useState(0);
5
6 // 1. Create a new state isRefreshing
7 const [isRefreshing, setIsRefreshing] = useState(false);
8
9 //...
10
11 function onScroll(event) {
12 const { nativeEvent } = event;
13 const { contentOffset } = nativeEvent;
14 const { y } = contentOffset;
15 setOffsetY(y);
16 }
17
18 // 2. Create onRelease function
19 function onRelease() {
20 // offsetY must be less than the refreshing height
21 // to trigger refresh
22 if (offsetY <= -refreshingHeight && !isRefreshing) {
23 // For this example, we will set refreshing to true
24 // and then set it to false after 3 seconds.
25 // In your app this is where the actual refreshing happens
26 setIsRefreshing(true);
27 setTimeout(() => {
28 setIsRefreshing(false);
29 }, 3000);
30 }
31 }
32
33 //...
34
35 return (
36 <View>
37 <LottieView
38 style={styles.lottieView}
39 source={fruitsAnimation}
40 progress={progress}
41 />
42 <FlatList
43 data={fruits}
44 renderItem={renderItem}
45 style={[
46 styles.flatlist,
47 {
48 paddingTop: 20,
49 },
50 ]}
51 onScroll={onScroll}
52
53 // 3. Assign onRelease to the prop
54 onResponderRelease={onRelease}
55 />
56 </View>
57 );
58}
59
60//...

Step 5: Animation remains visible while refreshing

To make sure the animation is visible, we’ll add an empty ListHeaderComponent to the existing FlatList, with the paddingTop equivalent to the refreshingHeight.

Below isRefreshing, Create a new state extraPaddingTop. This will hold the padding value for our invisible ListHeaderComponent. And then in the onRelease function, set the extraPaddingTop to the refreshingHeight when it’s refreshing, and set it to 0 when it has finished refreshing.

1// 1. Create extraPaddingTop state
2const [extraPaddingTop, setExtraPaddingTop] = useState(0);
3
4//...
5
6// 2. Modify onRelease to update extraPaddingTop
7function onRelease() {
8 if (offsetY <= -refreshingHeight && !isRefreshing) {
9 setIsRefreshing(true);
10 setTimeout(() => {
11 setIsRefreshing(false);
12 }, 3000);
13 }
14}
15
16return (
17 <View>
18 <LottieView
19 style={styles.lottieView}
20 source={fruitsAnimation}
21 progress={progress}
22 />
23 <FlatList
24 data={fruits}
25 renderItem={renderItem}
26 style={[
27 styles.flatlist,
28 {
29 paddingTop: 20,
30 },
31 ]}
32 onScroll={onScroll}
33 onResponderRelease={onRelease}
34
35 // 3. Create list header component, with extraPaddingTop
36 // set to paddingTop style
37 ListHeaderComponent={(
38 <View style={{
39 paddingTop: extraPaddingTop,
40 }}
41 />
42 )}
43 />
44 </View>
45);
46
47//...

We’re almost there! Running the app now will display something like this

alt text

A couple of things doesn’t seem right. The animation is not moving while it’s refreshing, and when the refresh is complete, the FlatList jumps abruptly back up. But don’t worry we’ll fix those things next.

Step 6: Animate while refreshing, and hides when refreshing ends

We’ll start by making the FlatList collapse graciously when the refreshing ended. But before that let’s refactor our code a little. Let’s use useEffect to update the other states whenever the isRefreshing is updated.

1// 1. Import useEffect
2import React, { useState, useEffect } from 'react';
3
4function FruitList() {
5 const [offsetY, setOffsetY] = useState(0);
6 const [isRefreshing, setIsRefreshing] = useState(false);
7 const [extraPaddingTop, setExtraPaddingTop] = useState(0);
8
9 // 2. Whenever [isRefreshing] has been updated, do the following
10 useEffect(() => {
11 if (isRefreshing) {
12 setExtraPaddingTop(refreshingHeight);
13 } else {
14 setExtraPaddingTop(0);
15 }
16 }, [isRefreshing]);
17
18
19 function onRelease() {
20 if (offsetY <= -refreshingHeight && !isRefreshing) {
21 setIsRefreshing(true);
22 // 3. Removed setExtraPaddingTop(refreshingHeight);
23 setTimeout(() => {
24 setIsRefreshing(false);
25 // 4. Removed setExtraPaddingTop(0);
26 }, 3000);
27 }
28 }
29
30 //...
31}
32
33//...

Now all we have to do inonRelease is to setIsRefreshing and our useEffect hook will automatically set the padding for us.

Now, let’s fix the issue with our animation. Create a new ref, and attach it to LottieView, this way we’ll be able to trigger the play function during refresh. Refer to the code below.

1// 1. Import useRef
2import React, { useState, useEffect, useRef } from 'react';
3//...
4
5function FruitList() {
6 const [offsetY, setOffsetY] = useState(0);
7 const [isRefreshing, setIsRefreshing] = useState(false);
8 const [extraPaddingTop, setExtraPaddingTop] = useState(0);
9
10 // 2. Create lottieViewRef
11 const lottieViewRef = useRef(null);
12
13 useEffect(() => {
14 if (isRefreshing) {
15 setExtraPaddingTop(refreshingHeight);
16 // 3. Trigger play when is refreshing
17 lottieViewRef.current.play();
18 } else {
19 setExtraPaddingTop(0);
20 }
21 }, [isRefreshing]);
22
23 //...
24
25 return (
26 <View>
27 <LottieView
28 // 3. Attach lottieViewRef to ref prop
29 ref={lottieViewRef}
30 style={styles.lottieView}
31 source={fruitsAnimation}
32 progress={progress}
33 />
34 //...
35 </View>
36 );
37}
38
39//...

And finally, let’s make the FlatList collapse gracefully when the animation ends. To do that, we’ll make use of React Native’s Animated API, and update extraPaddingTop from a fixed value to an animated value.

1// ...
2
3// 1. Import Animated and Easing
4import {
5 View,
6 FlatList,
7 Text,
8 StyleSheet,
9 Animated,
10 Easing,
11} from 'react-native';
12
13//...
14
15function FruitList() {
16 // 2. Modify extraPaddingTop state to use Animated.Value
17 const [extraPaddingTop] = useState(new Animated.Value(0));
18
19 const lottieViewRef = useRef(null);
20
21 useEffect(() => {
22 if (isRefreshing) {
23 // 3. Instead of setting extraPaddingTop to refreshingHeight,
24 // we'll animate it instead.
25 // duration is set to 0 so it appears immediately.
26 Animated.timing(extraPaddingTop, {
27 toValue: refreshingHeight,
28 duration: 0,
29 }).start();
30 lottieViewRef.current.play();
31 } else {
32 // 4. Instead of setting extraPaddingTop to 0,
33 // we'll animate it instead.
34 Animated.timing(extraPaddingTop, {
35 toValue: 0,
36 duration: 400,
37 easing: Easing.elastic(1.3),
38 }).start();
39 }
40 }, [isRefreshing]);
41
42 return (
43 <View>
44
45 //...
46
47 <FlatList
48 data={fruits}
49 renderItem={renderItem}
50 style={[
51 styles.flatlist,
52 {
53 paddingTop: 20,
54 },
55 ]}
56 onScroll={onScroll}
57 onResponderRelease={onRelease}
58 ListHeaderComponent={(
59 // 5. To use Animated values,
60 // we have to use Animated.View
61 // instead of a normal View
62 <Animated.View style={{
63 paddingTop: extraPaddingTop,
64 }}
65 />
66 )}
67 />
68 </View>
69 );
70}
71
72//...

And we’re done!

alt text

Your newly completed pull to refresh animation

You can find the completed project here. Or in the starter project, you can check out to the branch completed to see the completed version.


Edit: To those of you interested in using as a component, I’ve also made it available as an npm package. Check it out here.

Links

Enjoyed this article?

Are you ready to go to the next level? 🚀 Register to our newsletter now to receive freshly brewed software development, design, and business articles and tips, right to your inbox! 📥
Our promise to you: No spam, just great articles.
© 2020 by blog.groftware.tech. All rights reserved.