react-native-refresh-list2 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +67 -0
  3. package/lib/module/Loading/Loading.js +19 -0
  4. package/lib/module/Loading/Loading.js.map +1 -0
  5. package/lib/module/Loading/index.js +5 -0
  6. package/lib/module/Loading/index.js.map +1 -0
  7. package/lib/module/RefreshControl/BottomContainer.js +88 -0
  8. package/lib/module/RefreshControl/BottomContainer.js.map +1 -0
  9. package/lib/module/RefreshControl/NormalControl.js +94 -0
  10. package/lib/module/RefreshControl/NormalControl.js.map +1 -0
  11. package/lib/module/RefreshControl/RefreshContainer.js +53 -0
  12. package/lib/module/RefreshControl/RefreshContainer.js.map +1 -0
  13. package/lib/module/RefreshControl/RefreshContext.js +32 -0
  14. package/lib/module/RefreshControl/RefreshContext.js.map +1 -0
  15. package/lib/module/RefreshControl/RefreshFlatList.js +291 -0
  16. package/lib/module/RefreshControl/RefreshFlatList.js.map +1 -0
  17. package/lib/module/RefreshControl/RefreshScrollView.js +279 -0
  18. package/lib/module/RefreshControl/RefreshScrollView.js.map +1 -0
  19. package/lib/module/RefreshControl/index.js +7 -0
  20. package/lib/module/RefreshControl/index.js.map +1 -0
  21. package/lib/module/RefreshControl/type.js +39 -0
  22. package/lib/module/RefreshControl/type.js.map +1 -0
  23. package/lib/module/icon/CommentIcon.js +32 -0
  24. package/lib/module/icon/CommentIcon.js.map +1 -0
  25. package/lib/module/icon/Icon.js +25 -0
  26. package/lib/module/icon/Icon.js.map +1 -0
  27. package/lib/module/icon/MoreIcon.js +40 -0
  28. package/lib/module/icon/MoreIcon.js.map +1 -0
  29. package/lib/module/icon/Praise.js +23 -0
  30. package/lib/module/icon/Praise.js.map +1 -0
  31. package/lib/module/icon/SearchIcon.js +35 -0
  32. package/lib/module/icon/SearchIcon.js.map +1 -0
  33. package/lib/module/icon/index.js +5 -0
  34. package/lib/module/icon/index.js.map +1 -0
  35. package/lib/module/icon/library.js +20 -0
  36. package/lib/module/icon/library.js.map +1 -0
  37. package/lib/module/index.js +4 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/typescript/package.json +1 -0
  41. package/lib/typescript/src/Loading/Loading.d.ts +9 -0
  42. package/lib/typescript/src/Loading/Loading.d.ts.map +1 -0
  43. package/lib/typescript/src/Loading/index.d.ts +3 -0
  44. package/lib/typescript/src/Loading/index.d.ts.map +1 -0
  45. package/lib/typescript/src/RefreshControl/BottomContainer.d.ts +17 -0
  46. package/lib/typescript/src/RefreshControl/BottomContainer.d.ts.map +1 -0
  47. package/lib/typescript/src/RefreshControl/NormalControl.d.ts +20 -0
  48. package/lib/typescript/src/RefreshControl/NormalControl.d.ts.map +1 -0
  49. package/lib/typescript/src/RefreshControl/RefreshContainer.d.ts +17 -0
  50. package/lib/typescript/src/RefreshControl/RefreshContainer.d.ts.map +1 -0
  51. package/lib/typescript/src/RefreshControl/RefreshContext.d.ts +9 -0
  52. package/lib/typescript/src/RefreshControl/RefreshContext.d.ts.map +1 -0
  53. package/lib/typescript/src/RefreshControl/RefreshFlatList.d.ts +20 -0
  54. package/lib/typescript/src/RefreshControl/RefreshFlatList.d.ts.map +1 -0
  55. package/lib/typescript/src/RefreshControl/RefreshScrollView.d.ts +20 -0
  56. package/lib/typescript/src/RefreshControl/RefreshScrollView.d.ts.map +1 -0
  57. package/lib/typescript/src/RefreshControl/index.d.ts +5 -0
  58. package/lib/typescript/src/RefreshControl/index.d.ts.map +1 -0
  59. package/lib/typescript/src/RefreshControl/type.d.ts +70 -0
  60. package/lib/typescript/src/RefreshControl/type.d.ts.map +1 -0
  61. package/lib/typescript/src/icon/CommentIcon.d.ts +3 -0
  62. package/lib/typescript/src/icon/CommentIcon.d.ts.map +1 -0
  63. package/lib/typescript/src/icon/Icon.d.ts +10 -0
  64. package/lib/typescript/src/icon/Icon.d.ts.map +1 -0
  65. package/lib/typescript/src/icon/MoreIcon.d.ts +3 -0
  66. package/lib/typescript/src/icon/MoreIcon.d.ts.map +1 -0
  67. package/lib/typescript/src/icon/Praise.d.ts +9 -0
  68. package/lib/typescript/src/icon/Praise.d.ts.map +1 -0
  69. package/lib/typescript/src/icon/SearchIcon.d.ts +9 -0
  70. package/lib/typescript/src/icon/SearchIcon.d.ts.map +1 -0
  71. package/lib/typescript/src/icon/index.d.ts +3 -0
  72. package/lib/typescript/src/icon/index.d.ts.map +1 -0
  73. package/lib/typescript/src/icon/library.d.ts +17 -0
  74. package/lib/typescript/src/icon/library.d.ts.map +1 -0
  75. package/lib/typescript/src/index.d.ts +2 -0
  76. package/lib/typescript/src/index.d.ts.map +1 -0
  77. package/package.json +168 -0
  78. package/src/Loading/Loading.tsx +15 -0
  79. package/src/Loading/index.tsx +5 -0
  80. package/src/RefreshControl/BottomContainer.tsx +112 -0
  81. package/src/RefreshControl/NormalControl.tsx +118 -0
  82. package/src/RefreshControl/RefreshContainer.tsx +74 -0
  83. package/src/RefreshControl/RefreshContext.tsx +30 -0
  84. package/src/RefreshControl/RefreshFlatList.tsx +372 -0
  85. package/src/RefreshControl/RefreshScrollView.tsx +359 -0
  86. package/src/RefreshControl/index.tsx +5 -0
  87. package/src/RefreshControl/type.ts +74 -0
  88. package/src/icon/CommentIcon.tsx +29 -0
  89. package/src/icon/Icon.tsx +26 -0
  90. package/src/icon/MoreIcon.tsx +38 -0
  91. package/src/icon/Praise.tsx +21 -0
  92. package/src/icon/SearchIcon.tsx +36 -0
  93. package/src/icon/index.tsx +3 -0
  94. package/src/icon/library.ts +30 -0
  95. package/src/index.tsx +1 -0
