@onekeyfe/react-native-pager-view 3.0.158 → 3.0.159

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 (42) hide show
  1. package/README.md +57 -4
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +379 -31
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt +15 -0
  4. package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
  5. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +41 -0
  6. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
  8. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
  9. package/android/src/main/jni/CMakeLists.txt +19 -0
  10. package/android/src/main/jni/pagerview.h +11 -0
  11. package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -0
  12. package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt +26 -0
  13. package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
  14. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +75 -0
  15. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
  16. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
  17. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
  18. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
  19. package/docs/SPEC.md +451 -0
  20. package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
  21. package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
  22. package/ios/RNCCollapsiblePagerViewComponentView.mm +420 -2
  23. package/ios/RNCNativeScrollerComponentView.h +17 -0
  24. package/ios/RNCNativeScrollerComponentView.mm +479 -0
  25. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
  26. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +788 -0
  27. package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
  28. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +214 -0
  29. package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.mm +23 -0
  30. package/ios/tests/RNCNativeScrollerTests.mm +252 -0
  31. package/lib/module/CollapsiblePagerView.js +10 -1
  32. package/lib/module/NativeScroller.js +171 -11
  33. package/lib/module/NativeScrollerNativeComponent.ts +101 -0
  34. package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
  35. package/lib/typescript/src/NativeScroller.d.ts +15 -7
  36. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
  37. package/package.json +8 -4
  38. package/react-native-pager-view.podspec +2 -1
  39. package/react-native.config.js +15 -0
  40. package/src/CollapsiblePagerView.tsx +14 -4
  41. package/src/NativeScroller.tsx +291 -14
  42. package/src/NativeScrollerNativeComponent.ts +101 -0
@@ -1,34 +1,311 @@
1
1
  import * as React from "react";
2
- import { ScrollView, type ScrollViewProps } from "react-native";
2
+ import {
3
+ findNodeHandle,
4
+ Keyboard,
5
+ Platform,
6
+ StyleSheet,
7
+ TextInput,
8
+ View,
9
+ type GestureResponderEvent,
10
+ type NativeMethods,
11
+ type RefreshControlProps,
12
+ type ScrollView,
13
+ type ScrollViewProps,
14
+ type ViewProps,
15
+ } from "react-native";
16
+
17
+ import NativeScrollerHost, { Commands } from "./NativeScrollerNativeComponent";
3
18
 
4
19
  export const NATIVE_SCROLLER_NATIVE_ID_PREFIX =
5
20
  "rnc-collapsible-pager-native-scroller";
6
21
 
