Refreshing content seamlessly enhances user experience in mobile applications. In this blog, we'll explore how to implement an animated pull-to-refresh feature in React Native using react-native-reanimated
and Lottie animations. π
π Key Features
β
Smooth pull-down gesture for refreshing
β
Animated loader using Lottie
β
Optimized performance with Reanimated
β
Works seamlessly with FlatList
π§ Setting Up the Project
To get started, install the required dependencies:
npm install react-native-reanimated lottie-react-native react-native-safe-area-context
ποΈ Implementing the Pull-to-Refresh Component
import React, { useRef, useCallback, memo } from 'react';
import {
StyleSheet,
Text,
View,
Image,
PanResponder,
Dimensions,
StatusBar,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
useAnimatedScrollHandler,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import LottieView from 'lottie-react-native';
import data from './data';
const { width } = Dimensions.get('screen');
const AnimatedPullToRefresh = () => {
const scrollPosition = useSharedValue(0);
const insets = useSafeAreaInsets();
const pullDownPosition = useSharedValue(0);
const isReadyToRefresh = useSharedValue(false);
const isLoaderActive = useSharedValue(false);
const onRefresh = useCallback((done) => {
isLoaderActive.value = true;
setTimeout(() => {
isLoaderActive.value = false;
isReadyToRefresh.value = false;
done();
}, 5000);
}, []);
const onPanRelease = () => {
pullDownPosition.value = withTiming(isReadyToRefresh.value ? 120 : 0, {
duration: 180,
});
if (isReadyToRefresh.value) {
isReadyToRefresh.value = false;
onRefresh(() => {
pullDownPosition.value = withTiming(0, { duration: 180 });
});
}
};
const panResponderRef = useRef(
PanResponder.create({
onStartShouldSetPanResponderCapture: (_, gestureState) => {
return scrollPosition.value <= 0 && gestureState.dy > 0;
},
onMoveShouldSetPanResponderCapture: (_, gestureState) => {
return scrollPosition.value <= 0 && gestureState.dy > 0;
},
onPanResponderMove: (_, gestureState) => {
const maxPullDistance = 150;
pullDownPosition.value = Math.min(
maxPullDistance,
Math.max(0, gestureState.dy),
);
isReadyToRefresh.value = pullDownPosition.value >= maxPullDistance / 2;
},
onPanResponderRelease: onPanRelease,
onPanResponderTerminate: onPanRelease,
}),
);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollPosition.value = event.contentOffset.y;
},
});
const pullDownStyle = useAnimatedStyle(() => ({
transform: [{ translateY: pullDownPosition.value }],
}));
const refreshContainerStyle = useAnimatedStyle(() => ({
height: pullDownPosition.value,
opacity: 1,
top: pullDownPosition.value - 200,
}));
const renderItem = useCallback(
({ item }) => (
<View>
<Image source={item.image} style={styles.image} resizeMode="cover" />
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subTitle}>{`${item.director} | ${item.year}`}</Text>
</View>
),
[],
);
return (
<View style={styles.container}>
<StatusBar backgroundColor={'#000'} />
<Animated.View style={[refreshContainerStyle, styles.loaderContainer]}>
<LottieView
source={require('./4.json')}
autoPlay
loop
speed={0.5}
style={styles.loader}
/>
</Animated.View>
<Animated.View
style={[
pullDownStyle,
styles.pullDownStyles,
{ paddingTop: Math.max(insets.top, 15) },
]}
{...panResponderRef.current.panHandlers}
>
<Animated.FlatList
data={data}
scrollEventThrottle={16}
renderItem={renderItem}
keyExtractor={(_, index) => index.toString()}
ItemSeparatorComponent={() => (
<View style={styles.itemSeparatorStyle} />
)}
onScroll={scrollHandler}
numColumns={2}
showsVerticalScrollIndicator={false}
overScrollMode="never"
/>
</Animated.View>
</View>
);
};
export default memo(AnimatedPullToRefresh);
const styles = StyleSheet.create({
container: {
backgroundColor: '#111',
flex: 1,
},
pullDownStyles: {
backgroundColor: '#0A0A0A',
flex: 1,
paddingHorizontal: 5,
},
itemSeparatorStyle: {
margin: 6,
},
image: {
width: 200,
height: 300,
marginRight: 10,
borderRadius: 8,
},
loader: {
width,
height: 300,
},
loaderContainer: {
alignItems: 'center',
width,
position: 'absolute',
},
title: {
width: 180,
color: '#fff',
fontSize: 16,
fontWeight: '600',
marginTop: 15,
marginBottom: 5,
},
subTitle: {
width: 180,
color: '#888',
fontSize: 12,
fontWeight: '600',
marginBottom: 10,
},
});
π― Final Thoughts
This implementation provides a smooth and visually appealing pull-to-refresh experience in React Native. With Reanimated and Lottie, you can create highly interactive and engaging UI components. Try this out in your projects and elevate your appβs user experience! ππ₯
Let me know in the comments if you have any questions or suggestions! π¬π
Top comments (2)
@amitkumar13 I noticed react-native-gesture-handler is included in the dependencies, but I don't see it used in the code. Could you clarify where itβs needed? Thanks
I guess there is no need. Let me update it.