react-native-keyboard-controller 1.20.6 → 1.21.0-beta.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 (80) hide show
  1. package/android/.settings/org.eclipse.buildship.core.prefs +2 -0
  2. package/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +2 -0
  3. package/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +30 -0
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +1 -1
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/ClippingScrollViewDecoratorViewManagerImpl.kt +13 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +37 -0
  7. package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +24 -0
  8. package/ios/KeyboardController.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
  9. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  10. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  11. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcuserdata/kirylziusko.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  12. package/ios/Tests/Tests.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +32 -0
  13. package/ios/animations/KeyboardAnimation.swift +2 -2
  14. package/ios/animations/SpringAnimation.swift +1 -1
  15. package/ios/animations/TimingAnimation.swift +1 -3
  16. package/ios/delegates/KCTextInputCompositeDelegate.swift +2 -2
  17. package/ios/extensions/CGFloat.swift +1 -3
  18. package/ios/interactive/KeyboardAreaExtender.swift +2 -1
  19. package/ios/observers/FocusedInputObserver.swift +4 -2
  20. package/ios/observers/movement/KeyboardTrackingView.swift +4 -1
  21. package/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift +4 -0
  22. package/ios/protocols/TextInput.swift +1 -1
  23. package/ios/swizzling/UIResponderSwizzle.swift +2 -4
  24. package/ios/traversal/FocusedInputHolder.swift +3 -3
  25. package/ios/traversal/KeyboardView.swift +1 -1
  26. package/ios/traversal/ViewHierarchyNavigator.swift +2 -2
  27. package/ios/views/KeyboardControllerViewManager.swift +5 -5
  28. package/lib/commonjs/bindings.js +7 -1
  29. package/lib/commonjs/bindings.js.map +1 -1
  30. package/lib/commonjs/bindings.native.js +4 -1
  31. package/lib/commonjs/bindings.native.js.map +1 -1
  32. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +80 -26
  33. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  34. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +43 -0
  35. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -0
  36. package/lib/commonjs/components/ScrollViewWithBottomPadding/styles.js +14 -0
  37. package/lib/commonjs/components/ScrollViewWithBottomPadding/styles.js.map +1 -0
  38. package/lib/commonjs/components/hooks/useCombinedRef.js +24 -0
  39. package/lib/commonjs/components/hooks/useCombinedRef.js.map +1 -0
  40. package/lib/commonjs/components/hooks/useScrollState.js +43 -0
  41. package/lib/commonjs/components/hooks/useScrollState.js.map +1 -0
  42. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js +12 -0
  43. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -0
  44. package/lib/commonjs/types/views.js.map +1 -1
  45. package/lib/module/bindings.js +6 -0
  46. package/lib/module/bindings.js.map +1 -1
  47. package/lib/module/bindings.native.js +3 -0
  48. package/lib/module/bindings.native.js.map +1 -1
  49. package/lib/module/components/KeyboardAwareScrollView/index.js +80 -27
  50. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  51. package/lib/module/components/ScrollViewWithBottomPadding/index.js +35 -0
  52. package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -0
  53. package/lib/module/components/ScrollViewWithBottomPadding/styles.js +8 -0
  54. package/lib/module/components/ScrollViewWithBottomPadding/styles.js.map +1 -0
  55. package/lib/module/components/hooks/useCombinedRef.js +18 -0
  56. package/lib/module/components/hooks/useCombinedRef.js.map +1 -0
  57. package/lib/module/components/hooks/useScrollState.js +37 -0
  58. package/lib/module/components/hooks/useScrollState.js.map +1 -0
  59. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js +6 -0
  60. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -0
  61. package/lib/module/types/views.js.map +1 -1
  62. package/lib/typescript/bindings.d.ts +7 -1
  63. package/lib/typescript/bindings.native.d.ts +1 -0
  64. package/lib/typescript/components/KeyboardAwareScrollView/index.d.ts +3 -2
  65. package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +12 -0
  66. package/lib/typescript/components/ScrollViewWithBottomPadding/styles.d.ts +6 -0
  67. package/lib/typescript/components/hooks/useCombinedRef.d.ts +3 -0
  68. package/lib/typescript/components/hooks/useScrollState.d.ts +14 -0
  69. package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +8 -0
  70. package/lib/typescript/types/views.d.ts +4 -0
  71. package/package.json +4 -3
  72. package/src/bindings.native.ts +5 -0
  73. package/src/bindings.ts +8 -0
  74. package/src/components/KeyboardAwareScrollView/index.tsx +113 -33
  75. package/src/components/ScrollViewWithBottomPadding/index.tsx +72 -0
  76. package/src/components/ScrollViewWithBottomPadding/styles.ts +9 -0
  77. package/src/components/hooks/useCombinedRef.ts +22 -0
  78. package/src/components/hooks/useScrollState.ts +59 -0
  79. package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +17 -0
  80. package/src/types/views.ts +6 -0