7
- export interface NativeScrollerProps extends ScrollViewProps {
8
- /** Stable identity used when a page replaces or remounts its primary scroller. */
22
+ /** Supported vertical subset; horizontal, snapping, zoom and sticky rows are not supported. */
23
+ export interface NativeScrollerProps
24
+ extends ViewProps,
25
+ Pick<
26
+ ScrollViewProps,
27
+ | "contentContainerStyle"
28
+ | "onContentSizeChange"
29
+ | "scrollEnabled"
30
+ | "nestedScrollEnabled"
31
+ | "showsVerticalScrollIndicator"
32
+ | "showsHorizontalScrollIndicator"
33
+ | "bounces"
34
+ | "alwaysBounceVertical"
35
+ | "contentInset"
36
+ | "keyboardDismissMode"
37
+ | "keyboardShouldPersistTaps"
38
+ | "scrollEventThrottle"
39
+ | "decelerationRate"
40
+ | "onScroll"
41
+ | "onScrollBeginDrag"
42
+ | "onScrollEndDrag"
43
+ | "onMomentumScrollBegin"
44
+ | "onMomentumScrollEnd"
45
+ > {
9
46
  pagerScrollKey?: string;
47
+ /** iOS only. Android retains AndroidX's native fling physics. */
48
+ decelerationRate?: ScrollViewProps["decelerationRate"];
49
+ /** Standard RefreshControl props are mapped to the independently owned native controller. */
50
+ refreshControl?: React.ReactElement<RefreshControlProps>;
51
+ }
52
+
53
+ type HostRef = React.ElementRef<typeof NativeScrollerHost>;
54
+
55
+ export interface NativeScrollerRef
56
+ extends NativeMethods,
57
+ Pick<
58
+ React.ElementRef<typeof ScrollView>,
59
+ "scrollTo" | "scrollToEnd" | "flashScrollIndicators"
60
+ > {
61
+ getNativeScrollRef(): HostRef | null;
62
+ getScrollableNode(): number | null;
10
63
  }
11
64
 
12
- /**
13
- * Primary non-virtualized vertical scroller for a CollapsiblePagerView page.
14
- */
65
+ // The RN 0.86 JS registry distinguishes another input from an unhandled tap.
66
+ // TextInput.State's public facade does not export this query.
67
+ const textInputState =
68
+ require("react-native/Libraries/Components/TextInput/TextInputState")
69
+ .default as {
70
+ isTextInput(target: GestureResponderEvent["target"]): boolean;
71
+ };
72
+
73
+ const styles = StyleSheet.create({
74
+ viewport: { flexGrow: 1, flexShrink: 1, overflow: "hidden" },
75
+ content: { position: "absolute", top: 0, left: 0, right: 0, flexShrink: 0 },
76
+ });
77
+
15
78
  export const NativeScroller = React.forwardRef<
16
- React.ElementRef<typeof ScrollView>,
79
+ NativeScrollerRef,
17
80
  NativeScrollerProps
18
81
  >(function NativeScroller(
19
- { nativeID, nestedScrollEnabled = true, pagerScrollKey, ...props },
20
- ref
82
+ {
83
+ children,
84
+ nativeID,
85
+ pagerScrollKey,
86
+ style,
87
+ contentContainerStyle,
88
+ onLayout,
89
+ onContentSizeChange,
90
+ refreshControl,
91
+ nestedScrollEnabled = true,
92
+ contentInset,
93
+ decelerationRate = "normal",
94
+ keyboardShouldPersistTaps = "never",
95
+ onScroll,
96
+ onScrollBeginDrag,
97
+ onMomentumScrollBegin,
98
+ onMomentumScrollEnd,
99
+ onStartShouldSetResponder,
100
+ onStartShouldSetResponderCapture,
101
+ onResponderGrant,
102
+ onResponderRelease,
103
+ onResponderTerminationRequest,
104
+ ...props
105
+ },
106
+ forwardedRef
21
107
  ) {
108
+ const hostRef = React.useRef<HostRef>(null);
22
109
  const generatedId = React.useId();
23
- const scrollKey = pagerScrollKey ?? nativeID ?? generatedId;
110
+ const [contentViewportHeight, setContentViewportHeight] = React.useState(0);
111
+ const [contentSize, setContentSize] = React.useState({ width: 0, height: 0 });
112
+ const [refreshEpoch, requestRefreshSync] = React.useReducer(
113
+ (value: number) => value + 1,
114
+ 0
115
+ );
116
+ const contentSizeRef = React.useRef(contentSize);
117
+ const scrolledSinceGrant = React.useRef(false);
118
+ const momentum = React.useRef(false);
119
+ const grantedDuringMomentum = React.useRef(false);
120
+ const persistTaps =
121
+ keyboardShouldPersistTaps === true
122
+ ? "always"
123
+ : keyboardShouldPersistTaps === false
124
+ ? "never"
125
+ : keyboardShouldPersistTaps;
126
+ const contentStyle = StyleSheet.flatten(contentContainerStyle);
127
+ const refresh = refreshControl?.props;
128
+ const rate =
129
+ typeof decelerationRate === "number"
130
+ ? decelerationRate
131
+ : decelerationRate === "fast"
132
+ ? Platform.OS === "ios"
133
+ ? 0.99
134
+ : 0.9
135
+ : Platform.OS === "ios"
136
+ ? 0.998
137
+ : 0.985;
138
+
139
+ React.useEffect(() => {
140
+ if (hostRef.current)
141
+ Commands.setRefreshState(hostRef.current, refresh?.refreshing ?? false);
142
+ }, [refresh?.refreshing, refreshEpoch]);
143
+
144
+ React.useImperativeHandle(
145
+ forwardedRef,
146
+ () => ({
147
+ scrollTo(options, x, animated) {
148
+ const host = hostRef.current;
149
+ if (!host) return;
150
+ if (typeof options === "number") {
151
+ Commands.scrollTo(host, x ?? 0, options, animated !== false);
152
+ } else {
153
+ Commands.scrollTo(
154
+ host,
155
+ options?.x ?? 0,
156
+ options?.y ?? 0,
157
+ options?.animated !== false
158
+ );
159
+ }
160
+ },
161
+ scrollToEnd(options) {
162
+ if (hostRef.current)
163
+ Commands.scrollToEnd(hostRef.current, options?.animated !== false);
164
+ },
165
+ flashScrollIndicators() {
166
+ if (hostRef.current) Commands.flashScrollIndicators(hostRef.current);
167
+ },
168
+ getNativeScrollRef: () => hostRef.current,
169
+ getScrollableNode: () => findNodeHandle(hostRef.current) ?? null,
170
+ measure: (...args) => hostRef.current?.measure(...args),
171
+ measureInWindow: (...args) => hostRef.current?.measureInWindow(...args),
172
+ measureLayout: (...args) => hostRef.current?.measureLayout(...args),
173
+ setNativeProps: (nativeProps) =>
174
+ hostRef.current?.setNativeProps(nativeProps),
175
+ focus: () => hostRef.current?.focus(),
176
+ blur: () => hostRef.current?.blur(),
177
+ }),
178
+ []
179
+ );
180
+
181
+ const keyboardIsDismissible = () =>
182
+ Boolean(
183
+ TextInput.State.currentlyFocusedInput() &&
184
+ (Keyboard.isVisible() ||
185
+ (Platform.OS === "android" && Number(Platform.Version) < 30)) &&
186
+ Keyboard.metrics()?.height !== 0
187
+ );
24
188
 
25
189
  return (
26
- <ScrollView
190
+ <NativeScrollerHost
27
191
  {...props}
28
- ref={ref}
29
- nativeID={`${NATIVE_SCROLLER_NATIVE_ID_PREFIX}:${scrollKey}`}
192
+ ref={hostRef}
193
+ nativeID={`${NATIVE_SCROLLER_NATIVE_ID_PREFIX}:${
194
+ pagerScrollKey ?? nativeID ?? generatedId
195
+ }`}
196
+ style={[styles.viewport, style]}
30
197
  nestedScrollEnabled={nestedScrollEnabled}
31
- />
198
+ contentWidth={contentSize.width}
199
+ contentHeight={contentSize.height}
200
+ contentInsetTop={contentInset?.top ?? 0}
201
+ contentInsetRight={contentInset?.right ?? 0}
202
+ contentInsetBottom={contentInset?.bottom ?? 0}
203
+ contentInsetLeft={contentInset?.left ?? 0}
204
+ decelerationRate={rate}
205
+ keyboardShouldPersistTaps={persistTaps}
206
+ refreshEnabled={Boolean(refresh) && refresh?.enabled !== false}
207
+ refreshing={refresh?.refreshing ?? false}
208
+ refreshTintColor={refresh?.tintColor}
209
+ refreshColors={refresh?.colors}
210
+ refreshProgressBackgroundColor={refresh?.progressBackgroundColor}
211
+ refreshProgressViewOffset={refresh?.progressViewOffset ?? 0}
212
+ refreshTitle={refresh?.title}
213
+ refreshTitleColor={refresh?.titleColor}
214
+ refreshSize={refresh?.size === "large" ? "large" : "default"}
215
+ onRefresh={() => {
216
+ try {
217
+ refresh?.onRefresh?.();
218
+ } finally {
219
+ requestRefreshSync();
220
+ }
221
+ }}
222
+ onLayout={onLayout}
223
+ onContentViewportChange={(event) => {
224
+ const height = event.nativeEvent.height;
225
+ if (!Number.isFinite(height) || height < 0) return;
226
+ setContentViewportHeight((previous) =>
227
+ previous === height ? previous : height
228
+ );
229
+ }}
230
+ onScroll={(event) => {
231
+ scrolledSinceGrant.current = true;
232
+ onScroll?.(event);
233
+ }}
234
+ onScrollBeginDrag={(event) => {
235
+ scrolledSinceGrant.current = true;
236
+ onScrollBeginDrag?.(event);
237
+ }}
238
+ onMomentumScrollBegin={(event) => {
239
+ momentum.current = true;
240
+ onMomentumScrollBegin?.(event);
241
+ }}
242
+ onMomentumScrollEnd={(event) => {
243
+ momentum.current = false;
244
+ onMomentumScrollEnd?.(event);
245
+ }}
246
+ onStartShouldSetResponder={(event) =>
247
+ onStartShouldSetResponder?.(event) ??
248
+ (persistTaps === "handled" &&
249
+ keyboardIsDismissible() &&
250
+ event.target !== TextInput.State.currentlyFocusedInput())
251
+ }
252
+ onStartShouldSetResponderCapture={(event) =>
253
+ onStartShouldSetResponderCapture?.(event) ??
254
+ (momentum.current ||
255
+ (persistTaps === "never" &&
256
+ keyboardIsDismissible() &&
257
+ typeof event.target !== "number" &&
258
+ !textInputState.isTextInput(event.target)))
259
+ }
260
+ onResponderGrant={(event) => {
261
+ scrolledSinceGrant.current = false;
262
+ grantedDuringMomentum.current = momentum.current;
263
+ onResponderGrant?.(event);
264
+ }}
265
+ onResponderRelease={(event) => {
266
+ onResponderRelease?.(event);
267
+ if (
268
+ persistTaps !== "always" &&
269
+ keyboardIsDismissible() &&
270
+ event.target !== TextInput.State.currentlyFocusedInput() &&
271
+ !scrolledSinceGrant.current &&
272
+ !grantedDuringMomentum.current
273
+ )
274
+ Keyboard.dismiss();
275
+ }}
276
+ onResponderTerminationRequest={(event) =>
277
+ onResponderTerminationRequest?.(event) ?? true
278
+ }
279
+ >
280
+ <View
281
+ collapsable={false}
282
+ style={[
283
+ contentContainerStyle,
284
+ (contentStyle?.flexGrow ?? 0) > 0 &&
285
+ (contentStyle?.minHeight === undefined ||
286
+ typeof contentStyle.minHeight === "number")
287
+ ? {
288
+ minHeight: Math.max(
289
+ contentViewportHeight,
290
+ contentStyle?.minHeight ?? 0
291
+ ),
292
+ }
293
+ : undefined,
294
+ styles.content,
295
+ ]}
296
+ onLayout={(event) => {
297
+ const { width, height } = event.nativeEvent.layout;
298
+ const previous = contentSizeRef.current;
299
+ if (previous.width === width && previous.height === height) return;
300
+ const next = { width, height };
301
+ contentSizeRef.current = next;
302
+ setContentSize(next);
303
+ onContentSizeChange?.(width, height);
304
+ }}
305
+ >
306
+ {children}
307
+ </View>
308
+ </NativeScrollerHost>
32
309
  );
33
310
  });
34
311
 
@@ -0,0 +1,101 @@
1
+ import type * as React from "react";
2
+ import {
3
+ codegenNativeCommands,
4
+ codegenNativeComponent,
5
+ type ColorValue,
6
+ type HostComponent,
7
+ type ViewProps,
8
+ } from "react-native";
9
+ import type {
10
+ DirectEventHandler,
11
+ Double,
12
+ WithDefault,
13
+ } from "react-native/Libraries/Types/CodegenTypes";
14
+
15
+ export type NativeScrollerScrollEvent = Readonly<{
16
+ contentInset: Readonly<{
17
+ top: Double;
18
+ right: Double;
19
+ bottom: Double;
20
+ left: Double;
21
+ }>;
22
+ contentOffset: Readonly<{ x: Double; y: Double }>;
23
+ contentSize: Readonly<{ width: Double; height: Double }>;
24
+ layoutMeasurement: Readonly<{ width: Double; height: Double }>;
25
+ velocity: Readonly<{ x: Double; y: Double }>;
26
+ targetContentOffset: Readonly<{ x: Double; y: Double }>;
27
+ zoomScale: Double;
28
+ }>;
29
+
30
+ export interface NativeProps extends ViewProps {
31
+ contentWidth?: WithDefault<Double, 0>;
32
+ contentHeight?: WithDefault<Double, 0>;
33
+ scrollEnabled?: WithDefault<boolean, true>;
34
+ nestedScrollEnabled?: WithDefault<boolean, true>;
35
+ showsVerticalScrollIndicator?: WithDefault<boolean, true>;
36
+ showsHorizontalScrollIndicator?: WithDefault<boolean, false>;
37
+ bounces?: WithDefault<boolean, true>;
38
+ alwaysBounceVertical?: WithDefault<boolean, true>;
39
+ keyboardDismissMode?: WithDefault<"none" | "on-drag" | "interactive", "none">;
40
+ keyboardShouldPersistTaps?: WithDefault<
41
+ "never" | "always" | "handled",
42
+ "never"
43
+ >;
44
+ scrollEventThrottle?: WithDefault<Double, 0>;
45
+ decelerationRate?: WithDefault<Double, 0.998>;
46
+ contentInsetTop?: WithDefault<Double, 0>;
47
+ contentInsetRight?: WithDefault<Double, 0>;
48
+ contentInsetBottom?: WithDefault<Double, 0>;
49
+ contentInsetLeft?: WithDefault<Double, 0>;
50
+ refreshEnabled?: WithDefault<boolean, false>;
51
+ refreshing?: WithDefault<boolean, false>;
52
+ refreshTintColor?: ColorValue;
53
+ refreshColors?: ReadonlyArray<ColorValue>;
54
+ refreshProgressBackgroundColor?: ColorValue;
55
+ refreshProgressViewOffset?: WithDefault<Double, 0>;
56
+ refreshTitle?: string;
57
+ refreshTitleColor?: ColorValue;
58
+ refreshSize?: WithDefault<"default" | "large", "default">;
59
+ onContentViewportChange?: DirectEventHandler<Readonly<{ height: Double }>>;
60
+ onRefresh?: DirectEventHandler<null>;
61
+ onScroll?: DirectEventHandler<NativeScrollerScrollEvent>;
62
+ onScrollBeginDrag?: DirectEventHandler<NativeScrollerScrollEvent>;
63
+ onScrollEndDrag?: DirectEventHandler<NativeScrollerScrollEvent>;
64
+ onMomentumScrollBegin?: DirectEventHandler<NativeScrollerScrollEvent>;
65
+ onMomentumScrollEnd?: DirectEventHandler<NativeScrollerScrollEvent>;
66
+ }
67
+
68
+ type NativeScrollerHost = HostComponent<NativeProps>;
69
+
70
+ export interface NativeCommands {
71
+ scrollTo: (
72
+ viewRef: React.ElementRef<NativeScrollerHost>,
73
+ x: Double,
74
+ y: Double,
75
+ animated: boolean
76
+ ) => void;
77
+ scrollToEnd: (
78
+ viewRef: React.ElementRef<NativeScrollerHost>,
79
+ animated: boolean
80
+ ) => void;
81
+ flashScrollIndicators: (
82
+ viewRef: React.ElementRef<NativeScrollerHost>
83
+ ) => void;
84
+ setRefreshState: (
85
+ viewRef: React.ElementRef<NativeScrollerHost>,
86
+ refreshing: boolean
87
+ ) => void;
88
+ }
89
+
90
+ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
91
+ supportedCommands: [
92
+ "scrollTo",
93
+ "scrollToEnd",
94
+ "flashScrollIndicators",
95
+ "setRefreshState",
96
+ ],
97
+ });
98
+
99
+ export default codegenNativeComponent<NativeProps>("RNCNativeScroller", {
100
+ interfaceOnly: true,
101
+ }) as NativeScrollerHost;