Replies: 2 comments 3 replies
-
这个比较简单,用 NativeViewGestureHandler 或者新API Gesture.Native 包裹 FlatList或者ScrollView 。再在外面套一层 PanGesture。让PanGesture 和 NativeViewGesture 同时响应 ( simultanuesHandler )。用 reanimated 导出的 useAnimatedScrollHandler 同步列表的滚动偏移值。在 Pan Handler里根据当前列表滚动偏移值 和 Pan的 translationY 对刷新指示器组件做出相应的位移变换即可。为了避免列表item的点击事件无法响应,最好是使用新的Gesture API,开启 manualActivation ,手动判断是否需要激活 Pan 手势。 |
Beta Was this translation helpful? Give feedback.
1 reply
-
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
}
})
}
}) |
Beta Was this translation helpful? Give feedback.
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
I think it's a very common requirement that we don't want the default RefreshControl component in ScrollView or FlatList, but a more customizable pull-to-refresh component.
There are two ways to do this: use native code or use gesture handler.
I think it would be perfect if there's an example to demonstrate it officially.
Beta Was this translation helpful? Give feedback.
All reactions