@@ -0,0 +1,3 @@
1
+ import type { Ref, RefCallback } from "react";
2
+ declare const useCombinedRef: <T>(...refs: Ref<T>[]) => RefCallback<T>;
3
+ export default useCombinedRef;
@@ -0,0 +1,14 @@
1
+ import type { AnimatedRef } from "react-native-reanimated";
2
+ import type Reanimated from "react-native-reanimated";
3
+ declare const useScrollState: (ref: AnimatedRef<Reanimated.ScrollView>) => {
4
+ offset: import("react-native-reanimated").SharedValue<number>;
5
+ layout: import("react-native-reanimated").SharedValue<{
6
+ width: number;
7
+ height: number;
8
+ }>;
9
+ size: import("react-native-reanimated").SharedValue<{
10
+ width: number;
11
+ height: number;
12
+ }>;
13
+ };
14
+ export default useScrollState;
@@ -0,0 +1,8 @@
1
+ import type { HostComponent } from "react-native";
2
+ import type { ViewProps } from "react-native";
3
+ import type { Double } from "react-native/Libraries/Types/CodegenTypes";
4
+ export interface NativeProps extends ViewProps {
5
+ contentInsetBottom: Double;
6
+ }
7
+ declare const _default: HostComponent<NativeProps>;
8
+ export default _default;
@@ -48,3 +48,7 @@ export type KeyboardExtenderProps = PropsWithChildren<{
48
48
  /** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */
49
49
  enabled?: boolean;
50
50
  }>;
51
+ export type ClippingScrollViewProps = PropsWithChildren<ViewProps & {
52
+ /** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
53
+ contentInsetBottom?: number;
54
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.20.6",
3
+ "version": "1.21.0-beta.0",
4
4
  "description": "Keyboard manager which works in identical way on both iOS and Android",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -41,7 +41,7 @@
41
41
  "typescript": "tsc --noEmit --project tsconfig.build.json",
42
42
  "lint": "eslint --quiet \"**/*.{js,ts,tsx}\"",
43
43
  "lint-clang": "find ios/ -iname *.h -o -iname *.m -o -iname *.mm | grep -v -e Pods -e build | xargs clang-format -i -n --Werror",
44
- "prepare": "bob build > /dev/null 2>&1",
44
+ "prepare": "bob build",
45
45
  "release": "release-it",
46
46
  "example": "yarn --cwd example",
47
47
  "pods": "cd example && pod-install --quiet",
@@ -187,5 +187,6 @@
187
187
  "KeyboardExtender": "KeyboardExtender"
188
188
  }
189
189
  }
190
- }
190
+ },
191
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
191
192
  }
@@ -1,6 +1,7 @@
1
1
  import { NativeEventEmitter, Platform } from "react-native";
2
2
 
