React Native Refresh Control
npm install @byron-react-native/refresh-control$ yarn add @byron-react-native/refresh-control
javascript
import React, {useRef, useState, useCallback} from 'react';
import {
View,
Text,
Platform,
Animated,
LayoutChangeEvent,
ViewStyle,
} from 'react-native';
import {
FlatList,
FlatListProps,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import {
RefreshControl,
RNRefreshControl,
RNRefreshHeader,
RefreshControlProps,
} from '@byron-react-native/refresh-control';
import {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import {forwardRef, useImperativeHandle} from 'react';
import Spinner from 'react-native-spinkit';const iOS = Platform.OS === 'ios';
export interface RefreshFlatListProps extends FlatListProps {
onRefresh?: () => Promise;
onEndReached?: () => Promise;
}
export enum FooterStatus {
Idle, // 初始状态,无刷新的情况
CanLoadMore, // 可以加载更多,表示列表还有数据可以继续加载
Refreshing, // 正在刷新中
NoMoreData, // 没有更多数据了
Failure, // 刷新失败
}
export interface FooterRef {
changeStatus: (status: FooterStatus) => void;
}
function RefreshFlatList(props: RefreshFlatListProps) {
const headerTracker = useRef(false);
const footerRef = useRef(null);
const footerTracker = useRef>({});
const footerInProgress = useRef(false);
const onHeader = async () => {
if (!props.onRefresh) {
return;
}
headerTracker.current = true;
await props.onRefresh();
headerTracker.current = false;
footerTracker.current = {};
footerRef.current?.changeStatus(FooterStatus.Idle);
};
const onFooter = async () => {
if (!props.onEndReached) {
return;
}
if (headerTracker.current) {
return;
}
if (footerInProgress.current) {
return;
}
const length = props.data?.length || 0;
if (footerTracker.current[length]) {
footerRef.current?.changeStatus(FooterStatus.NoMoreData);
return;
}
footerInProgress.current = true;
footerRef.current?.changeStatus(FooterStatus.Refreshing);
await props.onEndReached();
footerTracker.current[length] = true;
footerInProgress.current = false;
footerRef.current?.changeStatus(FooterStatus.CanLoadMore);
};
const handleScroll = (event: NativeSyntheticEvent) => {
props.onScroll?.(event);
const offset = event.nativeEvent.contentOffset.y;
const visibleLength = event.nativeEvent.layoutMeasurement.height;
const contentLength = event.nativeEvent.contentSize.height;
const onEndReachedThreshold = props.onEndReachedThreshold || 10;
const length = contentLength - visibleLength - offset;
const isScrollAtEnd = length < onEndReachedThreshold;
if (isScrollAtEnd) onFooter();
};
const refreshControl = props.onRefresh ? (
) : (
//
void 0
);
return (
{...props}
onEndReached={null}
onScroll={handleScroll}
ListFooterComponent={() => (
)}
refreshControl={refreshControl}
refreshing={false}
/>
);
}
export const CustomRefreshControl = forwardRef(
({onRefresh, style, ...props}, ref) => {
const [height, setHeight] = useState(100);
const [title, setTitle] = useState('下拉可以刷新');
const [lastTime, setLastTime] = useState(fetchNowTime());
const animatedValue = useRef(new Animated.Value(0));
const [refreshing, setRefreshing] = useState(props.refreshing ?? false);
useImperativeHandle(ref, () => ({
startRefresh: () => {
setRefreshing(true);
},
stopRefresh: () => {
setRefreshing(false);
},
}));
const onPullingRefresh = () => {
Animated.timing(animatedValue.current, {
toValue: -180,
duration: 200,
useNativeDriver: true,
}).start(() => {
setTitle('释放立即刷新');
});
};
const onRefreshing = () => {
setTitle('正在刷新...');
setRefreshing(true);
if (onRefresh) {
onRefresh().then(() => {
setRefreshing(false);
setLastTime(fetchNowTime());
});
} else {
setTimeout(() => {
setRefreshing(false);
setLastTime(fetchNowTime());
}, 200);
}
};
const onIdleRefresh = () => {
Animated.timing(animatedValue.current, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => {
setTitle('下拉可以刷新');
setRefreshing(false);
});
};
const onRefreshFinished = () => {};
const onChangeState = useCallback((state: number) => {
props.onChangeState && props.onChangeState(state);
switch (state) {
case 1: // 可以下拉
onIdleRefresh();
break;
case 2: // 正在下拉
onPullingRefresh();
break;
case 3: // 正在刷新
onRefreshing();
break;
case 4: // 刷新完成
onRefreshFinished();
break;
default:
}
}, []);
const rotate = animatedValue.current.interpolate({
inputRange: [0, 180],
outputRange: ['0deg', '180deg'],
});
const onLayout = (event: LayoutChangeEvent) => {
const layout = event.nativeEvent.layout;
if (layout.height !== height) {
setHeight(Math.ceil(layout.height));
}
};
const onChangeOffset = (offset: number) => {
console.log('--CustomRefreshControl--onChangeOffset---', offset);
};
return (
refreshing={refreshing}
onChangeState={onChangeState}
onChangeOffset={onChangeOffset}
style={[styles.control, style, iOS ? {marginTop: -height} : {}]}
height={height}>
{refreshing ? (
) : (
style={[styles.header_left, {transform: [{rotate}]}]}
source={require('./assets/arrow.png')}
/>
)}
{title}
{
上次更新:${lastTime}}
{/ {props.children} 不能删除或注释,会导致 Android 无法设置 RefreshContent /}
{props.children}
);
},
);const fetchNowTime = () => {
const date = new Date();
const M = date.getMonth() + 1;
const D = date.getDate();
const h = date.getHours();
const m = date.getMinutes();
const MM = M < 10 ? '0' + M : M;
const DD = D < 10 ? '0' + D : D;
const hh = h < 10 ? '0' + h : h;
const mm = m < 10 ? '0' + m : m;
return
${MM}-${DD} ${hh}:${mm};
};const FooterComponent = forwardRef(
(props, ref) => {
const [status, setStatus] = useState(FooterStatus.Idle);
useImperativeHandle(ref, () => ({
changeStatus: (val: FooterStatus) => {
setStatus(val);
},
}));
const renderTemplate = () => {
let temp = <>>;
switch (status) {
case FooterStatus.CanLoadMore:
temp = (
{props.inverted ? '下拉加载更多' : '上拉加载更多'}
);
break;
case FooterStatus.Refreshing:
temp = (
{'努力加载中...'}
);
break;
case FooterStatus.NoMoreData:
temp = (
{'没有更多数据了'}
);
break;
case FooterStatus.Failure:
temp = (
{'加载失败'}
);
break;
}
return temp;
};
return renderTemplate();
},
);
const styles = StyleSheet.create({
control: Platform.select({
ios: {
justifyContent: 'flex-end',
},
android: {
flex: 1,
overflow: 'hidden',
},
default: {},
}),
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 20,
},
header_left: {
width: 32,
height: 32,
tintColor: 'gray',
},
header_right: {
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 15,
},
header_text: {
color: 'gray',
fontSize: 12,
},
indicator: {
width: '100%',
marginVertical: 5,
alignItems: 'center',
justifyContent: 'center',
},
footer: {
justifyContent: 'center',
alignItems: 'center',
marginVertical: 20,
},
text: {
color: '#AC9FB0',
fontSize: 14,
marginTop: 5,
},
});
export default RefreshFlatList;
``