@@ -0,0 +1,372 @@
1
+ // RefreshFlatList.tsx
2
+
3
+ import React, { useCallback, useEffect, useRef } from 'react';
4
+ import { Dimensions, type FlatListProps, Platform } from 'react-native';
5
+ import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
6
+ import Animated, {
7
+ runOnJS,
8
+ interpolate,
9
+ useAnimatedScrollHandler,
10
+ withTiming,
11
+ useAnimatedProps,
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ useDerivedValue,
15
+ withSequence,
16
+ withDelay,
17
+ Easing,
18
+ useAnimatedReaction, type SharedValue,
19
+ } from 'react-native-reanimated';
20
+ import { RefreshContainerContext, RefreshStatus } from './type';
21
+ import RefreshContainer from './RefreshContainer';
22
+ // 假设您不需要单独的 BottomContainer 组件,而是将加载更多内容放在 ListFooterComponent 中
23
+
24
+ // ------------------- 常量定义 -------------------
25
+ const { height } = Dimensions.get('window');
26
+
27
+ const MAX_SCROLL_VELOCITY_Y = 20;
28
+ const MIN_SCROLL_VELOCITY_Y = 0.5;
29
+ const DEFAULT_TRIGGLE_HEIGHT = 100;
30
+ const RESET_TIMING_EASING = Easing.bezier(0.33, 1, 0.68, 1);
31
+ const MAX_BOUNCE_DISTANCE = 100
32
+
33
+ // ------------------- Props 定义 -------------------
34
+ interface RefreshFlatListProps<ItemT> extends FlatListProps<ItemT> {
35
+ refreshing: boolean;
36
+ refreshComponent: () => React.ReactNode;
37
+ onRefresh: () => void;
38
+ onScroll?: (event: any) => void;
39
+ handleOnLoadMore?: () => void;
40
+ triggleHeight?: number;
41
+ canOffset?: boolean;
42
+ bounces?: boolean;
43
+ offsetY?: number
44
+ scrollParentRef?: any,
45
+ transitionY?: SharedValue<number>;
46
+ scrollViewParentTransitionY?: SharedValue<number>;
47
+ }
48
+
49
+ const RefreshFlatList = <ItemT,>(props: RefreshFlatListProps<ItemT>) => {
50
+ const {
51
+ refreshing,
52
+ onRefresh,
53
+ refreshComponent,
54
+ transitionY,
55
+ triggleHeight = DEFAULT_TRIGGLE_HEIGHT,
56
+ canOffset = true,
57
+ bounces = true,
58
+ onScroll: onScrollHandler,
59
+ data,
60
+ renderItem,
61
+ keyExtractor,
62
+ offsetY = 0,
63
+ scrollParentRef,
64
+ scrollViewParentTransitionY,
65
+ ...restProps
66
+ } = props;
67
+
68
+ // ------------------- Shared Values 和 Refs -------------------
69
+ const panRef = useRef<any>(null);
70
+ const internalScrollRef = useRef<any>(null);
71
+ const scrollRef = scrollParentRef || internalScrollRef;
72
+
73
+ const internalScrollViewParentTransitionY = useSharedValue(0);
74
+
75
+ const scrollViewTransitionY = scrollViewParentTransitionY || internalScrollViewParentTransitionY;
76
+ const offset = useSharedValue(0);
77
+ const scrollBounse = useSharedValue(false);
78
+ const refreshStatus = useSharedValue<RefreshStatus>(RefreshStatus.Idle);
79
+ const scrollViewTotalHeight = useSharedValue(0);
80
+ const scrollBeginTop = useSharedValue(0)
81
+ const internalRefreshTransitionY = useSharedValue(0);
82
+ const refreshTransitionY = transitionY ?? internalRefreshTransitionY
83
+
84
+ // ------------------- Derived Values -------------------
85
+ const direction = useDerivedValue(() => {
86
+ return refreshTransitionY.value > 0 ? 1 : -1;
87
+ }, [refreshTransitionY]);
88
+
89
+ const canRefresh = useDerivedValue(() => {
90
+ const marginTop = scrollViewTransitionY.value;
91
+ // 核心:判断是否触顶 (下拉) 或触底 (上拉/加载更多)
92
+ return marginTop < 1
93
+ });
94
+
95
+ useAnimatedReaction(
96
+ () => refreshStatus.value,
97
+ () => {
98
+ }
99
+ );
100
+
101
+ // ------------------- Refreshing Side Effect -------------------
102
+ useEffect(() => {
103
+ console.log(refreshing)
104
+ if (refreshing) {
105
+ console.log(direction.value)
106
+ if (direction.value === 1) {
107
+ refreshStatus.value = RefreshStatus.Holding;
108
+ // 刷新中保持指示器在触发高度
109
+ refreshTransitionY.value = withTiming(triggleHeight * direction.value);
110
+ } else if (refreshStatus.value === RefreshStatus.Idle) {
111
+ // scrollRef.current.scrollToOffset({offset: 0, animated: false});
112
+ refreshStatus.value = RefreshStatus.Holding;
113
+ refreshTransitionY.value = withTiming(triggleHeight);
114
+ }
115
+ } else if (refreshStatus.value !== RefreshStatus.Idle) {
116
+ if (direction.value === 1) {
117
+ // 下拉刷新完成,收起指示器
118
+ refreshStatus.value = RefreshStatus.Done;
119
+ refreshTransitionY.value = withDelay(
120
+ 500,
121
+ withTiming(
122
+ 0,
123
+ {
124
+ easing: RESET_TIMING_EASING,
125
+ },
126
+ () => {
127
+ refreshStatus.value = RefreshStatus.Idle;
128
+ }
129
+ )
130
+ );
131
+ }
132
+ }
133
+ }, [refreshing, direction.value, refreshStatus, refreshTransitionY, triggleHeight]);
134
+
135
+ const handleOnRefresh = useCallback(() => {
136
+ if (refreshing) return;
137
+ onRefresh && onRefresh();
138
+ }, [refreshing, onRefresh]);
139
+
140
+ // ------------------- Scroll Handler -------------------
141
+ const onScroll = useAnimatedScrollHandler<{
142
+ scrollBeginTime: number;
143
+ scrollBeginY: number;
144
+ }>({
145
+ onBeginDrag: (event, context) => {
146
+ context.scrollBeginTime = new Date().valueOf();
147
+ context.scrollBeginY = event.contentOffset.y;
148
+ },
149
+ onScroll: (event, context) => {
150
+ onScrollHandler && runOnJS(onScrollHandler)({
151
+ nativeEvent: event,
152
+ });
153
+ const { scrollBeginY, scrollBeginTime } = context;
154
+ scrollViewTransitionY.value = event.contentOffset.y;
155
+ // 滚动边界反弹动画逻辑
156
+ const marginTop = scrollViewTransitionY.value;
157
+
158
+ if (marginTop === 0 && !scrollBounse.value) {
159
+
160
+ const bounceDirection = marginTop === 0 ? 1 : -1;
161
+ const endTime = new Date().valueOf();
162
+ const velocityY = Math.min(
163
+ Math.abs(
164
+ (scrollViewTransitionY.value - scrollBeginY) /
165
+ (endTime - scrollBeginTime)
166
+ ),
167
+ MAX_SCROLL_VELOCITY_Y
168
+ );
169
+ if (!bounces || velocityY < MIN_SCROLL_VELOCITY_Y) return;
170
+
171
+ const ratio = (Math.PI / 2 / MAX_SCROLL_VELOCITY_Y) * velocityY;
172
+ const bounceDistance = (height / 4) * Math.sin(ratio);
173
+ const duration = 100 + 100 * Math.sin(ratio * 2);
174
+
175
+ scrollBounse.value = true;
176
+ refreshTransitionY.value = withSequence(
177
+ withTiming(bounceDistance * bounceDirection, { duration }),
178
+ withTiming(
179
+ 0,
180
+ {
181
+ duration,
182
+ easing: RESET_TIMING_EASING,
183
+ },
184
+ () => {
185
+ scrollBounse.value = false;
186
+ }
187
+ )
188
+ );
189
+ }
190
+ }
191
+ });
192
+
193
+ // @ts-ignore
194
+ const nativeRef: any = useRef();
195
+ const nativeGesture = Gesture.Native().withRef(nativeRef);
196
+
197
+
198
+ const scrollToTopJS = useCallback(() => {
199
+ // 检查 ref 是否存在
200
+ if (scrollRef.current) {
201
+ // 使用 scrollToOffset 替代 scrollToTop,因为 FlatList/ScrollView 实例上
202
+ // 没有原生 scrollToTop 方法,但有 scrollToOffset
203
+ // 或者使用你之前提到的 scrollToOffset({offset: 0, animated: false})
204
+ (scrollRef.current as any).scrollToOffset({
205
+ offset: 0,
206
+ animated: false, // 禁用动画以确保即时归位
207
+ });
208
+
209
+ // 如果你的代码中 FlatList 是经过 Animated 包装的,可能需要调用其原生方法
210
+ // (scrollRef.current as any).getNode().scrollToOffset({ offset: 0, animated: false });
211
+ }
212
+ }, [scrollRef]);
213
+
214
+ // ------------------- Pan Gesture -------------------
215
+ const panGesture = Gesture.Pan()
216
+ .withRef(panRef)
217
+ .activeOffsetY(10)
218
+ .simultaneousWithExternalGesture(nativeRef)
219
+ // 移除 activeOffsetY 或设置为极小值,让优先级完全由 requireExternalGestureToFail 决定
220
+ // 关键:与原生滚动同时进行,以便在触顶时接管
221
+ .onBegin(() => {
222
+ offset.value = refreshTransitionY.value;
223
+ scrollBeginTop.value = scrollViewTransitionY.value;
224
+ })
225
+ .onUpdate(({ translationY }) => {
226
+ const shouldAllowModify = canRefresh.value || (refreshStatus.value !== RefreshStatus.Idle && (translationY - scrollBeginTop.value) >= 0);
227
+ if (!shouldAllowModify) {
228
+ refreshTransitionY.value = 0;
229
+ // 如果不在触顶区域且未处于刷新状态,则不修改位移
230
+ return;
231
+ }
232
+
233
+ if (Platform.OS !== 'ios') runOnJS(scrollToTopJS)()
234
+
235
+ let nextY =
236
+ offset.value +
237
+ interpolate(
238
+ (translationY - scrollBeginTop.value),
239
+ [0, height],
240
+ [0, height / 2],
241
+ // 确保超过 height 时不会失控,但这里通常不需要
242
+ );
243
+
244
+ refreshTransitionY.value = Math.min(nextY, MAX_BOUNCE_DISTANCE);
245
+
246
+ if (!refreshing) {
247
+ if (Math.abs(refreshTransitionY.value) >= triggleHeight) {
248
+ refreshStatus.value = RefreshStatus.Reached;
249
+ } else {
250
+ refreshStatus.value = RefreshStatus.Pulling;
251
+ }
252
+ }
253
+ })
254
+ .onEnd(() => {
255
+ if (refreshing) {
256
+ if (refreshTransitionY.value >= triggleHeight * direction.value) {
257
+ refreshTransitionY.value = withTiming(
258
+ triggleHeight * direction.value
259
+ );
260
+ } else {
261
+ ``
262
+ refreshTransitionY.value = withTiming(0);
263
+ }
264
+ return;
265
+ }
266
+ if (Math.abs(refreshTransitionY.value) >= triggleHeight) {
267
+ if (refreshTransitionY.value > 0) {
268
+ runOnJS(handleOnRefresh)();
269
+ }
270
+ } else {
271
+ refreshTransitionY.value = withTiming(
272
+ 0,
273
+ {
274
+ easing: RESET_TIMING_EASING,
275
+ },
276
+ () => {
277
+ refreshStatus.value = RefreshStatus.Idle;
278
+ }
279
+ );
280
+ }
281
+
282
+ })
283
+
284
+ // ------------------- Animated Styles and Props -------------------
285
+ const animatedStyle = useAnimatedStyle(() => {
286
+ // 🌟 核心修复:这个 style 将应用于外部 Animated.View,实现整个列表的平移
287
+ return {
288
+ transform: [
289
+ {
290
+ translateY: canOffset ? refreshTransitionY.value : 0,
291
+ },
292
+ ],
293
+ };
294
+ });
295
+
296
+ const animatedScrollEnabled = useDerivedValue(() => {
297
+ if (Platform.OS === 'ios') return true
298
+ return refreshTransitionY.value <= 0;
299
+ }, [refreshTransitionY]);
300
+
301
+ const animatedProps = useAnimatedProps(() => {
302
+ const top = -refreshTransitionY.value;
303
+ return {
304
+ scrollEnabled: animatedScrollEnabled.value,
305
+ scrollIndicatorInsets: {
306
+ top: top - 1,
307
+ left: 0,
308
+ bottom: 0,
309
+ right: 0,
310
+ }
311
+ };
312
+ });
313
+
314
+ const derivedTransitionY = useDerivedValue(() => {
315
+ return refreshTransitionY.value + offsetY;
316
+ }, [offsetY]); // 依赖 offsetY,如果 offsetY 变化,重新计算
317
+
318
+ // ------------------- JSX 渲染 -------------------
319
+ return (
320
+ <GestureHandlerRootView style={{
321
+ flex: 1
322
+ }}>
323
+ <RefreshContainerContext.Provider
324
+ value={{
325
+ transitionY: refreshTransitionY,
326
+ scrollBounse,
327
+ triggleHeight,
328
+ refreshing,
329
+ refreshStatus,
330
+ direction,
331
+ canRefresh,
332
+ }}
333
+ >
334
+ <GestureDetector gesture={panGesture}>
335
+ {/* 🌟 核心修复:使用 Animated.View 包裹 FlatList 并应用 animatedStyle */}
336
+ <GestureDetector gesture={nativeGesture}>
337
+ {/* @ts-ignore */}
338
+ <Animated.FlatList
339
+ ref={scrollRef}
340
+ bounces={false}
341
+ overScrollMode={'never'}
342
+ scrollEventThrottle={16}
343
+ onScroll={onScroll}
344
+ animatedProps={animatedProps}
345
+ // 移除 style={animatedStyle}
346
+ onContentSizeChange={(_: number, h: number) => {
347
+ scrollViewTotalHeight.value = h;
348
+ }}
349
+ // FlatList 核心 Props
350
+ data={data}
351
+ renderItem={renderItem}
352
+ keyExtractor={keyExtractor}
353
+ {...restProps}
354
+ style={[{ flex: 1 }, animatedStyle, restProps.style]} // 合并样式
355
+ />
356
+ </GestureDetector>
357
+ </GestureDetector>
358
+ {/* RefreshContainer 必须在列表之上,使用绝对定位 */}
359
+ <RefreshContainer
360
+ offsetTop={offsetY}
361
+ refreshStatus={refreshStatus}
362
+ transitionY={derivedTransitionY}
363
+ triggleHeight={triggleHeight + offsetY}
364
+ >
365
+ {refreshComponent && refreshComponent()}
366
+ </RefreshContainer>
367
+ </RefreshContainerContext.Provider>
368
+ </GestureHandlerRootView>
369
+ );
370
+ }
371
+
372
+ export default RefreshFlatList;