3
3
  import type {
4
+ ClippingScrollViewProps,
4
5
  FocusedInputEventsModule,
5
6
  KeyboardBackgroundViewProps,
6
7
  KeyboardControllerNativeModule,
@@ -71,3 +72,7 @@ export const RCTKeyboardExtender: React.FC<KeyboardExtenderProps> =
71
72
  Platform.OS === "ios"
72
73
  ? require("./specs/KeyboardExtenderNativeComponent").default
73
74
  : ({ children }: KeyboardExtenderProps) => children;
75
+ export const ClippingScrollView: React.FC<KeyboardBackgroundViewProps> =
76
+ Platform.OS === "android"
77
+ ? require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default
78
+ : ({ children }: ClippingScrollViewProps) => children;
package/src/bindings.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { View } from "react-native";
2
2
 
3
3
  import type {
4
+ ClippingScrollViewProps,
4
5
  FocusedInputEventsModule,
5
6
  KeyboardBackgroundViewProps,
6
7
  KeyboardControllerNativeModule,
@@ -82,3 +83,10 @@ export const KeyboardBackgroundView =
82
83
  */
83
84
  export const RCTKeyboardExtender =
84
85
  View as unknown as React.FC<KeyboardExtenderProps>;
86
+ /**
87
+ * A decorator that will clip the content of the `ScrollView`. It helps to simulate `contentInset` behavior on Android.
88
+ * Supports only `bottom` property (`paddingBottom` is not supported property of `ScrollView.style`).
89
+ * Using this component we can modify bottom inset without having a fake view.
90
+ */
91
+ export const ClippingScrollView =
92
+ View as unknown as React.FC<ClippingScrollViewProps>;
@@ -12,8 +12,7 @@ import Reanimated, {
12
12
  scrollTo,
13
13
  useAnimatedReaction,
14
14
  useAnimatedRef,
15
- useAnimatedStyle,
16
- useScrollViewOffset,
15
+ useDerivedValue,
17
16
  useSharedValue,
18
17
  } from "react-native-reanimated";
19
18
 
@@ -23,10 +22,14 @@ import {
23
22
  useWindowDimensions,
24
23
  } from "../../hooks";
25
24
  import { findNodeHandle } from "../../utils/findNodeHandle";
25
+ import useCombinedRef from "../hooks/useCombinedRef";
26
+ import useScrollState from "../hooks/useScrollState";
27
+ import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
26
28
 
27
29
  import { useSmoothKeyboardHandler } from "./useSmoothKeyboardHandler";
28
30
  import { debounce, scrollDistanceWithRespectToSnapPoints } from "./utils";
29
31
 
32
+ import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
30
33
  import type {
31
34
  LayoutChangeEvent,
32
35
  ScrollView,
@@ -48,7 +51,7 @@ export type KeyboardAwareScrollViewProps = {
48
51
  /** Adjusting the bottom spacing of KeyboardAwareScrollView. Default is `0`. */
49
52
  extraKeyboardSpace?: number;
50
53
  /** Custom component for `ScrollView`. Default is `ScrollView`. */
51
- ScrollViewComponent?: React.ComponentType<ScrollViewProps>;
54
+ ScrollViewComponent?: AnimatedScrollViewComponent;
52
55
  } & ScrollViewProps;
53
56
  export type KeyboardAwareScrollViewRef = {
54
57
  assureFocusedInputVisible: () => void;
@@ -127,9 +130,14 @@ const KeyboardAwareScrollView = forwardRef<
127
130
  ) => {
128
131
  const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
129
132
  const scrollViewRef = React.useRef<ScrollView>(null);
133
+ const onRef = useCombinedRef(scrollViewAnimatedRef, scrollViewRef);
130
134
  const scrollViewTarget = useSharedValue<number | null>(null);
131
135
  const scrollPosition = useSharedValue(0);
132
- const position = useScrollViewOffset(scrollViewAnimatedRef);
136
+ const {
137
+ offset: position,
138
+ layout: scrollViewLayout,
139
+ size: scrollViewContentSize,
140
+ } = useScrollState(scrollViewAnimatedRef);
133
141
  const currentKeyboardFrameHeight = useSharedValue(0);
134
142
  const keyboardHeight = useSharedValue(0);
135
143
  const keyboardWillAppear = useSharedValue(false);
@@ -140,14 +148,11 @@ const KeyboardAwareScrollView = forwardRef<
140
148
  const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);
141
149
  const lastSelection =
142
150
  useSharedValue<FocusedInputSelectionChangedEvent | null>(null);
151
+ const ghostViewSpace = useSharedValue(-1);
152
+ const pendingSelectionForFocus = useSharedValue(false);
143
153
 
144
154
  const { height } = useWindowDimensions();
145
155
 
146
- const onRef = useCallback((assignedRef: Reanimated.ScrollView) => {
147
- scrollViewRef.current = assignedRef;
148
-
149
- scrollViewAnimatedRef(assignedRef);
150
- }, []);
151
156
  const onScrollViewLayout = useCallback(
152
157
  (e: LayoutChangeEvent) => {
153
158
  scrollViewTarget.value = findNodeHandle(scrollViewAnimatedRef.current);
@@ -216,6 +221,30 @@ const KeyboardAwareScrollView = forwardRef<
216
221
  },
217
222
  [bottomOffset, enabled, height, snapToOffsets],
218
223
  );
224
+ const removeGhostPadding = useCallback((e: number) => {
225
+ "worklet";
226
+
227
+ // new `ScrollViewWithBottomPadding` behavior: if we hide keyboard and we are in the end of `ScrollView`
228
+ // then we always need to scroll back, because we apply a padding that doesn't change layout, so we will
229
+ // not have auto scroll back in this case
230
+ if (!keyboardWillAppear.value && ghostViewSpace.value > 0) {
231
+ scrollTo(
232
+ scrollViewAnimatedRef,
233
+ 0,
234
+ scrollPosition.value -
235
+ interpolate(
236
+ e,
237
+ [initialKeyboardSize.value, keyboardHeight.value],
238
+ [ghostViewSpace.value, 0],
239
+ ),
240
+ false,
241
+ );
242
+
243
+ return true;
244
+ }
245
+
246
+ return false;
247
+ }, []);
219
248
  const performScrollWithPositionRestoration = useCallback(
220
249
  (newPosition: number) => {
221
250
  "worklet";
@@ -296,7 +325,18 @@ const KeyboardAwareScrollView = forwardRef<
296
325
  lastSelection.value = e;
297
326
 
298
327
  if (e.target !== lastTarget) {
299
- // ignore this event, because "focus changed" event handled in `useSmoothKeyboardHandler`
328
+ if (pendingSelectionForFocus.value) {
329
+ // selection arrived after onStart - complete the deferred setup
330
+ pendingSelectionForFocus.value = false;
331
+ updateLayoutFromSelection();
332
+
333
+ // if keyboard was already visible (focus change, no onMove expected),
334
+ // perform the deferred scroll now
335
+ if (!keyboardWillAppear.value && keyboardHeight.value > 0) {
336
+ position.value += maybeScroll(keyboardHeight.value, true);
337
+ }
338
+ }
339
+
300
340
  return;
301
341
  }
302
342
  // caret in the end + end coordinates has been changed -> we moved to a new line
@@ -314,7 +354,12 @@ const KeyboardAwareScrollView = forwardRef<
314
354
 
315
355
  onChangeTextHandler();
316
356
  },
317
- [scrollFromCurrentPosition, onChangeTextHandler],
357
+ [
358
+ scrollFromCurrentPosition,
359
+ onChangeTextHandler,
360
+ updateLayoutFromSelection,
361
+ maybeScroll,
362
+ ],
318
363
  );
319
364
 
320
365
  useFocusedInputHandler(
@@ -347,6 +392,7 @@ const KeyboardAwareScrollView = forwardRef<
347
392
  // on back transition need to interpolate as [0, keyboardHeight]
348
393
  initialKeyboardSize.value = 0;
349
394
  scrollPosition.value = scrollBeforeKeyboardMovement.value;
395
+ pendingSelectionForFocus.value = false;
350
396
  }
351
397
 
352
398
  if (
@@ -363,17 +409,40 @@ const KeyboardAwareScrollView = forwardRef<
363
409
  // focus was changed
364
410
  if (focusWasChanged) {
365
411
  tag.value = e.target;
366
- // save position of focused text input when keyboard starts to move
367
- updateLayoutFromSelection();
412
+
413
+ if (lastSelection.value?.target === e.target) {
414
+ // selection arrived before onStart - use it to update layout
415
+ updateLayoutFromSelection();
416
+ pendingSelectionForFocus.value = false;
417
+ } else {
418
+ // selection hasn't arrived yet for the new target.
419
+ // use input layout as-is; will be refined when selection arrives.
420
+ if (input.value) {
421
+ layout.value = input.value;
422
+ }
423
+ pendingSelectionForFocus.value = true;
424
+ }
425
+
368
426
  // save current scroll position - when keyboard will hide we'll reuse
369
427
  // this value to achieve smooth hide effect
370
428
  scrollBeforeKeyboardMovement.value = position.value;
371
429
  }
372
430
 
373
431
  if (focusWasChanged && !keyboardWillAppear.value) {
374
- // update position on scroll value, so `onEnd` handler
375
- // will pick up correct values
376
- position.value += maybeScroll(e.height, true);
432
+ if (!pendingSelectionForFocus.value) {
433
+ // update position on scroll value, so `onEnd` handler
434
+ // will pick up correct values
435
+ position.value += maybeScroll(e.height, true);
436
+ }
437
+ }
438
+
439
+ ghostViewSpace.value =
440
+ position.value +
441
+ scrollViewLayout.value.height -
442
+ scrollViewContentSize.value.height;
443
+
444
+ if (ghostViewSpace.value > 0) {
445
+ scrollPosition.value = position.value;
377
446
  }
378
447
  },
379
448
  onMove: (e) => {
@@ -381,6 +450,10 @@ const KeyboardAwareScrollView = forwardRef<
381
450
 
382
451
  syncKeyboardFrame(e);
383
452
 
453
+ if (removeGhostPadding(e.height)) {
454
+ return;
455
+ }
456
+
384
457
  // if the user has set disableScrollOnKeyboardHide, only auto-scroll when the keyboard opens
385
458
  if (!disableScrollOnKeyboardHide || keyboardWillAppear.value) {
386
459
  maybeScroll(e.height);
@@ -389,13 +462,24 @@ const KeyboardAwareScrollView = forwardRef<
389
462
  onEnd: (e) => {
390
463
  "worklet";
391
464
 
465
+ removeGhostPadding(e.height);
466
+
392
467
  keyboardHeight.value = e.height;
393
468
  scrollPosition.value = position.value;
394
469
 
470
+ if (e.height === 0) {
471
+ lastSelection.value = null;
472
+ }
473
+
395
474
  syncKeyboardFrame(e);
396
475
  },
397
476
  },
398
- [maybeScroll, disableScrollOnKeyboardHide, syncKeyboardFrame],
477
+ [
478
+ maybeScroll,
479
+ removeGhostPadding,
480
+ disableScrollOnKeyboardHide,
481
+ syncKeyboardFrame,
482
+ ],
399
483
  );
400
484
 
401
485
  const synchronize = useCallback(async () => {
@@ -446,32 +530,28 @@ const KeyboardAwareScrollView = forwardRef<
446
530
  [],
447
531
  );
448
532
 
449
- const view = useAnimatedStyle(
450
- () =>
451
- enabled
452
- ? {
453
- // animations become choppy when scrolling to the end of the `ScrollView` (when the last input is focused)
454
- // this happens because the layout recalculates on every frame. To avoid this we slightly increase padding
455
- // by `+1`. In this way we assure, that `scrollTo` will never scroll to the end, because it uses interpolation
456
- // from 0 to `keyboardHeight`, and here our padding is `keyboardHeight + 1`. It allows us not to re-run layout
457
- // re-calculation on every animation frame and it helps to achieve smooth animation.
458
- // see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
459
- paddingBottom: currentKeyboardFrameHeight.value + 1,
460
- }
461
- : {},
533
+ // animations become choppy when scrolling to the end of the `ScrollView` (when the last input is focused)
534
+ // this happens because the layout recalculates on every frame. To avoid this we slightly increase padding
535
+ // by `+1`. In this way we assure, that `scrollTo` will never scroll to the end, because it uses interpolation
536
+ // from 0 to `keyboardHeight`, and here our padding is `keyboardHeight + 1`. It allows us not to re-run layout
537
+ // re-calculation on every animation frame and it helps to achieve smooth animation.
538
+ // see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
539
+ const padding = useDerivedValue(
540
+ () => (enabled ? currentKeyboardFrameHeight.value + 1 : 0),
462
541
  [enabled],
463
542
  );
464
543
 
465
544
  return (
466
- <ScrollViewComponent
545
+ <ScrollViewWithBottomPadding
467
546
  ref={onRef}
468
547
  {...rest}
548
+ bottomPadding={padding}
469
549
  scrollEventThrottle={16}
550
+ ScrollViewComponent={ScrollViewComponent}
470
551
  onLayout={onScrollViewLayout}
471
552
  >
472
553
  {children}
473
- {enabled && <Reanimated.View style={view} />}
474
- </ScrollViewComponent>
554
+ </ScrollViewWithBottomPadding>
475
555
  );
476
556
  },
477
557
  );
@@ -0,0 +1,72 @@
1
+ import React, { forwardRef } from "react";
2
+ import { Platform } from "react-native";
3
+ import Reanimated, { useAnimatedProps } from "react-native-reanimated";
4
+
5
+ import { ClippingScrollView } from "../../bindings";
6
+
7
+ import styles from "./styles";
8
+
9
+ import type { ScrollViewProps } from "react-native";
10
+ import type { SharedValue } from "react-native-reanimated";
11
+
12
+ const ReanimatedClippingScrollView =
13
+ Platform.OS === "android"
14
+ ? Reanimated.createAnimatedComponent(ClippingScrollView)
15
+ : ClippingScrollView;
16
+
17
+ type AnimatedScrollViewProps = React.ComponentProps<
18
+ typeof Reanimated.ScrollView
19
+ >;
20
+
21
+ export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<
22
+ AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>
23
+ >;
24
+
25
+ type ScrollViewWithBottomPaddingProps = {
26
+ ScrollViewComponent: AnimatedScrollViewComponent;
27
+ children?: React.ReactNode;
28
+ bottomPadding: SharedValue<number>;
29
+ } & ScrollViewProps;
30
+
31
+ const ScrollViewWithBottomPadding = forwardRef<
32
+ Reanimated.ScrollView,
33
+ ScrollViewWithBottomPaddingProps
34
+ >(
35
+ (
36
+ { ScrollViewComponent, bottomPadding, contentInset, children, ...rest },
37
+ ref,
38
+ ) => {
39
+ const animatedProps = useAnimatedProps(
40
+ () => ({
41
+ // iOS prop
42
+ contentInset: {
43
+ bottom: bottomPadding.value + (contentInset?.bottom || 0),
44
+ top: contentInset?.top,
45
+ right: contentInset?.right,
46
+ left: contentInset?.left,
47
+ },
48
+ // Android prop
49
+ contentInsetBottom: bottomPadding.value,
50
+ }),
51
+ [
52
+ contentInset?.bottom,
53
+ contentInset?.top,
54
+ contentInset?.right,
55
+ contentInset?.left,
56
+ ],
57
+ );
58
+
59
+ return (
60
+ <ReanimatedClippingScrollView
61
+ animatedProps={animatedProps}
62
+ style={styles.container}
63
+ >
64
+ <ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
65
+ {children}
66
+ </ScrollViewComponent>
67
+ </ReanimatedClippingScrollView>
68
+ );
69
+ },
70
+ );
71
+
72
+ export default ScrollViewWithBottomPadding;
@@ -0,0 +1,9 @@
1
+ import { StyleSheet } from "react-native";
2
+
3
+ const styles = StyleSheet.create({
4
+ container: {
5
+ flex: 1,
6
+ },
7
+ });
8
+
9
+ export default styles;
@@ -0,0 +1,22 @@
1
+ import { useCallback } from "react";
2
+
3
+ import type { Ref, RefCallback } from "react";
4
+
5
+ const useCombinedRef = <T>(...refs: Ref<T>[]): RefCallback<T> => {
6
+ return useCallback((value: T | null) => {
7
+ for (const ref of refs) {
8
+ if (!ref) {
9
+ continue;
10
+ }
11
+
12
+ if (typeof ref === "function") {
13
+ ref(value);
14
+ } else {
15
+ ref.current = value;
16
+ }
17
+ }
18
+ // eslint-disable-next-line react-compiler/react-compiler
19
+ }, refs);
20
+ };
21
+
22
+ export default useCombinedRef;
@@ -0,0 +1,59 @@
1
+ import { useEffect } from "react";
2
+ import { useEvent, useSharedValue } from "react-native-reanimated";
3
+
4
+ import { useEventHandlerRegistration } from "../../internal";
5
+
6
+ import type { AnimatedRef } from "react-native-reanimated";
7
+ import type Reanimated from "react-native-reanimated";
8
+
9
+ const NATIVE_SCROLL_EVENT_NAMES = [
10
+ "onScroll",
11
+ "onScrollBeginDrag",
12
+ "onScrollEndDrag",
13
+ "onMomentumScrollBegin",
14
+ "onMomentumScrollEnd",
15
+ ];
16
+
17
+ type ScrollEvent = {
18
+ contentOffset: {
19
+ x: number;
20
+ y: number;
21
+ };
22
+ layoutMeasurement: {
23
+ width: number;
24
+ height: number;
25
+ };
26
+ contentSize: {
27
+ width: number;
28
+ height: number;
29
+ };
30
+ };
31
+
32
+ const useScrollState = (ref: AnimatedRef<Reanimated.ScrollView>) => {
33
+ const offset = useSharedValue(0);
34
+ const layout = useSharedValue({ width: 0, height: 0 });
35
+ const size = useSharedValue({ width: 0, height: 0 });
36
+
37
+ const register = useEventHandlerRegistration(ref);
38
+
39
+ const eventHandler = useEvent((event: ScrollEvent) => {
40
+ "worklet";
41
+
42
+ // eslint-disable-next-line react-compiler/react-compiler
43
+ offset.value = event.contentOffset.y;
44
+ layout.value = event.layoutMeasurement;
45
+ size.value = event.contentSize;
46
+ }, NATIVE_SCROLL_EVENT_NAMES);
47
+
48
+ useEffect(() => {
49
+ const cleanup = register(eventHandler);
50
+
51
+ return () => {
52
+ cleanup();
53
+ };
54
+ }, []);
55
+
56
+ return { offset, layout, size };
57
+ };
58
+
59
+ export default useScrollState;
@@ -0,0 +1,17 @@
1
+ import { codegenNativeComponent } from "react-native";
2
+
3
+ import type { HostComponent } from "react-native";
4
+ import type { ViewProps } from "react-native";
5
+ import type { Double } from "react-native/Libraries/Types/CodegenTypes";
6
+
7
+ export interface NativeProps extends ViewProps {
8
+ contentInsetBottom: Double;
9
+ }
10
+
11
+ export default codegenNativeComponent<NativeProps>(
12
+ "ClippingScrollViewDecoratorView",
13
+ {
14
+ interfaceOnly: true,
15
+ excludedPlatforms: ["iOS"],
16
+ },
17
+ ) as HostComponent<NativeProps>;
@@ -49,3 +49,9 @@ export type KeyboardExtenderProps = PropsWithChildren<{
49
49
  /** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */
50
50
  enabled?: boolean;
51
51
  }>;
52
+ export type ClippingScrollViewProps = PropsWithChildren<
53
+ ViewProps & {
54
+ /** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
55
+ contentInsetBottom?: number;
56
+ }
57
+ >;