通过 react-native-gesture-handler + react-native-reanimated 实现完全自定义的下拉刷新功能。下面是可以直接复制使用的demo代码。下拉刷新行为与安卓原生下拉刷新行为一致,若需要实现iOS端下拉刷新需要禁用 bounces 、通过正值的Y轴偏移动画来添加上 overdrag 效果。
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Text, View, Pressable, ActivityIndicator, FlatList, StyleSheet, SafeAreaView, Platform, Alert } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming, runOnJS, useAnimatedScrollHandler, interpolate } from 'react-native-reanimated';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
const INDICATOR_HEIGHT = 40;
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const PullRefreshExample = (props) => {
const names = [' 琼恩·雪诺', ' 丹妮莉丝·坦格利安', ' 艾德·史塔克', ' 凯特琳·徒利·史塔克', ' 罗柏·史塔克', ' 珊莎·史塔克', ' 艾莉亚·史塔克', ' 布兰·史塔克', ' 瑞肯·史塔克',' 劳勃·拜拉席恩',' 史坦尼斯·拜拉席恩',' 蓝礼·拜拉席恩',' 乔佛里·拜拉席恩',' 弥赛拉·拜拉席恩',' 托曼·拜拉席恩'];
const [containerLayout, setContainerLayout] = useState({ width: 0, height: 0, x: 0, y: 0 })
const fetchData = () => {
//模拟执行其他事件,2秒后结束刷新
setTimeout(() => {
setRefreshing(false);
console.log('Async Task Finished');
}, 2000);
}
const [isRefreshing,setRefreshing] = useState(false);
const onRefresh = () => {
setRefreshing(true);
console.log('refreshing now');
fetchData();
}
const scrollOffset = useSharedValue(0);
const panOffset = useSharedValue(0);
const indicatorPanEdges = useMemo(() => {
let bh = containerLayout.y + INDICATOR_HEIGHT;
return [0, bh + 20, bh + 100, Number.MAX_SAFE_INTEGER];
}, [containerLayout]);
const inidcatorAnimatedStyle = useAnimatedStyle(() => {
let bh = containerLayout.y + INDICATOR_HEIGHT;
let dy = panOffset.value - scrollOffset.value;
if(scrollOffset.value>0) dy = 0;
let polatedDy = interpolate(dy,indicatorPanEdges,
[0, bh + 20, bh + 36, bh + 36]);
return {
opacity: 1.0,
transform: [
{ translateY: polatedDy },
]
}
}, [indicatorPanEdges])
const panRef = useRef();
const nativeRef = useRef();
const onContainerLayout = React.useCallback((event) => {
setContainerLayout(event.nativeEvent.layout);
}, [])
const touchStartPosition = useSharedValue({ x: 0, y: 0 })
const isOnRefresh = useSharedValue(false);
useEffect(() => {
if(isRefreshing === false){
/**
* 此时可能用户滑动事件手动取消了刷新 (isOnRefresh 为 false)。取消了刷新则不再更新 panOffset
*/
if(isOnRefresh.value){
panOffset.value = withTiming(0)
}
}
},[isRefreshing])
const panGesture = Gesture.Pan()
.manualActivation(true)
.minPointers(1)
.onTouchesDown((event, stateManager) => {
touchStartPosition.value = { x: event.allTouches[0].x, y: event.allTouches[0].y }
})
.onTouchesMove((event, stateManager) => {
let dx = event.allTouches[0].x - touchStartPosition.value.x;
let dy = event.allTouches[0].y - touchStartPosition.value.y;
if(Math.abs(dy) > 2 && Math.abs(dx) < 5){
stateManager.activate();
}
})
.onUpdate(event => {
panOffset.value = event.translationY;
})
.onEnd(event => {
let ty = event.translationY;
if(scrollOffset.value > 0) {
panOffset.value = 0;
return;
}
if(ty >= indicatorPanEdges[1]){
//位移值达到触发刷新的指定位置
isOnRefresh.value = true;
panOffset.value = withTiming(indicatorPanEdges[2],undefined,(finished) => {
runOnJS(onRefresh)();
})
}else{
//复位
panOffset.value = withTiming(0)
if(isOnRefresh.value) {
isOnRefresh.value = false;
}
runOnJS(setRefreshing)(false);
}
})
.simultaneousWithExternalGesture(nativeRef)
.withRef(panRef);
const nativeGesture = Gesture.Native()
.simultaneousWithExternalGesture(panRef)
.withRef(nativeRef)
const scrollhandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollOffset.value = event.contentOffset.y;
}
})
const renderItem = ({ item, index }) => {
return (
<Pressable onPress={() => {Alert.alert('Greet Message',' Hi , this is '+item)}} style={styles.item}>
<Text style={styles.item_label}>{item}</Text>
</Pressable>
)
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Animated.View style={[styles.indicator, inidcatorAnimatedStyle]}>
<Text style={{ fontSize: 17, color: '#666' }}>自定义刷新</Text>
<ActivityIndicator size={'small'} style={{ marginStart: 7 }} />
</Animated.View>
<GestureDetector gesture={panGesture}>
<SafeAreaView style={{ flex: 1, position: 'relative' }}>
<View style={{ flex: 1 }} onLayout={onContainerLayout}>
<GestureDetector gesture={nativeGesture}>
<AnimatedFlatList
data={names}
keyExtractor={(item, index) => item}
renderItem={renderItem}
onScroll={scrollhandler}
/>
</GestureDetector>
</View>
</SafeAreaView>
</GestureDetector>
</GestureHandlerRootView>
);
}
export default PullRefreshExample;
const styles = StyleSheet.create({
item: {
paddingVertical: 20,
borderBottomWidth: 1,
borderBottomColor: '#f3f3f5'
},
item_label: {
fontSize: 20,
color: '#222'
},
indicator: {
position: 'absolute',
zIndex: 99,
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
top: -INDICATOR_HEIGHT,
backgroundColor: '#FFF',
height: INDICATOR_HEIGHT,
paddingHorizontal: 18,
borderRadius: 18,
...Platform.select({
ios: {
shadowColor: '#000000',
shadowOpacity: 0.15,
shadowRadius: 8,
shadowOffset: { width: 3, height: 3 }
},
android: {
elevation: 12
}
})
}
});